diff --git a/static/app/types/workflowEngine/dataConditions.tsx b/static/app/types/workflowEngine/dataConditions.tsx index 7e9819f22d6d20..643747ef67b25b 100644 --- a/static/app/types/workflowEngine/dataConditions.tsx +++ b/static/app/types/workflowEngine/dataConditions.tsx @@ -1,21 +1,5 @@ import type {Action} from './actions'; -interface SnubaQuery { - aggregate: string; - dataset: string; - id: string; - query: string; - timeWindow: number; - environment?: string; -} - -export interface DataSource { - id: string; - snubaQuery: SnubaQuery; - status: number; - subscription?: string; -} - export enum DataConditionType { // operators EQUAL = 'eq', @@ -68,12 +52,19 @@ export enum DataConditionGroupLogicType { NONE = 'none', } +export const enum DetectorPriorityLevel { + HIGH = 75, + MEDIUM = 50, + LOW = 25, +} + export interface DataCondition { comparison: any; comparison_type: DataConditionType; id: string; condition_result?: any; } + export interface DataConditionGroup { conditions: DataCondition[]; id: string; diff --git a/static/app/types/workflowEngine/detectors.tsx b/static/app/types/workflowEngine/detectors.tsx index 334310a240efd8..eda35234392e87 100644 --- a/static/app/types/workflowEngine/detectors.tsx +++ b/static/app/types/workflowEngine/detectors.tsx @@ -1,7 +1,33 @@ -import type { - DataConditionGroup, - DataSource, -} from 'sentry/types/workflowEngine/dataConditions'; +import type {DataConditionGroup} from 'sentry/types/workflowEngine/dataConditions'; + +interface SnubaQuery { + aggregate: string; + dataset: string; + id: string; + query: string; + /** + * Time window in seconds + */ + timeWindow: number; + environment?: string; +} + +interface QueryObject { + id: string; + snubaQuery: SnubaQuery; + status: number; + subscription: string; +} + +export interface SnubaQueryDataSource { + id: string; + organizationId: string; + queryObj: QueryObject; + sourceId: string; + type: 'snuba_query_subscription'; +} + +export type DataSource = SnubaQueryDataSource; export type DetectorType = | 'crons' @@ -13,9 +39,9 @@ export type DetectorType = | 'uptime'; interface NewDetector { + conditionGroup: DataConditionGroup; config: Record; - dataCondition: DataConditionGroup; - dataSource: DataSource; + dataSources: DataSource[]; disabled: boolean; name: string; projectId: string; diff --git a/static/app/utils/useParams.tsx b/static/app/utils/useParams.tsx index a6bc199570d237..e86b208b426f9e 100644 --- a/static/app/utils/useParams.tsx +++ b/static/app/utils/useParams.tsx @@ -18,6 +18,7 @@ type ParamKeys = | 'codeId' | 'dataExportId' | 'dashboardId' + | 'detectorId' | 'docIntegrationSlug' | 'eventId' | 'fineTuneType' diff --git a/static/app/views/detectors/components/detailsPanel.tsx b/static/app/views/detectors/components/detailsPanel.tsx index 547ec9d5f4bf4b..5ad30548e618fd 100644 --- a/static/app/views/detectors/components/detailsPanel.tsx +++ b/static/app/views/detectors/components/detailsPanel.tsx @@ -4,17 +4,45 @@ import {Flex} from 'sentry/components/container/flex'; import {Container} from 'sentry/components/workflowEngine/ui/container'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; +import type {Detector, SnubaQueryDataSource} from 'sentry/types/workflowEngine/detectors'; +import {getExactDuration} from 'sentry/utils/duration/getExactDuration'; + +interface DetailsPanelProps { + detector: Detector; +} + +function SnubaQueryDetails({dataSource}: {dataSource: SnubaQueryDataSource}) { + return ( + + + {t('Query:')} + + {' '} + {dataSource.queryObj.snubaQuery.aggregate} + {' '} + {dataSource.queryObj.snubaQuery.query} + + + + {t('Threshold:')} + {getExactDuration(dataSource.queryObj.snubaQuery.timeWindow, true)} + + + ); +} + +function DetailsPanel({detector}: DetailsPanelProps) { + if (detector.dataSources[0]?.type === 'snuba_query_subscription') { + return ; + } -// TODO: Make component flexible for different alert types -function DetailsPanel() { return ( {t('Query:')} - {t('p75')} - {t('device.name is "Chrome"')} - {t('release')} + placeholder + placeholder {t('Threshold:')} diff --git a/static/app/views/detectors/detail.spec.tsx b/static/app/views/detectors/detail.spec.tsx new file mode 100644 index 00000000000000..216adf4d0c5264 --- /dev/null +++ b/static/app/views/detectors/detail.spec.tsx @@ -0,0 +1,64 @@ +import {DetectorDataSourceFixture, DetectorFixture} from 'sentry-fixture/detectors'; +import {OrganizationFixture} from 'sentry-fixture/organization'; +import {ProjectFixture} from 'sentry-fixture/project'; + +import {render, screen} from 'sentry-test/reactTestingLibrary'; + +import ProjectsStore from 'sentry/stores/projectsStore'; +import DetectorDetails from 'sentry/views/detectors/detail'; + +describe('DetectorDetails', function () { + const organization = OrganizationFixture({features: ['workflow-engine-ui']}); + const project = ProjectFixture(); + const defaultDataSource = DetectorDataSourceFixture(); + const snubaQueryDetector = DetectorFixture({ + projectId: project.id, + dataSources: [ + DetectorDataSourceFixture({ + queryObj: { + ...defaultDataSource.queryObj, + snubaQuery: { + ...defaultDataSource.queryObj.snubaQuery, + query: 'test', + environment: 'test-environment', + }, + }, + }), + ], + }); + const initialRouterConfig = { + location: { + pathname: `/organizations/${organization.slug}/issues/detectors/${snubaQueryDetector.id}/`, + }, + route: '/organizations/:orgId/issues/detectors/:detectorId/', + }; + + beforeEach(() => { + ProjectsStore.loadInitialData([project]); + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/detectors/${snubaQueryDetector.id}/`, + body: snubaQueryDetector, + }); + }); + + it('renders the detector name and snuba query', async function () { + render(, { + organization, + initialRouterConfig, + }); + + expect( + await screen.findByRole('heading', {name: snubaQueryDetector.name}) + ).toBeInTheDocument(); + // Displays the snuba query + expect( + screen.getByText(snubaQueryDetector.dataSources[0]!.queryObj.snubaQuery.query) + ).toBeInTheDocument(); + // Displays the environment + expect( + screen.getByText( + snubaQueryDetector.dataSources[0]!.queryObj.snubaQuery.environment! + ) + ).toBeInTheDocument(); + }); +}); diff --git a/static/app/views/detectors/detail.tsx b/static/app/views/detectors/detail.tsx index 8aa3fa40a75068..6e60283e0304bb 100644 --- a/static/app/views/detectors/detail.tsx +++ b/static/app/views/detectors/detail.tsx @@ -6,6 +6,8 @@ import {Button} from 'sentry/components/core/button'; import {LinkButton} from 'sentry/components/core/button/linkButton'; import {DateTime} from 'sentry/components/dateTime'; import {KeyValueTable, KeyValueTableRow} from 'sentry/components/keyValueTable'; +import LoadingError from 'sentry/components/loadingError'; +import LoadingIndicator from 'sentry/components/loadingIndicator'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import TimeSince from 'sentry/components/timeSince'; import {ActionsProvider} from 'sentry/components/workflowEngine/layout/actions'; @@ -16,14 +18,19 @@ import {useWorkflowEngineFeatureGate} from 'sentry/components/workflowEngine/use import {IconArrow, IconEdit} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; +import type {Detector} from 'sentry/types/workflowEngine/detectors'; import getDuration from 'sentry/utils/duration/getDuration'; import useOrganization from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; +import useProjects from 'sentry/utils/useProjects'; import {ConnectedAutomationsList} from 'sentry/views/detectors/components/connectedAutomationList'; import DetailsPanel from 'sentry/views/detectors/components/detailsPanel'; import IssuesList from 'sentry/views/detectors/components/issuesList'; import {useDetectorQuery} from 'sentry/views/detectors/hooks'; -import {makeMonitorBasePathname} from 'sentry/views/detectors/pathnames'; +import { + makeMonitorBasePathname, + makeMonitorDetailsPathname, +} from 'sentry/views/detectors/pathnames'; type Priority = { sensitivity: string; @@ -35,22 +42,40 @@ const priorities: Priority[] = [ {sensitivity: 'high', threshold: 10}, ]; -export default function DetectorDetail() { +function getDetectorEnvironment(detector: Detector) { + return detector.dataSources.find(ds => ds.queryObj.snubaQuery.environment)?.queryObj + .snubaQuery.environment; +} + +export default function DetectorDetails() { const organization = useOrganization(); useWorkflowEngineFeatureGate({redirect: true}); - const {detectorId} = useParams(); - if (!detectorId) { - throw new Error(`Unable to find detector.`); + const params = useParams<{detectorId: string}>(); + const {projects} = useProjects(); + + const { + data: detector, + isPending, + isError, + refetch, + } = useDetectorQuery(params.detectorId); + const project = projects.find(p => p.id === detector?.projectId); + + if (isPending) { + return ; + } + + if (isError || !project) { + return ; } - const {data: detector} = useDetectorQuery(detectorId); return ( - + - }> - + }> + {/* TODO: Add chart here */}
@@ -63,7 +88,7 @@ export default function DetectorDetail() {
- +
{t('Assign to %s', 'admin@sentry.io')} @@ -88,15 +113,17 @@ export default function DetectorDetail() { } + value={} /> - + } + value={} + /> + - -
@@ -107,7 +134,8 @@ export default function DetectorDetail() { ); } -function Actions() { +function Actions({detector}: {detector: Detector}) { + const organization = useOrganization(); const disable = () => { window.alert('disable'); }; @@ -116,7 +144,12 @@ function Actions() { - } size="sm"> + } + size="sm" + > {t('Edit')} diff --git a/tests/js/fixtures/dataConditions.ts b/tests/js/fixtures/dataConditions.ts index 2106c3d791e087..ac991be4024178 100644 --- a/tests/js/fixtures/dataConditions.ts +++ b/tests/js/fixtures/dataConditions.ts @@ -7,7 +7,7 @@ import { DataConditionType, } from 'sentry/types/workflowEngine/dataConditions'; -export function DataConditionFixture(params: Partial): DataCondition { +export function DataConditionFixture(params: Partial = {}): DataCondition { return { comparison_type: DataConditionType.EQUAL, comparison: '8', @@ -17,10 +17,10 @@ export function DataConditionFixture(params: Partial): DataCondit } export function DataConditionGroupFixture( - params: Partial + params: Partial = {} ): DataConditionGroup { return { - conditions: [DataConditionFixture({})], + conditions: [DataConditionFixture()], id: '1', logicType: DataConditionGroupLogicType.ANY, actions: [], diff --git a/tests/js/fixtures/detectors.ts b/tests/js/fixtures/detectors.ts index 31a151328807e7..333cc507001bf0 100644 --- a/tests/js/fixtures/detectors.ts +++ b/tests/js/fixtures/detectors.ts @@ -1,10 +1,9 @@ import {DataConditionGroupFixture} from 'sentry-fixture/dataConditions'; import {UserFixture} from 'sentry-fixture/user'; -import type {DataSource} from 'sentry/types/workflowEngine/dataConditions'; -import type {Detector} from 'sentry/types/workflowEngine/detectors'; +import type {DataSource, Detector} from 'sentry/types/workflowEngine/detectors'; -export function DetectorFixture(params: Partial): Detector { +export function DetectorFixture(params: Partial = {}): Detector { return { id: '1', name: 'detector', @@ -16,24 +15,31 @@ export function DetectorFixture(params: Partial): Detector { workflowIds: [], config: {}, type: 'metric', - dataCondition: DataConditionGroupFixture({}), disabled: false, - dataSource: params.dataSource ?? DetectorDataSource({}), + conditionGroup: params.conditionGroup ?? DataConditionGroupFixture(), + dataSources: params.dataSources ?? [DetectorDataSourceFixture()], ...params, }; } -export function DetectorDataSource(params: Partial): DataSource { +export function DetectorDataSourceFixture(params: Partial = {}): DataSource { return { id: '1', - status: 1, - snubaQuery: { - aggregate: '', - dataset: '', - id: '', - query: '', - timeWindow: 60, - ...params, + organizationId: '1', + sourceId: '1', + type: 'snuba_query_subscription', + queryObj: { + id: '1', + status: 1, + subscription: '1', + snubaQuery: { + aggregate: '', + dataset: '', + id: '', + query: '', + timeWindow: 60, + }, }, + ...params, }; }