Files
sentinel/static/app/views/admin/installWizard/index.tsx
T
zeekayandHanzo Dev 07007b4790 the last place the wordmark was still shipping
sentry-pattern.png is a doodle tile with the Sentry script wordmark drawn into
it, and it existed twice: once under images/email, once under static/images/
pattern. The email copy lost its only reference with the template pass. The other
was live — webpack aliases sentry-images to static/images, so the install wizard
and .pattern-bg were both painting the upstream mark as a page background.

Neither use is load-bearing. The wizard's ::after existed only to hold the tile,
and layout.less keeps its ground colour. Redrawing the doodle in Hanzo's marks
would be inventing an illustration to replace one nobody needs, so both files are
deleted rather than substituted.

SHOW_LOGIN_BANNER goes too: the login banner it gated was two hardcoded upstream
campaigns, and with those removed the setting had no reader left.

LLM.md said "Branded as: Hanzo Sentry", which the code no longer agrees with.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-10 18:01:23 -07:00

201 lines
5.1 KiB
TypeScript

import {css} from '@emotion/react';
import styled from '@emotion/styled';
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 type {Field} from '../options';
import {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 Sentinel');
}
render() {
const version = ConfigStore.get('version');
return (
<SentryDocumentTitle noSuffix title={this.getTitle()}>
<Wrapper>
<Pattern />
<SetupWizard>
<Heading>
<span>{t('Welcome to Sentinel')}</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 Sentinel 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;
}
`;
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};
`;