index.tsx 5.4 KB

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