123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- import {useState} from 'react';
- import type {RouteComponentProps} from 'react-router';
- import styled from '@emotion/styled';
- import Feature from 'sentry/components/acl/feature';
- import FeatureDisabled from 'sentry/components/acl/featureDisabled';
- import CreateAlertButton from 'sentry/components/createAlertButton';
- import {Hovercard} from 'sentry/components/hovercard';
- import * as Layout from 'sentry/components/layouts/thirds';
- import ExternalLink from 'sentry/components/links/externalLink';
- import List from 'sentry/components/list';
- import ListItem from 'sentry/components/list/listItem';
- import Panel from 'sentry/components/panels/panel';
- import PanelBody from 'sentry/components/panels/panelBody';
- import PanelHeader from 'sentry/components/panels/panelHeader';
- import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {Organization} from 'sentry/types/organization';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import BuilderBreadCrumbs from 'sentry/views/alerts/builder/builderBreadCrumbs';
- import {Dataset} from 'sentry/views/alerts/rules/metric/types';
- import {AlertRuleType} from 'sentry/views/alerts/types';
- import type {AlertType, WizardRuleTemplate} from './options';
- import {
- AlertWizardAlertNames,
- AlertWizardRuleTemplates,
- getAlertWizardCategories,
- } from './options';
- import {AlertWizardPanelContent} from './panelContent';
- import RadioPanelGroup from './radioPanelGroup';
- type RouteParams = {
- projectId?: string;
- };
- type AlertWizardProps = RouteComponentProps<RouteParams, {}> & {
- organization: Organization;
- projectId: string;
- };
- const DEFAULT_ALERT_OPTION = 'issues';
- function AlertWizard({organization, params, location, projectId}: AlertWizardProps) {
- const [alertOption, setAlertOption] = useState<AlertType>(
- location.query.alert_option in AlertWizardAlertNames
- ? location.query.alert_option
- : DEFAULT_ALERT_OPTION
- );
- const projectSlug = params.projectId ?? projectId;
- const handleChangeAlertOption = (option: AlertType) => {
- setAlertOption(option);
- };
- function renderCreateAlertButton() {
- let metricRuleTemplate: Readonly<WizardRuleTemplate> | undefined =
- AlertWizardRuleTemplates[alertOption];
- const isMetricAlert = !!metricRuleTemplate;
- const isTransactionDataset = metricRuleTemplate?.dataset === Dataset.TRANSACTIONS;
- if (
- organization.features.includes('alert-crash-free-metrics') &&
- metricRuleTemplate?.dataset === Dataset.SESSIONS
- ) {
- metricRuleTemplate = {...metricRuleTemplate, dataset: Dataset.METRICS};
- }
- if (metricRuleTemplate?.dataset === Dataset.ERRORS) {
- // Pre-fill is:unresolved for error metric alerts
- // Filters out events in issues that are archived or resolved
- metricRuleTemplate = {...metricRuleTemplate, query: 'is:unresolved'};
- }
- const renderNoAccess = p => (
- <Hovercard
- body={
- <FeatureDisabled
- features={p.features}
- hideHelpToggle
- featureName={t('Metric Alerts')}
- />
- }
- >
- {p.children(p)}
- </Hovercard>
- );
- return (
- <Feature
- features={
- isTransactionDataset
- ? ['organizations:incidents', 'organizations:performance-view']
- : isMetricAlert
- ? ['organizations:incidents']
- : []
- }
- requireAll
- organization={organization}
- hookName="feature-disabled:alert-wizard-performance"
- renderDisabled={renderNoAccess}
- >
- {({hasFeature}) => (
- <WizardButtonContainer
- onClick={() =>
- trackAnalytics('alert_wizard.option_selected', {
- organization,
- alert_type: alertOption,
- })
- }
- >
- <CreateAlertButton
- organization={organization}
- projectSlug={projectSlug}
- disabled={!hasFeature}
- priority="primary"
- to={{
- pathname: `/organizations/${organization.slug}/alerts/new/${
- isMetricAlert ? AlertRuleType.METRIC : AlertRuleType.ISSUE
- }/`,
- query: {
- ...(metricRuleTemplate ? metricRuleTemplate : {}),
- project: projectSlug,
- referrer: location?.query?.referrer,
- },
- }}
- hideIcon
- >
- {t('Set Conditions')}
- </CreateAlertButton>
- </WizardButtonContainer>
- )}
- </Feature>
- );
- }
- const panelContent = AlertWizardPanelContent[alertOption];
- return (
- <Layout.Page>
- <SentryDocumentTitle title={t('Alert Creation Wizard')} projectSlug={projectSlug} />
- <Layout.Header>
- <StyledHeaderContent>
- <BuilderBreadCrumbs
- organization={organization}
- projectSlug={projectSlug}
- title={t('Select Alert')}
- />
- <Layout.Title>{t('Select Alert')}</Layout.Title>
- </StyledHeaderContent>
- </Layout.Header>
- <Layout.Body>
- <Layout.Main fullWidth>
- <WizardBody>
- <WizardOptions>
- {getAlertWizardCategories(organization).map(
- ({categoryHeading, options}) => (
- <div key={categoryHeading}>
- <CategoryTitle>{categoryHeading} </CategoryTitle>
- <RadioPanelGroup
- choices={options.map(alertType => {
- return [alertType, AlertWizardAlertNames[alertType]];
- })}
- onChange={handleChangeAlertOption}
- value={alertOption}
- label="alert-option"
- />
- </div>
- )
- )}
- </WizardOptions>
- <WizardPanel visible={!!panelContent && !!alertOption}>
- <WizardPanelBody>
- <div>
- <PanelHeader>{AlertWizardAlertNames[alertOption]}</PanelHeader>
- <PanelBody withPadding>
- <PanelDescription>
- {panelContent.description}{' '}
- {panelContent.docsLink && (
- <ExternalLink href={panelContent.docsLink}>
- {t('Learn more')}
- </ExternalLink>
- )}
- </PanelDescription>
- <WizardImage src={panelContent.illustration} />
- <ExampleHeader>{t('Examples')}</ExampleHeader>
- <ExampleList symbol="bullet">
- {panelContent.examples.map((example, i) => (
- <ExampleItem key={i}>{example}</ExampleItem>
- ))}
- </ExampleList>
- </PanelBody>
- </div>
- <WizardFooter>{renderCreateAlertButton()}</WizardFooter>
- </WizardPanelBody>
- </WizardPanel>
- </WizardBody>
- </Layout.Main>
- </Layout.Body>
- </Layout.Page>
- );
- }
- const StyledHeaderContent = styled(Layout.HeaderContent)`
- overflow: visible;
- `;
- const CategoryTitle = styled('h2')`
- font-weight: ${p => p.theme.fontWeightNormal};
- font-size: ${p => p.theme.fontSizeExtraLarge};
- margin-bottom: ${space(1)} !important;
- `;
- const WizardBody = styled('div')`
- display: flex;
- padding-top: ${space(1)};
- `;
- const WizardOptions = styled('div')`
- display: flex;
- flex-direction: column;
- gap: ${space(4)};
- flex: 3;
- margin-right: ${space(3)};
- padding-right: ${space(3)};
- max-width: 300px;
- `;
- const WizardImage = styled('img')`
- max-height: 300px;
- `;
- const WizardPanel = styled(Panel)<{visible?: boolean}>`
- max-width: 700px;
- position: sticky;
- top: 20px;
- flex: 5;
- display: flex;
- ${p => !p.visible && 'visibility: hidden'};
- flex-direction: column;
- align-items: start;
- align-self: flex-start;
- ${p => p.visible && 'animation: 0.6s pop ease forwards'};
- @keyframes pop {
- 0% {
- transform: translateY(30px);
- opacity: 0;
- }
- 100% {
- transform: translateY(0);
- opacity: 1;
- }
- }
- `;
- const ExampleList = styled(List)`
- margin-bottom: ${space(2)} !important;
- `;
- const WizardPanelBody = styled(PanelBody)`
- flex: 1;
- min-width: 100%;
- `;
- const PanelDescription = styled('p')`
- margin-bottom: ${space(2)};
- `;
- const ExampleHeader = styled('div')`
- margin: 0 0 ${space(1)} 0;
- font-size: ${p => p.theme.fontSizeLarge};
- `;
- const ExampleItem = styled(ListItem)`
- font-size: ${p => p.theme.fontSizeMedium};
- `;
- const WizardFooter = styled('div')`
- border-top: 1px solid ${p => p.theme.border};
- padding: ${space(1.5)} ${space(1.5)} ${space(1.5)} ${space(1.5)};
- `;
- const WizardButtonContainer = styled('div')`
- display: flex;
- justify-content: flex-end;
- a:not(:last-child) {
- margin-right: ${space(1)};
- }
- `;
- export default AlertWizard;
|