123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- import {css} from '@emotion/react';
- import styled from '@emotion/styled';
- import sentryPattern from 'sentry-images/pattern/sentry-pattern.png';
- import {Alert} from 'sentry/components/alert';
- import ApiForm from 'sentry/components/forms/apiForm';
- import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
- import {t} from 'sentry/locale';
- import ConfigStore from 'sentry/stores/configStore';
- import {space} from 'sentry/styles/space';
- import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
- import {Field, getForm, getOptionDefault, getOptionField} from '../options';
- export type InstallWizardProps = DeprecatedAsyncView['props'] & {
- onConfigured: () => void;
- };
- export type InstallWizardOptions = Record<
- string,
- {
- field: Field;
- value?: unknown;
- }
- >;
- type State = DeprecatedAsyncView['state'] & {
- data: null | InstallWizardOptions;
- };
- export default class InstallWizard extends DeprecatedAsyncView<
- InstallWizardProps,
- State
- > {
- getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
- return [['data', '/internal/options/?query=is:required']];
- }
- renderFormFields() {
- const options = this.state.data!;
- let missingOptions = new Set(
- Object.keys(options).filter(option => !options[option].field.isSet)
- );
- // This is to handle the initial installation case.
- // Even if all options are filled out, we want to prompt to confirm
- // them. This is a bit of a hack because we're assuming that
- // the backend only spit back all filled out options for
- // this case.
- if (missingOptions.size === 0) {
- missingOptions = new Set(Object.keys(options));
- }
- // A mapping of option name to Field object
- const fields = {};
- for (const key of missingOptions) {
- const option = options[key];
- if (option.field.disabled) {
- continue;
- }
- fields[key] = getOptionField(key, option.field);
- }
- return getForm(fields);
- }
- getInitialData() {
- const options = this.state.data!;
- const data = {};
- Object.keys(options).forEach(optionName => {
- const option = options[optionName];
- if (option.field.disabled) {
- return;
- }
- // TODO(dcramer): we need to rethink this logic as doing multiple "is this value actually set"
- // is problematic
- // all values to their server-defaults (as client-side defaults don't really work)
- const displayValue = option.value || getOptionDefault(optionName);
- if (
- // XXX(dcramer): we need the user to explicitly choose beacon.anonymous
- // vs using an implied default so effectively this is binding
- optionName !== 'beacon.anonymous' &&
- // XXX(byk): if we don't have a set value but have a default value filled
- // instead, from the client, set it on the data so it is sent to the server
- !option.field.isSet &&
- displayValue !== undefined
- ) {
- data[optionName] = displayValue;
- }
- });
- return data;
- }
- getTitle() {
- return t('Setup Sentry');
- }
- render() {
- const version = ConfigStore.get('version');
- return (
- <SentryDocumentTitle noSuffix title={this.getTitle()}>
- <Wrapper>
- <Pattern />
- <SetupWizard>
- <Heading>
- <span>{t('Welcome to Sentry')}</span>
- <Version>{version.current}</Version>
- </Heading>
- {this.state.loading
- ? this.renderLoading()
- : this.state.error
- ? this.renderError()
- : this.renderBody()}
- </SetupWizard>
- </Wrapper>
- </SentryDocumentTitle>
- );
- }
- renderError() {
- return (
- <Alert type="error" showIcon>
- {t(
- 'We were unable to load the required configuration from the Sentry server. Please take a look at the service logs.'
- )}
- </Alert>
- );
- }
- renderBody() {
- return (
- <ApiForm
- apiMethod="PUT"
- apiEndpoint={this.getEndpoints()[0][1]}
- submitLabel={t('Continue')}
- initialData={this.getInitialData()}
- onSubmitSuccess={this.props.onConfigured}
- >
- <p>{t('Complete setup by filling out the required configuration.')}</p>
- {this.renderFormFields()}
- </ApiForm>
- );
- }
- }
- const Wrapper = styled('div')`
- display: flex;
- justify-content: center;
- `;
- const fixedStyle = css`
- position: fixed;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- `;
- const Pattern = styled('div')`
- z-index: -1;
- &::before {
- ${fixedStyle}
- content: '';
- background-image: linear-gradient(
- to right,
- ${p => p.theme.purple200} 0%,
- ${p => p.theme.purple300} 100%
- );
- background-repeat: repeat-y;
- }
- &::after {
- ${fixedStyle}
- content: '';
- background: url(${sentryPattern});
- background-size: 400px;
- opacity: 0.8;
- }
- `;
- const Heading = styled('h1')`
- display: grid;
- gap: ${space(1)};
- justify-content: space-between;
- grid-auto-flow: column;
- line-height: 36px;
- `;
- const Version = styled('small')`
- font-size: ${p => p.theme.fontSizeExtraLarge};
- line-height: inherit;
- `;
- const SetupWizard = styled('div')`
- background: ${p => p.theme.background};
- border-radius: ${p => p.theme.borderRadius};
- box-shadow: ${p => p.theme.dropShadowHeavy};
- margin-top: 40px;
- padding: 40px 40px 20px;
- width: 600px;
- z-index: ${p => p.theme.zIndex.initial};
- `;
|