mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
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]>
201 lines
5.1 KiB
TypeScript
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};
|
|
`;
|