Skip to content

Commit e7ecd4c

Browse files
authored
feat(aci): Setup detector details sidebar more (#94442)
1 parent 28105e8 commit e7ecd4c

File tree

10 files changed

+512
-159
lines changed

10 files changed

+512
-159
lines changed

static/app/components/workflowEngine/form/control/priorityControl.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {space} from 'sentry/styles/space';
1313
import {PriorityLevel} from 'sentry/types/group';
1414
import {
1515
DataConditionType,
16+
DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL,
1617
DetectorPriorityLevel,
1718
} from 'sentry/types/workflowEngine/dataConditions';
1819
import {
@@ -27,15 +28,6 @@ const priorities = [
2728
DetectorPriorityLevel.HIGH,
2829
] as const;
2930

30-
const DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL: Record<
31-
(typeof priorities)[number],
32-
PriorityLevel
33-
> = {
34-
[DetectorPriorityLevel.LOW]: PriorityLevel.LOW,
35-
[DetectorPriorityLevel.MEDIUM]: PriorityLevel.MEDIUM,
36-
[DetectorPriorityLevel.HIGH]: PriorityLevel.HIGH,
37-
};
38-
3931
const conditionKindAndTypeToLabel: Record<
4032
'static' | 'percent',
4133
Record<DataConditionType.GREATER | DataConditionType.LESS, string>

static/app/types/workflowEngine/dataConditions.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {PriorityLevel} from 'sentry/types/group';
2+
13
import type {Action} from './actions';
24

35
export enum DataConditionType {
@@ -59,6 +61,15 @@ export enum DetectorPriorityLevel {
5961
HIGH = 75,
6062
}
6163

64+
export const DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL: Record<
65+
Exclude<DetectorPriorityLevel, DetectorPriorityLevel.OK>,
66+
PriorityLevel
67+
> = {
68+
[DetectorPriorityLevel.LOW]: PriorityLevel.LOW,
69+
[DetectorPriorityLevel.MEDIUM]: PriorityLevel.MEDIUM,
70+
[DetectorPriorityLevel.HIGH]: PriorityLevel.HIGH,
71+
};
72+
6273
/**
6374
* See DataConditionSerializer
6475
*/

static/app/utils/useUserFromId.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type {User} from '@sentry/core';
33
import {useApiQuery} from 'sentry/utils/queryClient';
44
import useOrganization from 'sentry/utils/useOrganization';
55

6-
export default function useUserFromId({id}: {id: number}) {
6+
export default function useUserFromId({id}: {id: number | undefined}) {
77
const organization = useOrganization();
88

99
const {isPending, isError, data} = useApiQuery<User>(

static/app/views/detectors/components/detailsPanel.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {Fragment} from 'react';
12
import styled from '@emotion/styled';
23

34
import {Flex} from 'sentry/components/core/layout';
@@ -21,13 +22,17 @@ function SnubaQueryDetails({dataSource}: {dataSource: SnubaQueryDataSource}) {
2122
<Flex direction="column" gap={space(0.5)}>
2223
<Heading>{t('Query:')}</Heading>
2324
<Query>
24-
<Label>{t('visualize:')}</Label>{' '}
25+
<Label>{t('visualize:')}</Label>
2526
<Value>{dataSource.queryObj.snubaQuery.aggregate}</Value>
26-
<Label>{t('where:')}</Label>{' '}
27-
<Value>{dataSource.queryObj.snubaQuery.query}</Value>
27+
{dataSource.queryObj.snubaQuery.query && (
28+
<Fragment>
29+
<Label>{t('where:')}</Label>
30+
<Value>{dataSource.queryObj.snubaQuery.query}</Value>
31+
</Fragment>
32+
)}
2833
</Query>
2934
</Flex>
30-
<Flex gap={space(0.5)}>
35+
<Flex gap={space(0.5)} align="center">
3136
<Heading>{t('Threshold:')}</Heading>
3237
<Value>{getExactDuration(dataSource.queryObj.snubaQuery.timeWindow, true)}</Value>
3338
</Flex>
@@ -63,9 +68,8 @@ const Heading = styled('h4')`
6368
const Query = styled('dl')`
6469
display: grid;
6570
grid-template-columns: auto auto;
66-
grid-template-rows: repeat(3, 1fr);
6771
width: fit-content;
68-
gap: ${space(0.5)} ${space(1)};
72+
gap: ${space(0.25)} ${space(0.5)};
6973
margin: 0;
7074
`;
7175

@@ -76,7 +80,7 @@ const Label = styled('dt')`
7680
`;
7781

7882
const Value = styled('dl')`
79-
${p => p.theme.overflowEllipsis};
83+
word-break: break-all;
8084
margin: 0;
8185
`;
8286

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import {Fragment} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import {GroupPriorityBadge} from 'sentry/components/badge/groupPriority';
5+
import {Flex} from 'sentry/components/core/layout';
6+
import {Tooltip} from 'sentry/components/core/tooltip';
7+
import {DateTime} from 'sentry/components/dateTime';
8+
import {KeyValueTable, KeyValueTableRow} from 'sentry/components/keyValueTable';
9+
import Placeholder from 'sentry/components/placeholder';
10+
import TextOverflow from 'sentry/components/textOverflow';
11+
import TimeSince from 'sentry/components/timeSince';
12+
import Section from 'sentry/components/workflowEngine/ui/section';
13+
import {IconArrow} from 'sentry/icons';
14+
import {t} from 'sentry/locale';
15+
import {space} from 'sentry/styles/space';
16+
import {
17+
DataConditionType,
18+
DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL,
19+
DetectorPriorityLevel,
20+
} from 'sentry/types/workflowEngine/dataConditions';
21+
import type {Detector} from 'sentry/types/workflowEngine/detectors';
22+
import {useTeamsById} from 'sentry/utils/useTeamsById';
23+
import useUserFromId from 'sentry/utils/useUserFromId';
24+
import DetailsPanel from 'sentry/views/detectors/components/detailsPanel';
25+
import {getResolutionDescription} from 'sentry/views/detectors/utils/getDetectorResolutionDescription';
26+
27+
function getDetectorEnvironment(detector: Detector) {
28+
return (
29+
detector.dataSources?.find(ds => ds.type === 'snuba_query_subscription')?.queryObj
30+
?.snubaQuery.environment ?? t('All environments')
31+
);
32+
}
33+
34+
function AssignToTeam({teamId}: {teamId: string}) {
35+
const {teams, isLoading} = useTeamsById({ids: [teamId]});
36+
const team = teams.find(tm => tm.id === teamId);
37+
38+
if (isLoading) {
39+
return (
40+
<Flex align="center" gap={space(0.5)}>
41+
{t('Assign to')} <Placeholder width="80px" height="16px" />
42+
</Flex>
43+
);
44+
}
45+
46+
return t('Assign to %s', `#${team?.slug ?? 'unknown'}`);
47+
}
48+
49+
function AssignToUser({userId}: {userId: string}) {
50+
const {isPending, data: user} = useUserFromId({id: parseInt(userId, 10)});
51+
52+
if (isPending) {
53+
return (
54+
<Flex align="center" gap={space(0.5)}>
55+
{t('Assign to')} <Placeholder width="80px" height="16px" />
56+
</Flex>
57+
);
58+
}
59+
60+
const title = user?.name ?? user?.email ?? t('Unknown user');
61+
return (
62+
<Tooltip title={title} showOnlyOnOverflow>
63+
{t('Assign to %s', title)}
64+
</Tooltip>
65+
);
66+
}
67+
68+
function DetectorPriorities({detector}: {detector: Detector}) {
69+
const detectionType = detector.config?.detection_type || 'static';
70+
71+
// For dynamic detectors, show the automatic priority message
72+
if (detectionType === 'dynamic') {
73+
return <div>{t('Sentry will automatically update priority.')}</div>;
74+
}
75+
76+
const conditions = detector.conditionGroup?.conditions || [];
77+
78+
// Filter out OK conditions and sort by priority level
79+
const priorityConditions = conditions
80+
.filter(condition => condition.conditionResult !== DetectorPriorityLevel.OK)
81+
.sort((a, b) => (a.conditionResult || 0) - (b.conditionResult || 0));
82+
83+
if (priorityConditions.length === 0) {
84+
return null;
85+
}
86+
87+
const getConditionLabel = (condition: (typeof priorityConditions)[0]) => {
88+
const typeLabel =
89+
condition.type === DataConditionType.GREATER ? t('Above') : t('Below');
90+
91+
// For static/percent detectors, comparison should be a simple number
92+
const comparisonValue =
93+
typeof condition.comparison === 'number'
94+
? String(condition.comparison)
95+
: String(condition.comparison || '0');
96+
const thresholdSuffix = detector.config?.detection_type === 'percent' ? '%' : 's';
97+
98+
return `${typeLabel} ${comparisonValue}${thresholdSuffix}`;
99+
};
100+
101+
return (
102+
<PrioritiesList>
103+
{priorityConditions.map((condition, index) => (
104+
<Fragment key={index}>
105+
<PriorityCondition>{getConditionLabel(condition)}</PriorityCondition>
106+
<IconArrow direction="right" />
107+
<GroupPriorityBadge
108+
showLabel
109+
priority={
110+
DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL[
111+
condition.conditionResult as keyof typeof DETECTOR_PRIORITY_LEVEL_TO_PRIORITY_LEVEL
112+
]
113+
}
114+
/>
115+
</Fragment>
116+
))}
117+
</PrioritiesList>
118+
);
119+
}
120+
121+
function DetectorResolve({detector}: {detector: Detector}) {
122+
const detectionType = detector.config?.detection_type || 'static';
123+
const conditions = detector.conditionGroup?.conditions || [];
124+
125+
// Get the main condition (first non-OK condition)
126+
const mainCondition = conditions.find(
127+
condition => condition.conditionResult !== DetectorPriorityLevel.OK
128+
);
129+
130+
const thresholdSuffix = detector.config?.detection_type === 'percent' ? '%' : 's';
131+
132+
const description = getResolutionDescription({
133+
detectionType,
134+
conditionType: mainCondition?.type,
135+
conditionValue: mainCondition?.comparison,
136+
comparisonDelta: (detector.config as any)?.comparison_delta,
137+
thresholdSuffix,
138+
});
139+
140+
return <div>{description}</div>;
141+
}
142+
143+
function DetectorCreatedBy({createdBy}: {createdBy: Detector['createdBy']}) {
144+
const {isPending, data: user} = useUserFromId({
145+
id: createdBy ? parseInt(createdBy, 10) : undefined,
146+
});
147+
148+
if (!createdBy) {
149+
return t('Sentry');
150+
}
151+
152+
if (isPending) {
153+
return <Placeholder width="80px" height="16px" />;
154+
}
155+
156+
const title = user?.name ?? user?.email ?? t('Unknown');
157+
return (
158+
<Tooltip title={title} showOnlyOnOverflow>
159+
<TextOverflow>{title}</TextOverflow>
160+
</Tooltip>
161+
);
162+
}
163+
164+
function DetectorAssignee({owner}: {owner: string | null}) {
165+
if (!owner) {
166+
return t('Unassigned');
167+
}
168+
169+
const [ownerType, ownerId] = owner.split(':');
170+
if (ownerType === 'team') {
171+
return <AssignToTeam teamId={ownerId!} />;
172+
}
173+
if (ownerType === 'user') {
174+
return <AssignToUser userId={ownerId!} />;
175+
}
176+
177+
return t('Unassigned');
178+
}
179+
180+
interface DetectorDetailsSidebarProps {
181+
detector: Detector;
182+
}
183+
184+
export function DetectorDetailsSidebar({detector}: DetectorDetailsSidebarProps) {
185+
return (
186+
<Fragment>
187+
<Section title={t('Detect')}>
188+
<DetailsPanel detector={detector} />
189+
</Section>
190+
<Section title={t('Assign')}>
191+
<DetectorAssignee owner={detector.owner} />
192+
</Section>
193+
<Section title={t('Prioritize')}>
194+
<DetectorPriorities detector={detector} />
195+
</Section>
196+
<Section title={t('Resolve')}>
197+
<DetectorResolve detector={detector} />
198+
</Section>
199+
<Section title={t('Details')}>
200+
<StyledKeyValueTable>
201+
<KeyValueTableRow
202+
keyName={t('Date created')}
203+
value={<DateTime date={detector.dateCreated} dateOnly year />}
204+
/>
205+
<KeyValueTableRow
206+
keyName={t('Created by')}
207+
value={<DetectorCreatedBy createdBy={detector.createdBy ?? null} />}
208+
/>
209+
<KeyValueTableRow
210+
keyName={t('Last modified')}
211+
value={<TimeSince date={detector.dateUpdated} />}
212+
/>
213+
<KeyValueTableRow
214+
keyName={t('Environment')}
215+
value={
216+
<Tooltip title={getDetectorEnvironment(detector)} showOnlyOnOverflow>
217+
<TextOverflow>{getDetectorEnvironment(detector)}</TextOverflow>
218+
</Tooltip>
219+
}
220+
/>
221+
</StyledKeyValueTable>
222+
</Section>
223+
</Fragment>
224+
);
225+
}
226+
227+
const PrioritiesList = styled('div')`
228+
display: grid;
229+
grid-template-columns: auto auto auto;
230+
align-items: center;
231+
width: fit-content;
232+
gap: ${space(0.5)} ${space(1)};
233+
234+
p {
235+
margin: 0;
236+
width: fit-content;
237+
}
238+
`;
239+
240+
const PriorityCondition = styled('div')`
241+
justify-self: flex-end;
242+
font-size: ${p => p.theme.fontSize.md};
243+
color: ${p => p.theme.textColor};
244+
`;
245+
246+
const StyledKeyValueTable = styled(KeyValueTable)`
247+
grid-template-columns: min-content auto;
248+
`;

0 commit comments

Comments
 (0)