diff --git a/static/app/components/workflowEngine/form/control/index.stories.tsx b/static/app/components/workflowEngine/form/control/index.stories.tsx
index 2c4febfcb1c9ec..f4f91b501f610a 100644
--- a/static/app/components/workflowEngine/form/control/index.stories.tsx
+++ b/static/app/components/workflowEngine/form/control/index.stories.tsx
@@ -5,6 +5,7 @@ import Form from 'sentry/components/forms/form';
import PriorityControl from 'sentry/components/workflowEngine/form/control/priorityControl';
import * as Storybook from 'sentry/stories';
import {space} from 'sentry/styles/space';
+import {DetectorPriorityLevel} from 'sentry/types/workflowEngine/dataConditions';
export default Storybook.story('Form Controls', story => {
story('PriorityControl', () => (
@@ -16,7 +17,7 @@ export default Storybook.story('Form Controls', story => {
diff --git a/static/app/components/workflowEngine/form/control/priorityControl.spec.tsx b/static/app/components/workflowEngine/form/control/priorityControl.spec.tsx
index 54da50adea6af6..603e5cb03e4907 100644
--- a/static/app/components/workflowEngine/form/control/priorityControl.spec.tsx
+++ b/static/app/components/workflowEngine/form/control/priorityControl.spec.tsx
@@ -19,7 +19,7 @@ describe('PriorityControl', function () {
});
render(
);
@@ -37,7 +37,7 @@ describe('PriorityControl', function () {
});
render(
);
expect(await screen.findByRole('button', {name: 'Low'})).toBeInTheDocument();
@@ -62,7 +62,7 @@ describe('PriorityControl', function () {
});
render(
);
const medium = await screen.findByTestId('priority-control-medium');
diff --git a/static/app/components/workflowEngine/form/control/priorityControl.tsx b/static/app/components/workflowEngine/form/control/priorityControl.tsx
index 17ebffabf0ae76..0a004feaa37439 100644
--- a/static/app/components/workflowEngine/form/control/priorityControl.tsx
+++ b/static/app/components/workflowEngine/form/control/priorityControl.tsx
@@ -4,7 +4,6 @@ import styled from '@emotion/styled';
import {GroupPriorityBadge} from 'sentry/components/badge/groupPriority';
import {Flex} from 'sentry/components/container/flex';
import {CompactSelect} from 'sentry/components/core/compactSelect';
-import {FieldWrapper} from 'sentry/components/forms/fieldGroup/fieldWrapper';
import NumberField from 'sentry/components/forms/fields/numberField';
import FormContext from 'sentry/components/forms/formContext';
import InteractionStateLayer from 'sentry/components/interactionStateLayer';
@@ -67,12 +66,10 @@ function ChangePriority() {
}
interface PriorityControlProps {
- minimumPriority?: DetectorPriorityLevel;
+ minimumPriority: DetectorPriorityLevel;
}
-export default function PriorityControl({
- minimumPriority = DetectorPriorityLevel.LOW,
-}: PriorityControlProps) {
+export default function PriorityControl({minimumPriority}: PriorityControlProps) {
// TODO: kind type not yet available from detector types
const detectorKind = useMetricDetectorFormField(METRIC_DETECTOR_FORM_FIELDS.kind);
const initialPriorityLevel = useMetricDetectorFormField(
@@ -84,7 +81,7 @@ export default function PriorityControl({
- {!detectorKind || detectorKind === 'threshold' ? (
+ {!detectorKind || detectorKind === 'static' ? (
) : (
@@ -97,7 +94,7 @@ export default function PriorityControl({
{priorityIsConfigurable(initialPriorityLevel, DetectorPriorityLevel.MEDIUM) && (
-
- {left}
- |
+ {left} |
-
- {right}
- |
+ {right} |
);
}
@@ -227,13 +220,16 @@ const Row = styled('div')`
display: contents;
`;
-const Cell = styled(Flex)`
+const Cell = styled('div')`
+ display: flex;
+ align-items: center;
+ justify-content: center;
padding: ${space(1)};
+`;
- ${FieldWrapper} {
- padding: 0;
- width: 5rem;
- }
+const SmallNumberField = styled(NumberField)`
+ width: 5rem;
+ padding: 0;
`;
const SecondaryLabel = styled('div')`
diff --git a/static/app/types/workflowEngine/detectors.tsx b/static/app/types/workflowEngine/detectors.tsx
index 1165a2006e02db..b02cc02727d748 100644
--- a/static/app/types/workflowEngine/detectors.tsx
+++ b/static/app/types/workflowEngine/detectors.tsx
@@ -1,4 +1,5 @@
import type {DataConditionGroup} from 'sentry/types/workflowEngine/dataConditions';
+import type {AlertRuleSensitivity} from 'sentry/views/alerts/rules/metric/types';
/**
* See SnubaQuerySerializer
@@ -60,9 +61,42 @@ type DataSource = SnubaQueryDataSource | UptimeSubscriptionDataSource;
export type DetectorType = 'error' | 'metric_issue' | 'uptime_domain_failure';
+interface BaseDetectorConfig {
+ threshold_period: number;
+}
+
+/**
+ * Configuration for static/threshold-based detection
+ */
+interface StaticDetectorConfig extends BaseDetectorConfig {
+ detection_type: 'static';
+}
+
+/**
+ * Configuration for percentage-based change detection
+ */
+interface PercentDetectorConfig extends BaseDetectorConfig {
+ comparison_delta: number;
+ detection_type: 'percent';
+}
+
+/**
+ * Configuration for dynamic/anomaly detection
+ */
+interface DynamicDetectorConfig extends BaseDetectorConfig {
+ detection_type: 'dynamic';
+ seasonality?: 'auto' | 'daily' | 'weekly' | 'monthly';
+ sensitivity?: AlertRuleSensitivity;
+}
+
+export type DetectorConfig =
+ | StaticDetectorConfig
+ | PercentDetectorConfig
+ | DynamicDetectorConfig;
+
interface NewDetector {
conditionGroup: DataConditionGroup | null;
- config: Record;
+ config: DetectorConfig;
dataSources: DataSource[] | null;
disabled: boolean;
name: string;
diff --git a/static/app/views/detectors/components/detectorSubtitle.tsx b/static/app/views/detectors/components/detectorSubtitle.tsx
new file mode 100644
index 00000000000000..0db7779b3e3a58
--- /dev/null
+++ b/static/app/views/detectors/components/detectorSubtitle.tsx
@@ -0,0 +1,27 @@
+import {Flex} from 'sentry/components/container/flex';
+import {ProjectAvatar} from 'sentry/components/core/avatar/projectAvatar';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import useProjects from 'sentry/utils/useProjects';
+
+interface DetectorSubtitleProps {
+ environment: string;
+ projectId: string;
+}
+
+export function DetectorSubtitle({projectId, environment}: DetectorSubtitleProps) {
+ const {projects} = useProjects();
+ const project = projects.find(p => p.id === projectId);
+ return (
+
+ {project && (
+
+
+ {project.slug}
+
+ )}
+ |
+ {environment || t('All Environments')}
+
+ );
+}
diff --git a/static/app/views/detectors/components/forms/fullHeightForm.tsx b/static/app/views/detectors/components/forms/fullHeightForm.tsx
new file mode 100644
index 00000000000000..a214a1e5d9b794
--- /dev/null
+++ b/static/app/views/detectors/components/forms/fullHeightForm.tsx
@@ -0,0 +1,18 @@
+import styled from '@emotion/styled';
+
+import Form from 'sentry/components/forms/form';
+
+/**
+ * Extends the Form component to be full height and have a sticky footer.
+ */
+export const FullHeightForm = styled(Form)`
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 0%;
+
+ & > div:first-child {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ }
+`;
diff --git a/static/app/views/detectors/components/forms/metric.tsx b/static/app/views/detectors/components/forms/metric.tsx
index 65ebb5bc74da22..6f89c9e9cc1d2d 100644
--- a/static/app/views/detectors/components/forms/metric.tsx
+++ b/static/app/views/detectors/components/forms/metric.tsx
@@ -16,7 +16,10 @@ import {IconAdd} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {TagCollection} from 'sentry/types/group';
-import {DataConditionType} from 'sentry/types/workflowEngine/dataConditions';
+import {
+ DataConditionType,
+ DetectorPriorityLevel,
+} from 'sentry/types/workflowEngine/dataConditions';
import {
ALLOWED_EXPLORE_VISUALIZE_AGGREGATES,
FieldKey,
@@ -48,12 +51,8 @@ export function MetricDetectorForm() {
function MonitorKind() {
const options: Array<[MetricDetectorFormData['kind'], string, string]> = [
- [
- 'threshold',
- t('Threshold'),
- t('Absolute-valued thresholds, for non-seasonal data.'),
- ],
- ['change', t('Change'), t('Percentage changes over defined time windows.')],
+ ['static', t('Threshold'), t('Absolute-valued thresholds, for non-seasonal data.')],
+ ['percent', t('Change'), t('Percentage changes over defined time windows.')],
[
'dynamic',
t('Dynamic'),
@@ -63,7 +62,7 @@ function MonitorKind() {
return (
- {kind !== 'dynamic' && }
+ {kind !== 'dynamic' && (
+
+ )}
);
@@ -193,7 +194,7 @@ function DetectSection() {
- {(!kind || kind === 'threshold') && (
+ {(!kind || kind === 'static') && (
{t('An issue will be created when query value is:')}
@@ -224,7 +225,7 @@ function DetectSection() {
)}
- {kind === 'change' && (
+ {kind === 'percent' && (
{t('An issue will be created when query value is:')}
diff --git a/static/app/views/detectors/components/forms/metricFormData.tsx b/static/app/views/detectors/components/forms/metricFormData.tsx
index f508dd7a30ae73..142fd031341e55 100644
--- a/static/app/views/detectors/components/forms/metricFormData.tsx
+++ b/static/app/views/detectors/components/forms/metricFormData.tsx
@@ -8,8 +8,9 @@ import {
DataConditionType,
DetectorPriorityLevel,
} from 'sentry/types/workflowEngine/dataConditions';
-import type {Detector} from 'sentry/types/workflowEngine/detectors';
+import type {Detector, DetectorConfig} from 'sentry/types/workflowEngine/detectors';
import {defined} from 'sentry/utils';
+import {parseFunction} from 'sentry/utils/discover/fields';
import {
AlertRuleSensitivity,
AlertRuleThresholdType,
@@ -61,7 +62,7 @@ export interface MetricDetectorFormData
MetricDetectorDynamicFormData {
aggregate: string;
environment: string;
- kind: 'threshold' | 'change' | 'dynamic';
+ kind: 'static' | 'percent' | 'dynamic';
name: string;
projectId: string;
query: string;
@@ -101,7 +102,7 @@ export const METRIC_DETECTOR_FORM_FIELDS = {
} satisfies Record;
export const DEFAULT_THRESHOLD_METRIC_FORM_DATA = {
- kind: 'threshold',
+ kind: 'static',
// Priority level fields
// Metric detectors only support MEDIUM and HIGH priority levels
@@ -153,16 +154,11 @@ interface NewDataSource {
export interface NewMetricDetector {
conditionGroup: NewConditionGroup;
- // TODO: config types don't exist yet
- config: {
- // TODO: what is the shape of config?
- detection_type: any;
- threshold_period: number;
- };
+ config: DetectorConfig;
dataSource: NewDataSource; // Single data source object (not array)
- detectorType: Detector['type'];
name: string;
projectId: Detector['projectId'];
+ type: Detector['type'];
}
/**
@@ -195,7 +191,7 @@ function createConditions(data: MetricDetectorFormData): NewConditionGroup['cond
}
// Create resolution condition if provided
- if (defined(data.resolveThreshold)) {
+ if (defined(data.resolveThreshold) && data.resolveThreshold !== '') {
// Resolution condition uses opposite comparison type
const resolveConditionType =
data.conditionType === DataConditionType.GREATER
@@ -204,7 +200,7 @@ function createConditions(data: MetricDetectorFormData): NewConditionGroup['cond
conditions.push({
type: resolveConditionType,
- comparison: data.resolveThreshold,
+ comparison: parseFloat(data.resolveThreshold) || 0,
conditionResult: DetectorPriorityLevel.OK,
});
}
@@ -226,7 +222,8 @@ function createDataSource(data: MetricDetectorFormData): NewDataSource {
query: data.query,
// TODO: aggregate doesn't always contain the selected "visualize" value.
aggregate: `${data.aggregate}(${data.visualize})`,
- timeWindow: data.conditionComparisonAgo ? data.conditionComparisonAgo / 60 : 60,
+ // TODO: Add interval to the form
+ timeWindow: 60 * 60,
environment: data.environment ? data.environment : null,
eventTypes,
};
@@ -238,19 +235,154 @@ export function getNewMetricDetectorData(
const conditions = createConditions(data);
const dataSource = createDataSource(data);
+ // Create config based on detection type
+ let config: DetectorConfig;
+ switch (data.kind) {
+ case 'percent':
+ config = {
+ threshold_period: 1,
+ detection_type: 'percent',
+ comparison_delta: data.conditionComparisonAgo || 3600,
+ };
+ break;
+ case 'dynamic':
+ config = {
+ threshold_period: 1,
+ detection_type: 'dynamic',
+ sensitivity: data.sensitivity,
+ };
+ break;
+ case 'static':
+ default:
+ config = {
+ threshold_period: 1,
+ detection_type: 'static',
+ };
+ break;
+ }
+
return {
name: data.name || 'New Monitor',
- detectorType: 'metric_issue',
+ type: 'metric_issue',
projectId: data.projectId,
conditionGroup: {
// TODO: Can this be different values?
logicType: DataConditionGroupLogicType.ANY,
conditions,
},
- config: {
- threshold_period: 1,
- detection_type: 'static',
- },
+ config,
dataSource,
};
}
+
+/**
+ * Convert the detector conditions array to the flattened form data
+ */
+function processDetectorConditions(
+ detector: Detector
+): PrioritizeLevelFormData &
+ Pick {
+ // Get conditions from the condition group
+ const conditions = detector.conditionGroup?.conditions || [];
+ // Sort by priority level, lowest first
+ const sortedConditions = conditions.toSorted((a, b) => {
+ return (a.conditionResult || 0) - (b.conditionResult || 0);
+ });
+
+ // Find the condition with the lowest non-zero priority level
+ const mainCondition = sortedConditions.find(
+ condition => condition.conditionResult !== DetectorPriorityLevel.OK
+ );
+
+ // Find high priority escalation condition
+ const highCondition = conditions.find(
+ condition => condition.conditionResult === DetectorPriorityLevel.HIGH
+ );
+
+ // Find resolution condition
+ const resolveCondition = conditions.find(
+ condition => condition.conditionResult === DetectorPriorityLevel.OK
+ );
+
+ // Determine initial priority level, ensuring it's valid for the form
+ let initialPriorityLevel: DetectorPriorityLevel.MEDIUM | DetectorPriorityLevel.HIGH =
+ DetectorPriorityLevel.MEDIUM;
+
+ if (mainCondition?.conditionResult === DetectorPriorityLevel.HIGH) {
+ initialPriorityLevel = DetectorPriorityLevel.HIGH;
+ } else if (mainCondition?.conditionResult === DetectorPriorityLevel.MEDIUM) {
+ initialPriorityLevel = DetectorPriorityLevel.MEDIUM;
+ }
+
+ // Ensure condition type is valid for the form
+ let conditionType: DataConditionType.GREATER | DataConditionType.LESS =
+ DataConditionType.GREATER;
+ if (
+ mainCondition?.type === DataConditionType.LESS ||
+ mainCondition?.type === DataConditionType.GREATER
+ ) {
+ conditionType = mainCondition.type;
+ }
+
+ return {
+ initialPriorityLevel,
+ conditionValue: mainCondition?.comparison.toString() || '',
+ conditionType,
+ highThreshold: highCondition?.comparison.toString() || '',
+ resolveThreshold: resolveCondition?.comparison.toString() || '',
+ };
+}
+
+/**
+ * Converts a Detector to MetricDetectorFormData for editing
+ */
+export function getMetricDetectorFormData(detector: Detector): MetricDetectorFormData {
+ // Get the first data source (assuming metric detectors have one)
+ const dataSource = detector.dataSources?.[0];
+
+ // Check if this is a snuba query data source
+ const snubaQuery =
+ dataSource?.type === 'snuba_query_subscription'
+ ? dataSource.queryObj?.snubaQuery
+ : undefined;
+
+ // Extract aggregate and visualize from the aggregate string
+ const parsedFunction = snubaQuery?.aggregate
+ ? parseFunction(snubaQuery.aggregate)
+ : null;
+ const aggregate = parsedFunction?.name || 'count';
+ const visualize = parsedFunction?.arguments[0] || 'transaction.duration';
+
+ // Process conditions using the extracted function
+ const conditionData = processDetectorConditions(detector);
+
+ return {
+ // Core detector fields
+ name: detector.name,
+ projectId: detector.projectId,
+ environment: snubaQuery?.environment || '',
+ query: snubaQuery?.query || '',
+ aggregate,
+ visualize,
+
+ // Priority level and condition fields from processed conditions
+ ...conditionData,
+ kind: detector.config.detection_type || 'static',
+
+ // Condition fields - get comparison delta from detector config (already in seconds)
+ conditionComparisonAgo:
+ (detector.config?.detection_type === 'percent'
+ ? detector.config.comparison_delta
+ : null) || 3600,
+
+ // Dynamic fields - extract from config for dynamic detectors
+ sensitivity:
+ detector.config?.detection_type === 'dynamic'
+ ? detector.config.sensitivity || AlertRuleSensitivity.LOW
+ : AlertRuleSensitivity.LOW,
+ thresholdType:
+ detector.config?.detection_type === 'dynamic'
+ ? (detector.config as any).threshold_type || AlertRuleThresholdType.ABOVE
+ : AlertRuleThresholdType.ABOVE,
+ };
+}
diff --git a/static/app/views/detectors/edit.tsx b/static/app/views/detectors/edit.tsx
index 92e94e20ead0e4..6c667aee37133e 100644
--- a/static/app/views/detectors/edit.tsx
+++ b/static/app/views/detectors/edit.tsx
@@ -1,56 +1,152 @@
-/* eslint-disable no-alert */
-import {Fragment, useState} from 'react';
+import {useCallback, useMemo} from 'react';
+import styled from '@emotion/styled';
+import Breadcrumbs from 'sentry/components/breadcrumbs';
+import {Flex} from 'sentry/components/container/flex';
import {Button} from 'sentry/components/core/button';
+import {LinkButton} from 'sentry/components/core/button/linkButton';
+import type {OnSubmitCallback} from 'sentry/components/forms/types';
+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 {ActionsProvider} from 'sentry/components/workflowEngine/layout/actions';
-import {BreadcrumbsProvider} from 'sentry/components/workflowEngine/layout/breadcrumbs';
-import EditLayout from 'sentry/components/workflowEngine/layout/edit';
+import {StickyFooter} from 'sentry/components/workflowEngine/ui/footer';
import {useWorkflowEngineFeatureGate} from 'sentry/components/workflowEngine/useWorkflowEngineFeatureGate';
import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {useNavigate} from 'sentry/utils/useNavigate';
import useOrganization from 'sentry/utils/useOrganization';
+import {useParams} from 'sentry/utils/useParams';
+import {DetectorSubtitle} from 'sentry/views/detectors/components/detectorSubtitle';
+import {EditableDetectorName} from 'sentry/views/detectors/components/forms/editableDetectorName';
+import {FullHeightForm} from 'sentry/views/detectors/components/forms/fullHeightForm';
import {MetricDetectorForm} from 'sentry/views/detectors/components/forms/metric';
-import {makeMonitorBasePathname} from 'sentry/views/detectors/pathnames';
+import type {MetricDetectorFormData} from 'sentry/views/detectors/components/forms/metricFormData';
+import {
+ getMetricDetectorFormData,
+ getNewMetricDetectorData,
+ useMetricDetectorFormField,
+} from 'sentry/views/detectors/components/forms/metricFormData';
+import {useDetectorQuery, useUpdateDetector} from 'sentry/views/detectors/hooks';
+import {
+ makeMonitorBasePathname,
+ makeMonitorDetailsPathname,
+} from 'sentry/views/detectors/pathnames';
+
+function DetectorBreadcrumbs({detectorId}: {detectorId: string}) {
+ const title = useMetricDetectorFormField('name');
+ const organization = useOrganization();
+ return (
+
+ );
+}
+
+function DetectorDocumentTitle() {
+ const title = useMetricDetectorFormField('name');
+ return ;
+}
export default function DetectorEdit() {
const organization = useOrganization();
+ const navigate = useNavigate();
+ const params = useParams<{detectorId: string}>();
+
useWorkflowEngineFeatureGate({redirect: true});
- const [title, setTitle] = useState(t('Edit Monitor'));
- return (
-
-
- }>
-
-
-
-
-
-
+ const {
+ data: detector,
+ isPending,
+ isError,
+ refetch,
+ } = useDetectorQuery(params.detectorId);
+
+ const {mutateAsync: updateDetector} = useUpdateDetector();
+
+ const handleSubmit = useCallback(
+ async (data, _, __, ___, formModel) => {
+ if (!detector) {
+ return;
+ }
+
+ const isValid = formModel.validateForm();
+ if (!isValid) {
+ return;
+ }
+
+ const updatedData = {
+ detectorId: detector.id,
+ ...getNewMetricDetectorData(data as MetricDetectorFormData),
+ };
+
+ const updatedDetector = await updateDetector(updatedData);
+ navigate(makeMonitorDetailsPathname(organization.slug, updatedDetector.id));
+ },
+ [updateDetector, navigate, organization.slug, detector]
);
-}
-function Actions() {
- const disable = () => {
- window.alert('disable');
- };
- const del = () => {
- window.alert('delete');
- };
- const save = () => {
- window.alert('save');
- };
+ const initialData = useMemo((): MetricDetectorFormData | null => {
+ if (!detector) {
+ return null;
+ }
+
+ return getMetricDetectorFormData(detector);
+ }, [detector]);
+
+ if (isPending && !initialData) {
+ return ;
+ }
+
+ if (isError || !detector || !initialData) {
+ return ;
+ }
+
return (
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('Cancel')}
+
+
+
+
+
);
}
+
+const StyledLayoutHeader = styled(Layout.Header)`
+ background-color: ${p => p.theme.background};
+`;
diff --git a/static/app/views/detectors/hooks/index.ts b/static/app/views/detectors/hooks/index.ts
index cd11ab756f9822..38686fd405da9b 100644
--- a/static/app/views/detectors/hooks/index.ts
+++ b/static/app/views/detectors/hooks/index.ts
@@ -92,6 +92,31 @@ export function useCreateDetector() {
});
}
+export function useUpdateDetector() {
+ const org = useOrganization();
+ const api = useApi({persistInFlight: true});
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: data =>
+ api.requestPromise(`/organizations/${org.slug}/detectors/${data.detectorId}/`, {
+ method: 'PUT',
+ data,
+ }),
+ onSuccess: (_, data) => {
+ queryClient.invalidateQueries({
+ queryKey: [`/organizations/${org.slug}/detectors/`],
+ });
+ queryClient.invalidateQueries({
+ queryKey: [`/organizations/${org.slug}/detectors/${data.detectorId}/`],
+ });
+ },
+ onError: _ => {
+ AlertStore.addAlert({type: 'error', message: t('Unable to update monitor')});
+ },
+ });
+}
+
const makeDetectorDetailsQueryKey = ({
orgSlug,
detectorId,
diff --git a/static/app/views/detectors/new-settings.tsx b/static/app/views/detectors/new-settings.tsx
index 09df5e7ec91ef4..92e3a150a75e7d 100644
--- a/static/app/views/detectors/new-settings.tsx
+++ b/static/app/views/detectors/new-settings.tsx
@@ -1,15 +1,13 @@
-import {useCallback} from 'react';
+import {useCallback, useLayoutEffect, useMemo, useRef} from 'react';
import styled from '@emotion/styled';
-import {Breadcrumbs} from 'sentry/components/breadcrumbs';
+import Breadcrumbs from 'sentry/components/breadcrumbs';
import {Flex} from 'sentry/components/container/flex';
import {Button} from 'sentry/components/core/button';
import {LinkButton} from 'sentry/components/core/button/linkButton';
-import Form from 'sentry/components/forms/form';
import type {OnSubmitCallback} from 'sentry/components/forms/types';
import * as Layout from 'sentry/components/layouts/thirds';
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
-import {useFormField} from 'sentry/components/workflowEngine/form/useFormField';
import {
StickyFooter,
StickyFooterLabel,
@@ -20,12 +18,15 @@ import {space} from 'sentry/styles/space';
import {useLocation} from 'sentry/utils/useLocation';
import {useNavigate} from 'sentry/utils/useNavigate';
import useOrganization from 'sentry/utils/useOrganization';
+import {DetectorSubtitle} from 'sentry/views/detectors/components/detectorSubtitle';
import {EditableDetectorName} from 'sentry/views/detectors/components/forms/editableDetectorName';
+import {FullHeightForm} from 'sentry/views/detectors/components/forms/fullHeightForm';
import {MetricDetectorForm} from 'sentry/views/detectors/components/forms/metric';
import type {MetricDetectorFormData} from 'sentry/views/detectors/components/forms/metricFormData';
import {
DEFAULT_THRESHOLD_METRIC_FORM_DATA,
getNewMetricDetectorData,
+ useMetricDetectorFormField,
} from 'sentry/views/detectors/components/forms/metricFormData';
import {useCreateDetector} from 'sentry/views/detectors/hooks';
import {
@@ -33,8 +34,17 @@ import {
makeMonitorDetailsPathname,
} from 'sentry/views/detectors/pathnames';
-function NewDetectorBreadcrumbs() {
- const title = useFormField('title');
+function DetectorDocumentTitle() {
+ const title = useMetricDetectorFormField('name');
+ return (
+
+ );
+}
+
+function DetectorBreadcrumbs() {
+ const title = useMetricDetectorFormField('name');
const organization = useOrganization();
return (
('title');
- return ;
-}
-
export default function DetectorNewSettings() {
- const location = useLocation();
const organization = useOrganization();
- useWorkflowEngineFeatureGate({redirect: true});
const navigate = useNavigate();
+ const location = useLocation();
+ // We'll likely use more query params on this page to open drawers, validate once
+ const validatedRequiredQueryParams = useRef(false);
+
+ useWorkflowEngineFeatureGate({redirect: true});
+
+ // Kick user back to the previous step if they don't have a project or detectorType
+ useLayoutEffect(() => {
+ const {project, detectorType} = location.query;
+ if (validatedRequiredQueryParams.current) {
+ return;
+ }
+
+ if (!project || !detectorType) {
+ navigate(`${makeMonitorBasePathname(organization.slug)}new/`);
+ }
+ validatedRequiredQueryParams.current = true;
+ }, [location.query, navigate, organization.slug]);
const {mutateAsync: createDetector} = useCreateDetector();
@@ -74,26 +95,33 @@ export default function DetectorNewSettings() {
[createDetector, navigate, organization.slug]
);
+ // Defaults and data from the previous step passed in as query params
+ const initialData = useMemo(
+ (): MetricDetectorFormData => ({
+ ...DEFAULT_THRESHOLD_METRIC_FORM_DATA,
+ projectId: (location.query.project as string) ?? '',
+ environment: (location.query.environment as string | undefined) || '',
+ name: (location.query.name as string | undefined) || '',
+ }),
+ [location.query]
+ );
+
return (
-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
@@ -123,15 +151,3 @@ export default function DetectorNewSettings() {
const StyledLayoutHeader = styled(Layout.Header)`
background-color: ${p => p.theme.background};
`;
-
-const FullHeightForm = styled(Form)`
- display: flex;
- flex-direction: column;
- flex: 1 1 0%;
-
- & > div:first-child {
- display: flex;
- flex-direction: column;
- flex: 1;
- }
-`;
diff --git a/tests/js/fixtures/detectors.ts b/tests/js/fixtures/detectors.ts
index a254fdd7ba04e5..d4ee1b71fd82c0 100644
--- a/tests/js/fixtures/detectors.ts
+++ b/tests/js/fixtures/detectors.ts
@@ -13,7 +13,10 @@ export function DetectorFixture(params: Partial = {}): Detector {
dateUpdated: '2025-01-01T00:00:00.000Z',
lastTriggered: '2025-01-01T00:00:00.000Z',
workflowIds: [],
- config: {},
+ config: {
+ detection_type: 'static',
+ threshold_period: 1,
+ },
type: 'metric_issue',
disabled: false,
conditionGroup: params.conditionGroup ?? DataConditionGroupFixture(),