index.tsx 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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 Form from 'sentry/components/forms/form';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  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 {useApiQuery} from 'sentry/utils/queryClient';
  12. import type {Field} from '../options';
  13. import {getForm, getOptionDefault, getOptionField} from '../options';
  14. export type InstallWizardProps = {
  15. onConfigured: () => void;
  16. };
  17. export type InstallWizardOptions = Record<
  18. string,
  19. {
  20. field: Field;
  21. value?: unknown;
  22. }
  23. >;
  24. export default function InstallWizard({onConfigured}: InstallWizardProps) {
  25. const {
  26. data: options,
  27. isPending,
  28. isError,
  29. } = useApiQuery<InstallWizardOptions>(['/internal/options/?query=is:required'], {
  30. staleTime: 0,
  31. });
  32. if (isPending) {
  33. return <LoadingIndicator />;
  34. }
  35. if (isError) {
  36. return (
  37. <Alert type="error" showIcon>
  38. {t(
  39. 'We were unable to load the required configuration from the Sentry server. Please take a look at the service logs.'
  40. )}
  41. </Alert>
  42. );
  43. }
  44. const renderFormFields = () => {
  45. let missingOptions = new Set(
  46. Object.keys(options).filter(option => !options[option]!.field.isSet)
  47. );
  48. // This is to handle the initial installation case.
  49. // Even if all options are filled out, we want to prompt to confirm
  50. // them. This is a bit of a hack because we're assuming that
  51. // the backend only spit back all filled out options for
  52. // this case.
  53. if (missingOptions.size === 0) {
  54. missingOptions = new Set(Object.keys(options));
  55. }
  56. // A mapping of option name to Field object
  57. const fields: Record<string, React.ReactNode> = {};
  58. for (const key of missingOptions) {
  59. const option = options[key]!;
  60. if (option.field.disabled) {
  61. continue;
  62. }
  63. fields[key] = getOptionField(key, option.field);
  64. }
  65. return getForm(fields);
  66. };
  67. const getInitialData = () => {
  68. const data = {};
  69. Object.keys(options).forEach(optionName => {
  70. const option = options[optionName]!;
  71. if (option.field.disabled) {
  72. return;
  73. }
  74. // TODO(dcramer): we need to rethink this logic as doing multiple "is this value actually set"
  75. // is problematic
  76. // all values to their server-defaults (as client-side defaults don't really work)
  77. const displayValue = option.value || getOptionDefault(optionName);
  78. if (
  79. // XXX(dcramer): we need the user to explicitly choose beacon.anonymous
  80. // vs using an implied default so effectively this is binding
  81. optionName !== 'beacon.anonymous' &&
  82. // XXX(byk): if we don't have a set value but have a default value filled
  83. // instead, from the client, set it on the data so it is sent to the server
  84. !option.field.isSet &&
  85. displayValue !== undefined
  86. ) {
  87. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  88. data[optionName] = displayValue;
  89. }
  90. });
  91. return data;
  92. };
  93. const version = ConfigStore.get('version');
  94. return (
  95. <SentryDocumentTitle noSuffix title={t('Setup Sentry')}>
  96. <Wrapper>
  97. <Pattern />
  98. <SetupWizard>
  99. <Heading>
  100. <span>{t('Welcome to Sentry')}</span>
  101. <Version>{version.current}</Version>
  102. </Heading>
  103. <Form
  104. apiMethod="PUT"
  105. apiEndpoint={'/internal/options/?query=is:required'}
  106. submitLabel={t('Continue')}
  107. initialData={getInitialData()}
  108. onSubmitSuccess={onConfigured}
  109. >
  110. <p>{t('Complete setup by filling out the required configuration.')}</p>
  111. {renderFormFields()}
  112. </Form>
  113. </SetupWizard>
  114. </Wrapper>
  115. </SentryDocumentTitle>
  116. );
  117. }
  118. const Wrapper = styled('div')`
  119. display: flex;
  120. justify-content: center;
  121. `;
  122. const fixedStyle = css`
  123. position: fixed;
  124. top: 0;
  125. right: 0;
  126. bottom: 0;
  127. left: 0;
  128. `;
  129. const Pattern = styled('div')`
  130. z-index: -1;
  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. 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. `;