Skip to content

Commit 7a3c0c1

Browse files
authored
feat(aci): Implement uptime detector details page (#95391)
1 parent 2c8fdfc commit 7a3c0c1

File tree

12 files changed

+256
-78
lines changed

12 files changed

+256
-78
lines changed

static/app/types/workflowEngine/detectors.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,13 @@ export interface SnubaQueryDataSource extends BaseDataSource {
4848
type: 'snuba_query_subscription';
4949
}
5050

51-
interface UptimeSubscriptionDataSource extends BaseDataSource {
51+
export interface UptimeSubscriptionDataSource extends BaseDataSource {
5252
/**
5353
* See UptimeSubscriptionSerializer
5454
*/
5555
queryObj: {
5656
body: string | null;
5757
headers: Array<[string, string]>;
58-
hostProviderId: string;
59-
hostProviderName: string;
6058
intervalSeconds: number;
6159
method: string;
6260
timeoutMs: number;
@@ -133,7 +131,7 @@ export interface MetricDetector extends BaseDetector {
133131

134132
export interface UptimeDetector extends BaseDetector {
135133
readonly config: UptimeDetectorConfig;
136-
readonly dataSources: UptimeSubscriptionDataSource[];
134+
readonly dataSources: [UptimeSubscriptionDataSource];
137135
readonly type: 'uptime_domain_failure';
138136
}
139137

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {Flex} from 'sentry/components/core/layout/flex';
2+
import {Tooltip} from 'sentry/components/core/tooltip';
3+
import Placeholder from 'sentry/components/placeholder';
4+
import Section from 'sentry/components/workflowEngine/ui/section';
5+
import {t} from 'sentry/locale';
6+
import {space} from 'sentry/styles/space';
7+
import {parseActorIdentifier} from 'sentry/utils/parseActorIdentifier';
8+
import {useTeamsById} from 'sentry/utils/useTeamsById';
9+
import useUserFromId from 'sentry/utils/useUserFromId';
10+
11+
function AssignToTeam({teamId}: {teamId: string}) {
12+
const {teams, isLoading} = useTeamsById({ids: [teamId]});
13+
const team = teams.find(tm => tm.id === teamId);
14+
15+
if (isLoading) {
16+
return (
17+
<Flex align="center" gap={space(0.5)}>
18+
{t('Assign to')} <Placeholder width="80px" height="16px" />
19+
</Flex>
20+
);
21+
}
22+
23+
return t('Assign to %s', `#${team?.slug ?? 'unknown'}`);
24+
}
25+
26+
function AssignToUser({userId}: {userId: string}) {
27+
const {isPending, data: user} = useUserFromId({id: parseInt(userId, 10)});
28+
29+
if (isPending) {
30+
return (
31+
<Flex align="center" gap={space(0.5)}>
32+
{t('Assign to')} <Placeholder width="80px" height="16px" />
33+
</Flex>
34+
);
35+
}
36+
37+
const title = user?.name ?? user?.email ?? t('Unknown user');
38+
return (
39+
<Tooltip title={title} showOnlyOnOverflow>
40+
{t('Assign to %s', title)}
41+
</Tooltip>
42+
);
43+
}
44+
45+
function DetectorOwner({owner}: {owner: string | null}) {
46+
const parsed = parseActorIdentifier(owner);
47+
if (parsed?.type === 'team') {
48+
return <AssignToTeam teamId={parsed.id} />;
49+
}
50+
if (parsed?.type === 'user') {
51+
return <AssignToUser userId={parsed.id} />;
52+
}
53+
54+
return t('Unassigned');
55+
}
56+
57+
export function DetectorDetailsAssignee({owner}: {owner: string | null}) {
58+
return (
59+
<Section title={t('Assign')}>
60+
<DetectorOwner owner={owner} />
61+
</Section>
62+
);
63+
}

static/app/views/detectors/components/details/common/extraDetails.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Section from 'sentry/components/workflowEngine/ui/section';
1010
import {t} from 'sentry/locale';
1111
import type {Detector} from 'sentry/types/workflowEngine/detectors';
1212
import useUserFromId from 'sentry/utils/useUserFromId';
13+
import {getDetectorEnvironment} from 'sentry/views/detectors/utils/getDetectorEnvironment';
1314

1415
type Props = {
1516
children: React.ReactNode;
@@ -97,19 +98,7 @@ DetectorExtraDetails.Environment = function DetectorExtraDetailsEnvironment({
9798
}: {
9899
detector: Detector;
99100
}) {
100-
// TODO: Add common function for getting environment from a detector
101-
const getEnvironment = () => {
102-
if (detector.type !== 'metric_issue') {
103-
return '<placeholder>';
104-
}
105-
106-
return (
107-
detector.dataSources?.find(ds => ds.type === 'snuba_query_subscription')?.queryObj
108-
?.snubaQuery.environment ?? t('All environments')
109-
);
110-
};
111-
112-
const environment = getEnvironment();
101+
const environment = getDetectorEnvironment(detector);
113102

114103
return (
115104
<KeyValueTableRow

static/app/views/detectors/components/details/fallback.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import DetailLayout from 'sentry/components/workflowEngine/layout/detail';
22
import type {Project} from 'sentry/types/project';
33
import type {Detector} from 'sentry/types/workflowEngine/detectors';
4+
import {DetectorDetailsAssignee} from 'sentry/views/detectors/components/details/common/assignee';
45
import {DetectorDetailsAutomations} from 'sentry/views/detectors/components/details/common/automations';
56
import {DetectorExtraDetails} from 'sentry/views/detectors/components/details/common/extraDetails';
67
import {DetectorDetailsHeader} from 'sentry/views/detectors/components/details/common/header';
@@ -24,6 +25,7 @@ export function FallbackDetectorDetails({
2425
<DetectorDetailsAutomations detector={detector} />
2526
</DetailLayout.Main>
2627
<DetailLayout.Sidebar>
28+
<DetectorDetailsAssignee owner={detector.owner} />
2729
<DetectorExtraDetails>
2830
<DetectorExtraDetails.DateCreated detector={detector} />
2931
<DetectorExtraDetails.CreatedBy detector={detector} />

static/app/views/detectors/components/details/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {Detector} from 'sentry/types/workflowEngine/detectors';
33
import {unreachable} from 'sentry/utils/unreachable';
44
import {FallbackDetectorDetails} from 'sentry/views/detectors/components/details/fallback';
55
import {MetricDetectorDetails} from 'sentry/views/detectors/components/details/metric';
6+
import {UptimeDetectorDetails} from 'sentry/views/detectors/components/details/uptime';
67

78
type DetectorDetailsContentProps = {
89
detector: Detector;
@@ -15,6 +16,7 @@ export function DetectorDetailsContent({detector, project}: DetectorDetailsConte
1516
case 'metric_issue':
1617
return <MetricDetectorDetails detector={detector} project={project} />;
1718
case 'uptime_domain_failure':
19+
return <UptimeDetectorDetails detector={detector} project={project} />;
1820
case 'uptime_subscription':
1921
case 'error':
2022
return <FallbackDetectorDetails detector={detector} project={project} />;

static/app/views/detectors/components/details/metric/sidebar.tsx

Lines changed: 2 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ import {Fragment} from 'react';
22
import styled from '@emotion/styled';
33

44
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 Placeholder from 'sentry/components/placeholder';
85
import Section from 'sentry/components/workflowEngine/ui/section';
96
import {IconArrow} from 'sentry/icons';
107
import {t} from 'sentry/locale';
@@ -15,8 +12,7 @@ import {
1512
DetectorPriorityLevel,
1613
} from 'sentry/types/workflowEngine/dataConditions';
1714
import type {MetricDetector} from 'sentry/types/workflowEngine/detectors';
18-
import {useTeamsById} from 'sentry/utils/useTeamsById';
19-
import useUserFromId from 'sentry/utils/useUserFromId';
15+
import {DetectorDetailsAssignee} from 'sentry/views/detectors/components/details/common/assignee';
2016
import {DetectorExtraDetails} from 'sentry/views/detectors/components/details/common/extraDetails';
2117
import {MetricDetectorDetailsDetect} from 'sentry/views/detectors/components/details/metric/detect';
2218
import {getResolutionDescription} from 'sentry/views/detectors/utils/getDetectorResolutionDescription';
@@ -26,40 +22,6 @@ interface DetectorDetailsSidebarProps {
2622
detector: MetricDetector;
2723
}
2824

29-
function AssignToTeam({teamId}: {teamId: string}) {
30-
const {teams, isLoading} = useTeamsById({ids: [teamId]});
31-
const team = teams.find(tm => tm.id === teamId);
32-
33-
if (isLoading) {
34-
return (
35-
<Flex align="center" gap={space(0.5)}>
36-
{t('Assign to')} <Placeholder width="80px" height="16px" />
37-
</Flex>
38-
);
39-
}
40-
41-
return t('Assign to %s', `#${team?.slug ?? 'unknown'}`);
42-
}
43-
44-
function AssignToUser({userId}: {userId: string}) {
45-
const {isPending, data: user} = useUserFromId({id: parseInt(userId, 10)});
46-
47-
if (isPending) {
48-
return (
49-
<Flex align="center" gap={space(0.5)}>
50-
{t('Assign to')} <Placeholder width="80px" height="16px" />
51-
</Flex>
52-
);
53-
}
54-
55-
const title = user?.name ?? user?.email ?? t('Unknown user');
56-
return (
57-
<Tooltip title={title} showOnlyOnOverflow>
58-
{t('Assign to %s', title)}
59-
</Tooltip>
60-
);
61-
}
62-
6325
function DetectorPriorities({detector}: {detector: MetricDetector}) {
6426
const detectionType = detector.config?.detectionType || 'static';
6527

@@ -134,31 +96,13 @@ function DetectorResolve({detector}: {detector: MetricDetector}) {
13496
return <div>{description}</div>;
13597
}
13698

137-
function DetectorAssignee({owner}: {owner: string | null}) {
138-
if (!owner) {
139-
return t('Unassigned');
140-
}
141-
142-
const [ownerType, ownerId] = owner.split(':');
143-
if (ownerType === 'team') {
144-
return <AssignToTeam teamId={ownerId!} />;
145-
}
146-
if (ownerType === 'user') {
147-
return <AssignToUser userId={ownerId!} />;
148-
}
149-
150-
return t('Unassigned');
151-
}
152-
15399
export function MetricDetectorDetailsSidebar({detector}: DetectorDetailsSidebarProps) {
154100
return (
155101
<Fragment>
156102
<Section title={t('Detect')}>
157103
<MetricDetectorDetailsDetect detector={detector} />
158104
</Section>
159-
<Section title={t('Assign')}>
160-
<DetectorAssignee owner={detector.owner} />
161-
</Section>
105+
<DetectorDetailsAssignee owner={detector.owner} />
162106
<Section title={t('Prioritize')}>
163107
<DetectorPriorities detector={detector} />
164108
</Section>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {AutomationFixture} from 'sentry-fixture/automations';
2+
import {UptimeDetectorFixture} from 'sentry-fixture/detectors';
3+
import {ProjectFixture} from 'sentry-fixture/project';
4+
5+
import {render, screen} from 'sentry-test/reactTestingLibrary';
6+
7+
import {UptimeDetectorDetails} from 'sentry/views/detectors/components/details/uptime';
8+
9+
describe('UptimeDetectorDetails', function () {
10+
const defaultProps = {
11+
detector: UptimeDetectorFixture(),
12+
project: ProjectFixture(),
13+
};
14+
15+
beforeEach(function () {
16+
MockApiClient.addMockResponse({
17+
url: '/organizations/org-slug/workflows/',
18+
method: 'GET',
19+
body: [AutomationFixture()],
20+
});
21+
});
22+
23+
it('displays correct detector details', async function () {
24+
render(<UptimeDetectorDetails {...defaultProps} />);
25+
26+
expect(screen.getByText('Three consecutive failed checks.')).toBeInTheDocument();
27+
expect(screen.getByText('Three consecutive successful checks.')).toBeInTheDocument();
28+
29+
// Interval
30+
expect(screen.getByText('Every 1 minute')).toBeInTheDocument();
31+
// URL
32+
expect(screen.getByText('https://example.com')).toBeInTheDocument();
33+
// Method
34+
expect(screen.getByText('GET')).toBeInTheDocument();
35+
// Environment
36+
expect(screen.getByText('production')).toBeInTheDocument();
37+
38+
// Connected automation
39+
expect(await screen.findByText('Automation 1')).toBeInTheDocument();
40+
41+
// Edit button takes you to the edit page
42+
expect(screen.getByRole('button', {name: 'Edit'})).toHaveAttribute(
43+
'href',
44+
'/organizations/org-slug/issues/monitors/3/edit/'
45+
);
46+
});
47+
});
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {KeyValueTableRow} from 'sentry/components/keyValueTable';
2+
import DetailLayout from 'sentry/components/workflowEngine/layout/detail';
3+
import Section from 'sentry/components/workflowEngine/ui/section';
4+
import {t} from 'sentry/locale';
5+
import type {Project} from 'sentry/types/project';
6+
import type {UptimeDetector} from 'sentry/types/workflowEngine/detectors';
7+
import getDuration from 'sentry/utils/duration/getDuration';
8+
import {DetectorDetailsAssignee} from 'sentry/views/detectors/components/details/common/assignee';
9+
import {DetectorDetailsAutomations} from 'sentry/views/detectors/components/details/common/automations';
10+
import {DetectorExtraDetails} from 'sentry/views/detectors/components/details/common/extraDetails';
11+
import {DetectorDetailsHeader} from 'sentry/views/detectors/components/details/common/header';
12+
import {DetectorDetailsOngoingIssues} from 'sentry/views/detectors/components/details/common/ongoingIssues';
13+
14+
type UptimeDetectorDetailsProps = {
15+
detector: UptimeDetector;
16+
project: Project;
17+
};
18+
19+
export function UptimeDetectorDetails({detector, project}: UptimeDetectorDetailsProps) {
20+
const dataSource = detector.dataSources[0];
21+
22+
return (
23+
<DetailLayout>
24+
<DetectorDetailsHeader detector={detector} project={project} />
25+
<DetailLayout.Body>
26+
<DetailLayout.Main>
27+
<DetectorDetailsOngoingIssues />
28+
<DetectorDetailsAutomations detector={detector} />
29+
</DetailLayout.Main>
30+
<DetailLayout.Sidebar>
31+
<Section title={t('Detect')}>{t('Three consecutive failed checks.')}</Section>
32+
<Section title={t('Resolve')}>
33+
{t('Three consecutive successful checks.')}
34+
</Section>
35+
<DetectorDetailsAssignee owner={detector.owner} />
36+
<DetectorExtraDetails>
37+
<KeyValueTableRow
38+
keyName={t('Interval')}
39+
value={t('Every %s', getDuration(dataSource.queryObj.intervalSeconds))}
40+
/>
41+
<KeyValueTableRow
42+
keyName={t('URL')}
43+
value={detector.dataSources[0].queryObj.url}
44+
/>
45+
<KeyValueTableRow
46+
keyName={t('Method')}
47+
value={detector.dataSources[0].queryObj.method}
48+
/>
49+
<DetectorExtraDetails.Environment detector={detector} />
50+
<DetectorExtraDetails.DateCreated detector={detector} />
51+
<DetectorExtraDetails.CreatedBy detector={detector} />
52+
<DetectorExtraDetails.LastModified detector={detector} />
53+
</DetectorExtraDetails>
54+
</DetailLayout.Sidebar>
55+
</DetailLayout.Body>
56+
</DetailLayout>
57+
);
58+
}

static/app/views/detectors/components/forms/metric/metricFormData.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
Dataset,
2323
EventTypes,
2424
} from 'sentry/views/alerts/rules/metric/types';
25+
import {getDetectorEnvironment} from 'sentry/views/detectors/utils/getDetectorEnvironment';
2526

2627
/**
2728
* Dataset types for detectors
@@ -467,7 +468,7 @@ export function metricSavedDetectorToFormData(
467468
// Core detector fields
468469
name: detector.name,
469470
projectId: detector.projectId,
470-
environment: snubaQuery?.environment || '',
471+
environment: getDetectorEnvironment(detector) || '',
471472
owner: detector.owner || '',
472473
query: snubaQuery?.query || '',
473474
aggregateFunction,

static/app/views/detectors/components/forms/uptime/fields.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
Detector,
44
UptimeDetectorUpdatePayload,
55
} from 'sentry/types/workflowEngine/detectors';
6+
import {getDetectorEnvironment} from 'sentry/views/detectors/utils/getDetectorEnvironment';
67

78
export interface UptimeDetectorFormData {
89
body: string;
@@ -76,7 +77,7 @@ export function uptimeSavedDetectorToFormData(
7677
}
7778

7879
const dataSource = detector.dataSources?.[0];
79-
const environment = 'environment' in detector.config ? detector.config.environment : '';
80+
const environment = getDetectorEnvironment(detector);
8081

8182
const common = {
8283
name: detector.name,

0 commit comments

Comments
 (0)