index.tsx 5.1 KB

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