diff --git a/static/app/views/detectors/components/detectorTypeForm.tsx b/static/app/views/detectors/components/detectorTypeForm.tsx index 091094425899ef..892775f477bd5e 100644 --- a/static/app/views/detectors/components/detectorTypeForm.tsx +++ b/static/app/views/detectors/components/detectorTypeForm.tsx @@ -1,5 +1,6 @@ import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; +import {parseAsStringEnum, useQueryState} from 'nuqs'; import {Flex, Stack} from 'sentry/components/core/layout'; import {ExternalLink, Link} from 'sentry/components/core/link'; @@ -9,8 +10,6 @@ import Hook from 'sentry/components/hook'; import {t, tct} from 'sentry/locale'; import HookStore from 'sentry/stores/hookStore'; import type {DetectorType} from 'sentry/types/workflowEngine/detectors'; -import {useLocation} from 'sentry/utils/useLocation'; -import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; import { makeAutomationBasePathname, @@ -44,9 +43,28 @@ export function DetectorTypeForm() { ); } +type SelectableDetectorType = Extract< + DetectorType, + 'metric_issue' | 'monitor_check_in_failure' | 'uptime_domain_failure' +>; + +const ALLOWED_DETECTOR_TYPES = [ + 'metric_issue', + 'monitor_check_in_failure', + 'uptime_domain_failure', +] as const satisfies SelectableDetectorType[]; + +const detectorTypeParser = parseAsStringEnum(ALLOWED_DETECTOR_TYPES) + .withOptions({history: 'replace', clearOnDefault: false}) + .withDefault(ALLOWED_DETECTOR_TYPES[0]); + +export function useDetectorTypeQueryState() { + return useQueryState('detectorType', detectorTypeParser); +} + interface DetectorTypeOption { description: string; - id: DetectorType; + id: SelectableDetectorType; name: string; visualization: React.ReactNode; disabled?: boolean; @@ -54,23 +72,15 @@ interface DetectorTypeOption { } function MonitorTypeField() { - const location = useLocation(); - const navigate = useNavigate(); - const selectedDetectorType = location.query.detectorType as DetectorType; + const [selectedDetectorType, setDetectorType] = useDetectorTypeQueryState(); const useMetricDetectorLimit = HookStore.get('react-hook:use-metric-detector-limit')[0] ?? (() => null); const quota = useMetricDetectorLimit(); const canCreateMetricDetector = !quota?.hasReachedLimit; - const handleChange = (value: DetectorType) => { - navigate({ - pathname: location.pathname, - query: { - ...location.query, - detectorType: value, - }, - }); + const handleChange = (value: SelectableDetectorType) => { + setDetectorType(value); }; const options: DetectorTypeOption[] = [ diff --git a/static/app/views/detectors/new-settings.tsx b/static/app/views/detectors/new-settings.tsx index fe702a34999ad5..85c580f8b4f958 100644 --- a/static/app/views/detectors/new-settings.tsx +++ b/static/app/views/detectors/new-settings.tsx @@ -1,12 +1,13 @@ +import {parseAsString, useQueryState} from 'nuqs'; + import * as Layout from 'sentry/components/layouts/thirds'; import LoadingError from 'sentry/components/loadingError'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import {useWorkflowEngineFeatureGate} from 'sentry/components/workflowEngine/useWorkflowEngineFeatureGate'; import {t} from 'sentry/locale'; -import type {DetectorType} from 'sentry/types/workflowEngine/detectors'; -import {useLocation} from 'sentry/utils/useLocation'; import useProjects from 'sentry/utils/useProjects'; +import {useDetectorTypeQueryState} from 'sentry/views/detectors/components/detectorTypeForm'; import {NewDetectorForm} from 'sentry/views/detectors/components/forms'; import {DetectorFormProvider} from 'sentry/views/detectors/components/forms/context'; import { @@ -15,13 +16,13 @@ import { } from 'sentry/views/detectors/utils/detectorTypeConfig'; export default function DetectorNewSettings() { - const location = useLocation(); const {projects, fetching: isFetchingProjects} = useProjects(); - const detectorType = location.query.detectorType as DetectorType; + const [detectorType] = useDetectorTypeQueryState(); + const [projectId] = useQueryState('project', parseAsString); useWorkflowEngineFeatureGate({redirect: true}); - if (!isValidDetectorType(detectorType)) { - return ; + if (!detectorType || !isValidDetectorType(detectorType)) { + return ; } if (isFetchingProjects) { @@ -36,7 +37,7 @@ export default function DetectorNewSettings() { ); } - const project = projects.find(p => p.id === (location.query.project as string)); + const project = projectId ? projects.find(p => p.id === projectId) : undefined; if (!project) { return ; } diff --git a/static/app/views/detectors/new.spec.tsx b/static/app/views/detectors/new.spec.tsx index ba70e8de97d429..fc1e98d364c3fa 100644 --- a/static/app/views/detectors/new.spec.tsx +++ b/static/app/views/detectors/new.spec.tsx @@ -23,9 +23,6 @@ describe('DetectorNew', () => { it('sets query parameters for project, environment, and detectorType', async () => { const {router} = render(); - // Next button should be disabled if no detectorType is selected - expect(screen.getByRole('button', {name: 'Next'})).toBeDisabled(); - // Set detectorType await userEvent.click(screen.getByRole('radio', {name: 'Uptime'})); diff --git a/static/app/views/detectors/new.tsx b/static/app/views/detectors/new.tsx index 852d9de475e83b..606308aba71eef 100644 --- a/static/app/views/detectors/new.tsx +++ b/static/app/views/detectors/new.tsx @@ -1,19 +1,21 @@ import {useTheme} from '@emotion/react'; +import {parseAsString, useQueryState} from 'nuqs'; import {Breadcrumbs} from 'sentry/components/breadcrumbs'; import {Button} from 'sentry/components/core/button'; import {LinkButton} from 'sentry/components/core/button/linkButton'; -import {Tooltip} from 'sentry/components/core/tooltip'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import EditLayout from 'sentry/components/workflowEngine/layout/edit'; import {useWorkflowEngineFeatureGate} from 'sentry/components/workflowEngine/useWorkflowEngineFeatureGate'; import {t} from 'sentry/locale'; import type {DetectorType} from 'sentry/types/workflowEngine/detectors'; -import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import useOrganization from 'sentry/utils/useOrganization'; import useProjects from 'sentry/utils/useProjects'; -import {DetectorTypeForm} from 'sentry/views/detectors/components/detectorTypeForm'; +import { + DetectorTypeForm, + useDetectorTypeQueryState, +} from 'sentry/views/detectors/components/detectorTypeForm'; import {MonitorFeedbackButton} from 'sentry/views/detectors/components/monitorFeedbackButton'; import {makeMonitorBasePathname} from 'sentry/views/detectors/pathnames'; @@ -43,14 +45,11 @@ export default function DetectorNew() { const navigate = useNavigate(); const organization = useOrganization(); useWorkflowEngineFeatureGate({redirect: true}); - const location = useLocation(); const {projects} = useProjects(); const theme = useTheme(); const maxWidth = theme.breakpoints.xl; - const detectorType = location.query.detectorType as DetectorType; - - const projectIdFromLocation = - typeof location.query.project === 'string' ? location.query.project : undefined; + const [detectorType] = useDetectorTypeQueryState(); + const [projectIdFromLocation] = useQueryState('project', parseAsString); const defaultProject = projects.find(p => p.isMember) ?? projects[0]; const newMonitorName = t('New Monitor'); @@ -62,7 +61,7 @@ export default function DetectorNew() { navigate({ pathname: `${makeMonitorBasePathname(organization.slug)}new/settings/`, query: { - detectorType: location.query.detectorType as DetectorType, + detectorType, project: data.project, }, }); @@ -95,15 +94,9 @@ export default function DetectorNew() { {t('Cancel')} - - - + );