index.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import {css} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import sentryPattern from 'sentry-images/pattern/sentry-pattern.png';
  4. import {Alert} from 'sentry/components/alert';
  5. import ApiForm from 'sentry/components/forms/apiForm';
  6. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  7. import {t} from 'sentry/locale';
  8. import ConfigStore from 'sentry/stores/configStore';
  9. import {space} from 'sentry/styles/space';
  10. import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
  11. import {Field, getForm, getOptionDefault, getOptionField} from '../options';
  12. export type InstallWizardProps = DeprecatedAsyncView['props'] & {
  13. onConfigured: () => void;
  14. };
  15. export type InstallWizardOptions = Record<
  16. string,
  17. {
  18. field: Field;
  19. value?: unknown;
  20. }
  21. >;
  22. type State = DeprecatedAsyncView['state'] & {
  23. data: null | InstallWizardOptions;
  24. };
  25. export default class InstallWizard extends DeprecatedAsyncView<
  26. InstallWizardProps,
  27. State
  28. > {
  29. getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
  30. return [['data', '/internal/options/?query=is:required']];
  31. }
  32. renderFormFields() {
  33. const options = this.state.data!;
  34. let missingOptions = new Set(
  35. Object.keys(options).filter(option => !options[option].field.isSet)
  36. );
  37. // This is to handle the initial installation case.
  38. // Even if all options are filled out, we want to prompt to confirm
  39. // them. This is a bit of a hack because we're assuming that
  40. // the backend only spit back all filled out options for
  41. // this case.
  42. if (missingOptions.size === 0) {
  43. missingOptions = new Set(Object.keys(options));
  44. }
  45. // A mapping of option name to Field object
  46. const fields = {};
  47. for (const key of missingOptions) {
  48. const option = options[key];
  49. if (option.field.disabled) {
  50. continue;
  51. }
  52. fields[key] = getOptionField(key, option.field);
  53. }
  54. return getForm(fields);
  55. }
  56. getInitialData() {
  57. const options = this.state.data!;
  58. const data = {};
  59. Object.keys(options).forEach(optionName => {
  60. const option = options[optionName];
  61. if (option.field.disabled) {
  62. return;
  63. }
  64. // TODO(dcramer): we need to rethink this logic as doing multiple "is this value actually set"
  65. // is problematic
  66. // all values to their server-defaults (as client-side defaults don't really work)
  67. const displayValue = option.value || getOptionDefault(optionName);
  68. if (
  69. // XXX(dcramer): we need the user to explicitly choose beacon.anonymous
  70. // vs using an implied default so effectively this is binding
  71. optionName !== 'beacon.anonymous' &&
  72. // XXX(byk): if we don't have a set value but have a default value filled
  73. // instead, from the client, set it on the data so it is sent to the server
  74. !option.field.isSet &&
  75. displayValue !== undefined
  76. ) {
  77. data[optionName] = displayValue;
  78. }
  79. });
  80. return data;
  81. }
  82. getTitle() {
  83. return t('Setup Sentry');
  84. }
  85. render() {
  86. const version = ConfigStore.get('version');
  87. return (
  88. <SentryDocumentTitle noSuffix title={this.getTitle()}>
  89. <Wrapper>
  90. <Pattern />
  91. <SetupWizard>
  92. <Heading>
  93. <span>{t('Welcome to Sentry')}</span>
  94. <Version>{version.current}</Version>
  95. </Heading>
  96. {this.state.loading
  97. ? this.renderLoading()
  98. : this.state.error
  99. ? this.renderError()
  100. : this.renderBody()}
  101. </SetupWizard>
  102. </Wrapper>
  103. </SentryDocumentTitle>
  104. );
  105. }
  106. renderError() {
  107. return (
  108. <Alert type="error" showIcon>
  109. {t(
  110. 'We were unable to load the required configuration from the Sentry server. Please take a look at the service logs.'
  111. )}
  112. </Alert>
  113. );
  114. }
  115. renderBody() {
  116. return (
  117. <ApiForm
  118. apiMethod="PUT"
  119. apiEndpoint={this.getEndpoints()[0][1]}
  120. submitLabel={t('Continue')}
  121. initialData={this.getInitialData()}
  122. onSubmitSuccess={this.props.onConfigured}
  123. >
  124. <p>{t('Complete setup by filling out the required configuration.')}</p>
  125. {this.renderFormFields()}
  126. </ApiForm>
  127. );
  128. }
  129. }
  130. const Wrapper = styled('div')`
  131. display: flex;
  132. justify-content: center;
  133. `;
  134. const fixedStyle = css`
  135. position: fixed;
  136. top: 0;
  137. right: 0;
  138. bottom: 0;
  139. left: 0;
  140. `;
  141. const Pattern = styled('div')`
  142. z-index: -1;
  143. &::before {
  144. ${fixedStyle}
  145. content: '';
  146. background-image: linear-gradient(
  147. to right,
  148. ${p => p.theme.purple200} 0%,
  149. ${p => p.theme.purple300} 100%
  150. );
  151. background-repeat: repeat-y;
  152. }
  153. &::after {
  154. ${fixedStyle}
  155. content: '';
  156. background: url(${sentryPattern});
  157. background-size: 400px;
  158. opacity: 0.8;
  159. }
  160. `;
  161. const Heading = styled('h1')`
  162. display: grid;
  163. gap: ${space(1)};
  164. justify-content: space-between;
  165. grid-auto-flow: column;
  166. line-height: 36px;
  167. `;
  168. const Version = styled('small')`
  169. font-size: ${p => p.theme.fontSizeExtraLarge};
  170. line-height: inherit;
  171. `;
  172. const SetupWizard = styled('div')`
  173. background: ${p => p.theme.background};
  174. border-radius: ${p => p.theme.borderRadius};
  175. box-shadow: ${p => p.theme.dropShadowHeavy};
  176. margin-top: 40px;
  177. padding: 40px 40px 20px;
  178. width: 600px;
  179. z-index: ${p => p.theme.zIndex.initial};
  180. `;