From c4af1cad42112c61ec94bc873ede79bf253cb9f1 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Fri, 11 Jul 2025 16:07:37 -0700 Subject: [PATCH 1/2] feat(aci): Implement uptime detector details page --- static/app/types/workflowEngine/detectors.tsx | 6 +- .../components/details/common/assignee.tsx | 63 +++++++++++++++++++ .../details/common/extraDetails.tsx | 15 +---- .../detectors/components/details/fallback.tsx | 2 + .../detectors/components/details/index.tsx | 2 + .../components/details/metric/sidebar.tsx | 60 +----------------- .../components/details/uptime/index.spec.tsx | 47 ++++++++++++++ .../components/details/uptime/index.tsx | 58 +++++++++++++++++ .../forms/metric/metricFormData.tsx | 3 +- .../components/forms/uptime/fields.tsx | 3 +- .../utils/getDetectorEnvironment.tsx | 26 ++++++++ tests/js/fixtures/detectors.ts | 49 ++++++++++++++- 12 files changed, 256 insertions(+), 78 deletions(-) create mode 100644 static/app/views/detectors/components/details/common/assignee.tsx create mode 100644 static/app/views/detectors/components/details/uptime/index.spec.tsx create mode 100644 static/app/views/detectors/components/details/uptime/index.tsx create mode 100644 static/app/views/detectors/utils/getDetectorEnvironment.tsx diff --git a/static/app/types/workflowEngine/detectors.tsx b/static/app/types/workflowEngine/detectors.tsx index b20ad1334b1102..60bd996c0ce721 100644 --- a/static/app/types/workflowEngine/detectors.tsx +++ b/static/app/types/workflowEngine/detectors.tsx @@ -48,15 +48,13 @@ export interface SnubaQueryDataSource extends BaseDataSource { type: 'snuba_query_subscription'; } -interface UptimeSubscriptionDataSource extends BaseDataSource { +export interface UptimeSubscriptionDataSource extends BaseDataSource { /** * See UptimeSubscriptionSerializer */ queryObj: { body: string | null; headers: Array<[string, string]>; - hostProviderId: string; - hostProviderName: string; intervalSeconds: number; method: string; timeoutMs: number; @@ -133,7 +131,7 @@ export interface MetricDetector extends BaseDetector { export interface UptimeDetector extends BaseDetector { readonly config: UptimeDetectorConfig; - readonly dataSources: UptimeSubscriptionDataSource[]; + readonly dataSources: [UptimeSubscriptionDataSource]; readonly type: 'uptime_domain_failure'; } diff --git a/static/app/views/detectors/components/details/common/assignee.tsx b/static/app/views/detectors/components/details/common/assignee.tsx new file mode 100644 index 00000000000000..5ecf5cdaa815b1 --- /dev/null +++ b/static/app/views/detectors/components/details/common/assignee.tsx @@ -0,0 +1,63 @@ +import {Flex} from 'sentry/components/core/layout/flex'; +import {Tooltip} from 'sentry/components/core/tooltip'; +import Placeholder from 'sentry/components/placeholder'; +import Section from 'sentry/components/workflowEngine/ui/section'; +import {t} from 'sentry/locale'; +import {space} from 'sentry/styles/space'; +import {parseActorIdentifier} from 'sentry/utils/parseActorIdentifier'; +import {useTeamsById} from 'sentry/utils/useTeamsById'; +import useUserFromId from 'sentry/utils/useUserFromId'; + +function AssignToTeam({teamId}: {teamId: string}) { + const {teams, isLoading} = useTeamsById({ids: [teamId]}); + const team = teams.find(tm => tm.id === teamId); + + if (isLoading) { + return ( + + {t('Assign to')} + + ); + } + + return t('Assign to %s', `#${team?.slug ?? 'unknown'}`); +} + +function AssignToUser({userId}: {userId: string}) { + const {isPending, data: user} = useUserFromId({id: parseInt(userId, 10)}); + + if (isPending) { + return ( + + {t('Assign to')} + + ); + } + + const title = user?.name ?? user?.email ?? t('Unknown user'); + return ( + + {t('Assign to %s', title)} + + ); +} + +function DetectorOwner({owner}: {owner: string | null}) { + const parsed = parseActorIdentifier(owner); + if (parsed?.type === 'team') { + return ; + } + if (parsed?.type === 'user') { + return ; + } + + return t('Unassigned'); +} + +export function DetectorDetailsAssignee({owner}: {owner: string | null}) { + return ( +
+ +
+ ); +} diff --git a/static/app/views/detectors/components/details/common/extraDetails.tsx b/static/app/views/detectors/components/details/common/extraDetails.tsx index 6ae85bc2c89504..f2593ae7bc1b43 100644 --- a/static/app/views/detectors/components/details/common/extraDetails.tsx +++ b/static/app/views/detectors/components/details/common/extraDetails.tsx @@ -10,6 +10,7 @@ import Section from 'sentry/components/workflowEngine/ui/section'; import {t} from 'sentry/locale'; import type {Detector} from 'sentry/types/workflowEngine/detectors'; import useUserFromId from 'sentry/utils/useUserFromId'; +import {getDetectorEnvironment} from 'sentry/views/detectors/utils/getDetectorEnvironment'; type Props = { children: React.ReactNode; @@ -97,19 +98,7 @@ DetectorExtraDetails.Environment = function DetectorExtraDetailsEnvironment({ }: { detector: Detector; }) { - // TODO: Add common function for getting environment from a detector - const getEnvironment = () => { - if (detector.type !== 'metric_issue') { - return ''; - } - - return ( - detector.dataSources?.find(ds => ds.type === 'snuba_query_subscription')?.queryObj - ?.snubaQuery.environment ?? t('All environments') - ); - }; - - const environment = getEnvironment(); + const environment = getDetectorEnvironment(detector); return ( + diff --git a/static/app/views/detectors/components/details/index.tsx b/static/app/views/detectors/components/details/index.tsx index 3de6106ec040f1..9316ad79c30c2e 100644 --- a/static/app/views/detectors/components/details/index.tsx +++ b/static/app/views/detectors/components/details/index.tsx @@ -3,6 +3,7 @@ import type {Detector} from 'sentry/types/workflowEngine/detectors'; import {unreachable} from 'sentry/utils/unreachable'; import {FallbackDetectorDetails} from 'sentry/views/detectors/components/details/fallback'; import {MetricDetectorDetails} from 'sentry/views/detectors/components/details/metric'; +import {UptimeDetectorDetails} from 'sentry/views/detectors/components/details/uptime'; type DetectorDetailsContentProps = { detector: Detector; @@ -15,6 +16,7 @@ export function DetectorDetailsContent({detector, project}: DetectorDetailsConte case 'metric_issue': return ; case 'uptime_domain_failure': + return ; case 'uptime_subscription': case 'error': return ; diff --git a/static/app/views/detectors/components/details/metric/sidebar.tsx b/static/app/views/detectors/components/details/metric/sidebar.tsx index 666bb5e0e7a974..20670ceca252c1 100644 --- a/static/app/views/detectors/components/details/metric/sidebar.tsx +++ b/static/app/views/detectors/components/details/metric/sidebar.tsx @@ -2,9 +2,6 @@ import {Fragment} from 'react'; import styled from '@emotion/styled'; import {GroupPriorityBadge} from 'sentry/components/badge/groupPriority'; -import {Flex} from 'sentry/components/core/layout'; -import {Tooltip} from 'sentry/components/core/tooltip'; -import Placeholder from 'sentry/components/placeholder'; import Section from 'sentry/components/workflowEngine/ui/section'; import {IconArrow} from 'sentry/icons'; import {t} from 'sentry/locale'; @@ -15,8 +12,7 @@ import { DetectorPriorityLevel, } from 'sentry/types/workflowEngine/dataConditions'; import type {MetricDetector} from 'sentry/types/workflowEngine/detectors'; -import {useTeamsById} from 'sentry/utils/useTeamsById'; -import useUserFromId from 'sentry/utils/useUserFromId'; +import {DetectorDetailsAssignee} from 'sentry/views/detectors/components/details/common/assignee'; import {DetectorExtraDetails} from 'sentry/views/detectors/components/details/common/extraDetails'; import {MetricDetectorDetailsDetect} from 'sentry/views/detectors/components/details/metric/detect'; import {getResolutionDescription} from 'sentry/views/detectors/utils/getDetectorResolutionDescription'; @@ -26,40 +22,6 @@ interface DetectorDetailsSidebarProps { detector: MetricDetector; } -function AssignToTeam({teamId}: {teamId: string}) { - const {teams, isLoading} = useTeamsById({ids: [teamId]}); - const team = teams.find(tm => tm.id === teamId); - - if (isLoading) { - return ( - - {t('Assign to')} - - ); - } - - return t('Assign to %s', `#${team?.slug ?? 'unknown'}`); -} - -function AssignToUser({userId}: {userId: string}) { - const {isPending, data: user} = useUserFromId({id: parseInt(userId, 10)}); - - if (isPending) { - return ( - - {t('Assign to')} - - ); - } - - const title = user?.name ?? user?.email ?? t('Unknown user'); - return ( - - {t('Assign to %s', title)} - - ); -} - function DetectorPriorities({detector}: {detector: MetricDetector}) { const detectionType = detector.config?.detectionType || 'static'; @@ -134,31 +96,13 @@ function DetectorResolve({detector}: {detector: MetricDetector}) { return
{description}
; } -function DetectorAssignee({owner}: {owner: string | null}) { - if (!owner) { - return t('Unassigned'); - } - - const [ownerType, ownerId] = owner.split(':'); - if (ownerType === 'team') { - return ; - } - if (ownerType === 'user') { - return ; - } - - return t('Unassigned'); -} - export function MetricDetectorDetailsSidebar({detector}: DetectorDetailsSidebarProps) { return (
-
- -
+
diff --git a/static/app/views/detectors/components/details/uptime/index.spec.tsx b/static/app/views/detectors/components/details/uptime/index.spec.tsx new file mode 100644 index 00000000000000..a6f9603801863e --- /dev/null +++ b/static/app/views/detectors/components/details/uptime/index.spec.tsx @@ -0,0 +1,47 @@ +import {AutomationFixture} from 'sentry-fixture/automations'; +import {UptimeDetectorFixture} from 'sentry-fixture/detectors'; +import {ProjectFixture} from 'sentry-fixture/project'; + +import {render, screen} from 'sentry-test/reactTestingLibrary'; + +import {UptimeDetectorDetails} from 'sentry/views/detectors/components/details/uptime'; + +describe('UptimeDetectorDetails', function () { + const defaultProps = { + detector: UptimeDetectorFixture(), + project: ProjectFixture(), + }; + + beforeEach(function () { + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/workflows/', + method: 'GET', + body: [AutomationFixture()], + }); + }); + + it('displays correct detector details', async function () { + render(); + + expect(screen.getByText('Three consecutive failed checks.')).toBeInTheDocument(); + expect(screen.getByText('Three consecutive successful checks.')).toBeInTheDocument(); + + // Interval + expect(screen.getByText('Every 1 minute')).toBeInTheDocument(); + // URL + expect(screen.getByText('https://example.com')).toBeInTheDocument(); + // Method + expect(screen.getByText('GET')).toBeInTheDocument(); + // Environment + expect(screen.getByText('production')).toBeInTheDocument(); + + // Connected automation + expect(await screen.findByText('Automation 1')).toBeInTheDocument(); + + // Edit button takes you to the edit page + expect(screen.getByRole('button', {name: 'Edit'})).toHaveAttribute( + 'href', + '/organizations/org-slug/issues/monitors/3/edit/' + ); + }); +}); diff --git a/static/app/views/detectors/components/details/uptime/index.tsx b/static/app/views/detectors/components/details/uptime/index.tsx new file mode 100644 index 00000000000000..3593087cc6e1af --- /dev/null +++ b/static/app/views/detectors/components/details/uptime/index.tsx @@ -0,0 +1,58 @@ +import {KeyValueTableRow} from 'sentry/components/keyValueTable'; +import DetailLayout from 'sentry/components/workflowEngine/layout/detail'; +import Section from 'sentry/components/workflowEngine/ui/section'; +import {t} from 'sentry/locale'; +import type {Project} from 'sentry/types/project'; +import type {UptimeDetector} from 'sentry/types/workflowEngine/detectors'; +import getDuration from 'sentry/utils/duration/getDuration'; +import {DetectorDetailsAssignee} from 'sentry/views/detectors/components/details/common/assignee'; +import {DetectorDetailsAutomations} from 'sentry/views/detectors/components/details/common/automations'; +import {DetectorExtraDetails} from 'sentry/views/detectors/components/details/common/extraDetails'; +import {DetectorDetailsHeader} from 'sentry/views/detectors/components/details/common/header'; +import {DetectorDetailsOngoingIssues} from 'sentry/views/detectors/components/details/common/ongoingIssues'; + +type UptimeDetectorDetailsProps = { + detector: UptimeDetector; + project: Project; +}; + +export function UptimeDetectorDetails({detector, project}: UptimeDetectorDetailsProps) { + const dataSource = detector.dataSources[0]; + + return ( + + + + + + + + +
{t('Three consecutive failed checks.')}
+
+ {t('Three consecutive successful checks.')} +
+ + + + + + + + + + +
+
+
+ ); +} diff --git a/static/app/views/detectors/components/forms/metric/metricFormData.tsx b/static/app/views/detectors/components/forms/metric/metricFormData.tsx index 73c8782e450f87..e478e1532190ab 100644 --- a/static/app/views/detectors/components/forms/metric/metricFormData.tsx +++ b/static/app/views/detectors/components/forms/metric/metricFormData.tsx @@ -22,6 +22,7 @@ import { Dataset, EventTypes, } from 'sentry/views/alerts/rules/metric/types'; +import {getDetectorEnvironment} from 'sentry/views/detectors/utils/getDetectorEnvironment'; /** * Dataset types for detectors @@ -459,7 +460,7 @@ export function metricSavedDetectorToFormData( // Core detector fields name: detector.name, projectId: detector.projectId, - environment: snubaQuery?.environment || '', + environment: getDetectorEnvironment(detector) || '', owner: detector.owner || '', query: snubaQuery?.query || '', aggregateFunction, diff --git a/static/app/views/detectors/components/forms/uptime/fields.tsx b/static/app/views/detectors/components/forms/uptime/fields.tsx index 998b26c34377c5..2de0fdd4193d3c 100644 --- a/static/app/views/detectors/components/forms/uptime/fields.tsx +++ b/static/app/views/detectors/components/forms/uptime/fields.tsx @@ -3,6 +3,7 @@ import type { Detector, UptimeDetectorUpdatePayload, } from 'sentry/types/workflowEngine/detectors'; +import {getDetectorEnvironment} from 'sentry/views/detectors/utils/getDetectorEnvironment'; export interface UptimeDetectorFormData { body: string; @@ -76,7 +77,7 @@ export function uptimeSavedDetectorToFormData( } const dataSource = detector.dataSources?.[0]; - const environment = 'environment' in detector.config ? detector.config.environment : ''; + const environment = getDetectorEnvironment(detector); const common = { name: detector.name, diff --git a/static/app/views/detectors/utils/getDetectorEnvironment.tsx b/static/app/views/detectors/utils/getDetectorEnvironment.tsx new file mode 100644 index 00000000000000..55ff2cb078c5d9 --- /dev/null +++ b/static/app/views/detectors/utils/getDetectorEnvironment.tsx @@ -0,0 +1,26 @@ +import {t} from 'sentry/locale'; +import type {Detector} from 'sentry/types/workflowEngine/detectors'; +import {unreachable} from 'sentry/utils/unreachable'; + +const ALL_ENVIRONMENTS = t('All environments'); + +export function getDetectorEnvironment(detector: Detector) { + const detectorType = detector.type; + switch (detectorType) { + case 'metric_issue': + return ( + detector.dataSources?.find(ds => ds.type === 'snuba_query_subscription')?.queryObj + ?.snubaQuery.environment ?? ALL_ENVIRONMENTS + ); + case 'uptime_domain_failure': + return detector.config.environment ?? ALL_ENVIRONMENTS; + case 'uptime_subscription': + // TODO: Implement this when we know the shape of object + return ALL_ENVIRONMENTS; + case 'error': + return ALL_ENVIRONMENTS; + default: + unreachable(detectorType); + return ALL_ENVIRONMENTS; + } +} diff --git a/tests/js/fixtures/detectors.ts b/tests/js/fixtures/detectors.ts index 4893a032d0ab7e..678deea99d1d63 100644 --- a/tests/js/fixtures/detectors.ts +++ b/tests/js/fixtures/detectors.ts @@ -1,3 +1,4 @@ +import {AutomationFixture} from 'sentry-fixture/automations'; import {DataConditionGroupFixture} from 'sentry-fixture/dataConditions'; import {UserFixture} from 'sentry-fixture/user'; @@ -5,6 +6,8 @@ import type { ErrorDetector, MetricDetector, SnubaQueryDataSource, + UptimeDetector, + UptimeSubscriptionDataSource, } from 'sentry/types/workflowEngine/detectors'; import {Dataset, EventTypes} from 'sentry/views/alerts/rules/metric/types'; @@ -40,7 +43,7 @@ export function ErrorDetectorFixture(params: Partial = {}): Error dateCreated: '2025-01-01T00:00:00.000Z', dateUpdated: '2025-01-01T00:00:00.000Z', disabled: false, - id: '1', + id: '2', lastTriggered: '2025-01-01T00:00:00.000Z', owner: null, projectId: '1', @@ -50,6 +53,50 @@ export function ErrorDetectorFixture(params: Partial = {}): Error }; } +export function UptimeDetectorFixture( + params: Partial = {} +): UptimeDetector { + return { + name: 'Uptime Detector', + createdBy: null, + dateCreated: '2025-01-01T00:00:00.000Z', + dateUpdated: '2025-01-01T00:00:00.000Z', + disabled: false, + id: '3', + lastTriggered: '2025-01-01T00:00:00.000Z', + owner: null, + projectId: '1', + workflowIds: [AutomationFixture().id], + type: 'uptime_domain_failure', + config: { + environment: 'production', + }, + dataSources: [UptimeSubscriptionDataSourceFixture()], + ...params, + }; +} + +export function UptimeSubscriptionDataSourceFixture( + params: Partial = {} +): UptimeSubscriptionDataSource { + return { + id: '1', + organizationId: '1', + sourceId: '1', + type: 'uptime_subscription', + queryObj: { + body: null, + headers: [], + intervalSeconds: 60, + method: 'GET', + timeoutMs: 5000, + traceSampling: false, + url: 'https://example.com', + }, + ...params, + }; +} + export function SnubaQueryDataSourceFixture( params: Partial = {} ): SnubaQueryDataSource { From 3e68d934e12a27c49ad2e88d3a96ea4aa4435fab Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Fri, 11 Jul 2025 16:12:46 -0700 Subject: [PATCH 2/2] Remove unused export --- tests/js/fixtures/detectors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/js/fixtures/detectors.ts b/tests/js/fixtures/detectors.ts index 678deea99d1d63..5abde3acd3df83 100644 --- a/tests/js/fixtures/detectors.ts +++ b/tests/js/fixtures/detectors.ts @@ -76,7 +76,7 @@ export function UptimeDetectorFixture( }; } -export function UptimeSubscriptionDataSourceFixture( +function UptimeSubscriptionDataSourceFixture( params: Partial = {} ): UptimeSubscriptionDataSource { return {