123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563 |
- import * as React from 'react';
- import {RouteComponentProps} from 'react-router';
- import styled from '@emotion/styled';
- import {Location} from 'history';
- import moment from 'moment';
- import {Client} from 'app/api';
- import Alert from 'app/components/alert';
- import ActorAvatar from 'app/components/avatar/actorAvatar';
- import {SectionHeading} from 'app/components/charts/styles';
- import {getInterval} from 'app/components/charts/utils';
- import DropdownControl, {DropdownItem} from 'app/components/dropdownControl';
- import Duration from 'app/components/duration';
- import IdBadge from 'app/components/idBadge';
- import {KeyValueTable, KeyValueTableRow} from 'app/components/keyValueTable';
- import * as Layout from 'app/components/layouts/thirds';
- import NotAvailable from 'app/components/notAvailable';
- import {Panel, PanelBody} from 'app/components/panels';
- import Placeholder from 'app/components/placeholder';
- import {parseSearch} from 'app/components/searchSyntax/parser';
- import HighlightQuery from 'app/components/searchSyntax/renderer';
- import TimeSince from 'app/components/timeSince';
- import Tooltip from 'app/components/tooltip';
- import {IconCheckmark, IconFire, IconInfo, IconWarning} from 'app/icons';
- import {t, tct} from 'app/locale';
- import overflowEllipsis from 'app/styles/overflowEllipsis';
- import space from 'app/styles/space';
- import {Actor, DateString, Organization, Project} from 'app/types';
- import getDynamicText from 'app/utils/getDynamicText';
- import Projects from 'app/utils/projects';
- import {
- AlertRuleThresholdType,
- Dataset,
- IncidentRule,
- Trigger,
- } from 'app/views/alerts/incidentRules/types';
- import {extractEventTypeFilterFromRule} from 'app/views/alerts/incidentRules/utils/getEventTypeFilter';
- import Timeline from 'app/views/alerts/rules/details/timeline';
- import AlertBadge from '../../alertBadge';
- import {AlertRuleStatus, Incident, IncidentStatus} from '../../types';
- import {API_INTERVAL_POINTS_LIMIT, TIME_OPTIONS, TimePeriodType} from './constants';
- import MetricChart from './metricChart';
- import RelatedIssues from './relatedIssues';
- import RelatedTransactions from './relatedTransactions';
- type Props = {
- api: Client;
- rule?: IncidentRule;
- incidents?: Incident[];
- timePeriod: TimePeriodType;
- selectedIncident?: Incident | null;
- organization: Organization;
- location: Location;
- handleTimePeriodChange: (value: string) => void;
- handleZoom: (start: DateString, end: DateString) => void;
- } & RouteComponentProps<{orgId: string}, {}>;
- export default class DetailsBody extends React.Component<Props> {
- getMetricText(): React.ReactNode {
- const {rule} = this.props;
- if (!rule) {
- return '';
- }
- const {aggregate} = rule;
- return tct('[metric]', {
- metric: aggregate,
- });
- }
- getTimeWindow(): React.ReactNode {
- const {rule} = this.props;
- if (!rule) {
- return '';
- }
- const {timeWindow} = rule;
- return tct('[window]', {
- window: <Duration seconds={timeWindow * 60} />,
- });
- }
- getInterval() {
- const {
- timePeriod: {start, end},
- rule,
- } = this.props;
- const startDate = moment.utc(start);
- const endDate = moment.utc(end);
- const timeWindow = rule?.timeWindow;
- if (
- timeWindow &&
- endDate.diff(startDate) < API_INTERVAL_POINTS_LIMIT * timeWindow * 60 * 1000
- ) {
- return `${timeWindow}m`;
- }
- return getInterval({start, end}, 'high');
- }
- getFilter() {
- const {rule} = this.props;
- const {dataset, query} = rule ?? {};
- if (!rule) {
- return null;
- }
- const eventType =
- dataset === Dataset.SESSIONS ? null : extractEventTypeFilterFromRule(rule);
- const parsedQuery = parseSearch([eventType, query].join(' ').trim());
- return (
- <Filters>
- {query || eventType ? (
- <HighlightQuery parsedQuery={parsedQuery ?? []} />
- ) : (
- <NotAvailable />
- )}
- </Filters>
- );
- }
- renderTrigger(trigger: Trigger): React.ReactNode {
- const {rule} = this.props;
- if (!rule) {
- return null;
- }
- const status =
- trigger.label === 'critical' ? (
- <StatusWrapper>
- <IconFire color="red300" size="sm" /> Critical
- </StatusWrapper>
- ) : trigger.label === 'warning' ? (
- <StatusWrapper>
- <IconWarning color="yellow300" size="sm" /> Warning
- </StatusWrapper>
- ) : (
- <StatusWrapper>
- <IconCheckmark color="green300" size="sm" isCircled /> Resolved
- </StatusWrapper>
- );
- const thresholdTypeText =
- rule.thresholdType === AlertRuleThresholdType.ABOVE ? t('above') : t('below');
- return (
- <TriggerCondition>
- {status}
- <TriggerText>{`${thresholdTypeText} ${trigger.alertThreshold}`}</TriggerText>
- </TriggerCondition>
- );
- }
- renderRuleDetails() {
- const {rule} = this.props;
- if (rule === undefined) {
- return <Placeholder height="200px" />;
- }
- const criticalTrigger = rule?.triggers.find(({label}) => label === 'critical');
- const warningTrigger = rule?.triggers.find(({label}) => label === 'warning');
- const ownerId = rule.owner?.split(':')[1];
- const teamActor = ownerId && {type: 'team' as Actor['type'], id: ownerId, name: ''};
- return (
- <React.Fragment>
- <SidebarGroup>
- <Heading>{t('Metric')}</Heading>
- <RuleText>{this.getMetricText()}</RuleText>
- </SidebarGroup>
- <SidebarGroup>
- <Heading>{t('Environment')}</Heading>
- <RuleText>{rule.environment ?? 'All'}</RuleText>
- </SidebarGroup>
- <SidebarGroup>
- <Heading>{t('Filters')}</Heading>
- {this.getFilter()}
- </SidebarGroup>
- <SidebarGroup>
- <Heading>{t('Conditions')}</Heading>
- {criticalTrigger && this.renderTrigger(criticalTrigger)}
- {warningTrigger && this.renderTrigger(warningTrigger)}
- </SidebarGroup>
- <SidebarGroup>
- <Heading>{t('Other Details')}</Heading>
- <KeyValueTable>
- <KeyValueTableRow
- keyName={t('Team')}
- value={
- teamActor ? <ActorAvatar actor={teamActor} size={24} /> : 'Unassigned'
- }
- />
- {rule.createdBy && (
- <KeyValueTableRow
- keyName={t('Created By')}
- value={<CreatedBy>{rule.createdBy.name ?? '-'}</CreatedBy>}
- />
- )}
- {rule.dateModified && (
- <KeyValueTableRow
- keyName={t('Last Modified')}
- value={<TimeSince date={rule.dateModified} suffix={t('ago')} />}
- />
- )}
- </KeyValueTable>
- </SidebarGroup>
- </React.Fragment>
- );
- }
- renderMetricStatus() {
- const {incidents} = this.props;
-
- const activeIncident = incidents?.find(({dateClosed}) => !dateClosed);
- const status = activeIncident ? activeIncident.status : IncidentStatus.CLOSED;
- const latestIncident = incidents?.length ? incidents[0] : null;
-
- const activityDate = activeIncident
- ? activeIncident.dateStarted
- : latestIncident
- ? latestIncident.dateClosed
- : null;
- return (
- <StatusContainer>
- <HeaderItem>
- <Heading noMargin>{t('Current Status')}</Heading>
- <Status>
- <AlertBadge status={status} hideText />
- {activeIncident ? t('Triggered') : t('Resolved')}
- {activityDate ? <TimeSince date={activityDate} /> : ''}
- </Status>
- </HeaderItem>
- </StatusContainer>
- );
- }
- renderLoading() {
- return (
- <Layout.Body>
- <Layout.Main>
- <Placeholder height="38px" />
- <ChartPanel>
- <PanelBody withPadding>
- <Placeholder height="200px" />
- </PanelBody>
- </ChartPanel>
- </Layout.Main>
- <Layout.Side>
- <Placeholder height="200px" />
- </Layout.Side>
- </Layout.Body>
- );
- }
- render() {
- const {
- api,
- rule,
- incidents,
- location,
- organization,
- timePeriod,
- selectedIncident,
- handleZoom,
- params: {orgId},
- } = this.props;
- if (!rule) {
- return this.renderLoading();
- }
- const {query, projects: projectSlugs, dataset} = rule;
- const queryWithTypeFilter = `${query} ${extractEventTypeFilterFromRule(rule)}`.trim();
- return (
- <Projects orgId={orgId} slugs={projectSlugs}>
- {({initiallyLoaded, projects}) => {
- return initiallyLoaded ? (
- <React.Fragment>
- {selectedIncident &&
- selectedIncident.alertRule.status === AlertRuleStatus.SNAPSHOT && (
- <StyledLayoutBody>
- <StyledAlert type="warning" icon={<IconInfo size="md" />}>
- {t(
- 'Alert Rule settings have been updated since this alert was triggered.'
- )}
- </StyledAlert>
- </StyledLayoutBody>
- )}
- <StyledLayoutBodyWrapper>
- <Layout.Main>
- <HeaderContainer>
- <HeaderGrid>
- <HeaderItem>
- <Heading noMargin>{t('Display')}</Heading>
- <ChartControls>
- <DropdownControl
- label={getDynamicText({
- fixed: 'Oct 14, 2:56 PM — Oct 14, 4:55 PM',
- value: timePeriod.display,
- })}
- >
- {TIME_OPTIONS.map(({label, value}) => (
- <DropdownItem
- key={value}
- eventKey={value}
- isActive={
- !timePeriod.custom && timePeriod.period === value
- }
- onSelect={this.props.handleTimePeriodChange}
- >
- {label}
- </DropdownItem>
- ))}
- </DropdownControl>
- </ChartControls>
- </HeaderItem>
- {projects && projects.length && (
- <HeaderItem>
- <Heading noMargin>{t('Project')}</Heading>
- <IdBadge avatarSize={16} project={projects[0]} />
- </HeaderItem>
- )}
- <HeaderItem>
- <Heading noMargin>
- {t('Time Interval')}
- <Tooltip
- title={t(
- 'The time window over which the metric is evaluated.'
- )}
- >
- <IconInfo size="xs" color="gray200" />
- </Tooltip>
- </Heading>
- <RuleText>{this.getTimeWindow()}</RuleText>
- </HeaderItem>
- </HeaderGrid>
- </HeaderContainer>
- <MetricChart
- api={api}
- rule={rule}
- incidents={incidents}
- timePeriod={timePeriod}
- selectedIncident={selectedIncident}
- organization={organization}
- projects={projects}
- interval={this.getInterval()}
- filter={this.getFilter()}
- query={dataset === Dataset.SESSIONS ? query : queryWithTypeFilter}
- orgId={orgId}
- handleZoom={handleZoom}
- />
- <DetailWrapper>
- <ActivityWrapper>
- {[Dataset.SESSIONS, Dataset.ERRORS].includes(dataset) && (
- <RelatedIssues
- organization={organization}
- rule={rule}
- projects={((projects as Project[]) || []).filter(project =>
- rule.projects.includes(project.slug)
- )}
- timePeriod={timePeriod}
- query={
- dataset === Dataset.ERRORS
- ? queryWithTypeFilter
- : dataset === Dataset.SESSIONS
- ? `${query} error.unhandled:true`
- : undefined
- }
- />
- )}
- {dataset === Dataset.TRANSACTIONS && (
- <RelatedTransactions
- organization={organization}
- location={location}
- rule={rule}
- projects={((projects as Project[]) || []).filter(project =>
- rule.projects.includes(project.slug)
- )}
- start={timePeriod.start}
- end={timePeriod.end}
- filter={extractEventTypeFilterFromRule(rule)}
- />
- )}
- </ActivityWrapper>
- </DetailWrapper>
- </Layout.Main>
- <Layout.Side>
- {this.renderMetricStatus()}
- <Timeline
- api={api}
- organization={organization}
- rule={rule}
- incidents={incidents}
- />
- {this.renderRuleDetails()}
- </Layout.Side>
- </StyledLayoutBodyWrapper>
- </React.Fragment>
- ) : (
- <Placeholder height="200px" />
- );
- }}
- </Projects>
- );
- }
- }
- const SidebarGroup = styled('div')`
- margin-bottom: ${space(3)};
- `;
- const DetailWrapper = styled('div')`
- display: flex;
- flex: 1;
- @media (max-width: ${p => p.theme.breakpoints[0]}) {
- flex-direction: column-reverse;
- }
- `;
- const StatusWrapper = styled('div')`
- display: flex;
- align-items: center;
- svg {
- margin-right: ${space(0.5)};
- }
- `;
- const HeaderContainer = styled('div')`
- height: 60px;
- display: flex;
- flex-direction: row;
- align-content: flex-start;
- `;
- const HeaderGrid = styled('div')`
- display: grid;
- grid-template-columns: auto auto auto;
- align-items: stretch;
- grid-gap: 60px;
- `;
- const HeaderItem = styled('div')`
- flex: 1;
- display: flex;
- flex-direction: column;
- > *:nth-child(2) {
- flex: 1;
- display: flex;
- align-items: center;
- }
- `;
- const StyledLayoutBody = styled(Layout.Body)`
- flex-grow: 0;
- padding-bottom: 0 !important;
- @media (min-width: ${p => p.theme.breakpoints[1]}) {
- grid-template-columns: auto;
- }
- `;
- const StyledLayoutBodyWrapper = styled(Layout.Body)`
- margin-bottom: -${space(3)};
- `;
- const StyledAlert = styled(Alert)`
- margin: 0;
- `;
- const ActivityWrapper = styled('div')`
- display: flex;
- flex: 1;
- flex-direction: column;
- width: 100%;
- `;
- const Status = styled('div')`
- position: relative;
- display: grid;
- grid-template-columns: auto auto auto;
- grid-gap: ${space(0.5)};
- font-size: ${p => p.theme.fontSizeLarge};
- `;
- const StatusContainer = styled('div')`
- height: 60px;
- display: flex;
- margin-bottom: ${space(1.5)};
- `;
- const Heading = styled(SectionHeading)<{noMargin?: boolean}>`
- display: grid;
- grid-template-columns: auto auto;
- justify-content: flex-start;
- margin-top: ${p => (p.noMargin ? 0 : space(2))};
- margin-bottom: ${space(0.5)};
- line-height: 1;
- gap: ${space(1)};
- `;
- const ChartControls = styled('div')`
- display: flex;
- flex-direction: row;
- align-items: center;
- `;
- const ChartPanel = styled(Panel)`
- margin-top: ${space(2)};
- `;
- const RuleText = styled('div')`
- font-size: ${p => p.theme.fontSizeLarge};
- `;
- const Filters = styled('span')`
- overflow-wrap: break-word;
- word-break: break-word;
- white-space: pre-wrap;
- font-size: ${p => p.theme.fontSizeSmall};
- line-height: 25px;
- font-family: ${p => p.theme.text.familyMono};
- `;
- const TriggerCondition = styled('div')`
- display: flex;
- align-items: center;
- `;
- const TriggerText = styled('div')`
- margin-left: ${space(0.5)};
- white-space: nowrap;
- `;
- const CreatedBy = styled('div')`
- ${overflowEllipsis}
- `;
|