diff --git a/static/app/components/workflowEngine/form/control/priorityControl.tsx b/static/app/components/workflowEngine/form/control/priorityControl.tsx
index a50f600c2a5aad..4004f0af6e4106 100644
--- a/static/app/components/workflowEngine/form/control/priorityControl.tsx
+++ b/static/app/components/workflowEngine/form/control/priorityControl.tsx
@@ -76,6 +76,7 @@ export default function PriorityControl({
flexibleControlStateSize
size="sm"
suffix="s"
+ placeholder="0"
// empty string required to keep this as a controlled input
value={thresholds[PriorityLevel.MEDIUM] ?? ''}
onChange={threshold => setMediumThreshold(Number(threshold))}
@@ -96,6 +97,7 @@ export default function PriorityControl({
flexibleControlStateSize
size="sm"
suffix="s"
+ placeholder="0"
// empty string required to keep this as a controlled input
value={thresholds[PriorityLevel.HIGH] ?? ''}
onChange={threshold => setHighThreshold(Number(threshold))}
diff --git a/static/app/components/workflowEngine/gridCell/actionCell.tsx b/static/app/components/workflowEngine/gridCell/actionCell.tsx
index 5ef0e281397436..bfcae7178bc4dd 100644
--- a/static/app/components/workflowEngine/gridCell/actionCell.tsx
+++ b/static/app/components/workflowEngine/gridCell/actionCell.tsx
@@ -31,12 +31,10 @@ export function ActionCell({actions, disabled}: ActionCellProps) {
);
}
-
const actionsList = actions
.map(action => ActionMetadata[action]?.name)
.filter(x => x)
.join(', ');
-
return (
diff --git a/static/app/components/workflowEngine/gridCell/connectionCell.tsx b/static/app/components/workflowEngine/gridCell/connectionCell.tsx
index c7f06722d7c3e7..4edf617482674e 100644
--- a/static/app/components/workflowEngine/gridCell/connectionCell.tsx
+++ b/static/app/components/workflowEngine/gridCell/connectionCell.tsx
@@ -32,7 +32,7 @@ const links: Record<
};
export function ConnectionCell({
- ids: items,
+ ids: items = [],
type,
disabled = false,
className,
diff --git a/static/app/components/workflowEngine/gridCell/index.stories.tsx b/static/app/components/workflowEngine/gridCell/index.stories.tsx
index a55f46da8bb7b1..4b84ceae394e7f 100644
--- a/static/app/components/workflowEngine/gridCell/index.stories.tsx
+++ b/static/app/components/workflowEngine/gridCell/index.stories.tsx
@@ -50,7 +50,7 @@ export default storyBook('Grid Cell Components', story => {
},
openIssues: 3,
creator: '1',
- type: 'trace',
+ type: 'uptime',
},
{
title: {
@@ -95,7 +95,7 @@ export default storyBook('Grid Cell Components', story => {
},
actions: [ActionType.SLACK, ActionType.DISCORD, ActionType.EMAIL],
creator: 'sentry',
- type: 'errors',
+ type: 'uptime',
timeAgo: null,
linkedItems: {
ids: [],
diff --git a/static/app/components/workflowEngine/gridCell/timeAgoCell.tsx b/static/app/components/workflowEngine/gridCell/timeAgoCell.tsx
index 89b28ad23bead6..b936f25fec1831 100644
--- a/static/app/components/workflowEngine/gridCell/timeAgoCell.tsx
+++ b/static/app/components/workflowEngine/gridCell/timeAgoCell.tsx
@@ -2,7 +2,7 @@ import TimeSince from 'sentry/components/timeSince';
import {EmptyCell} from 'sentry/components/workflowEngine/gridCell/emptyCell';
type TimeAgoCellProps = {
- date?: Date;
+ date?: string | Date;
};
export function TimeAgoCell({date}: TimeAgoCellProps) {
diff --git a/static/app/components/workflowEngine/layout/actions.tsx b/static/app/components/workflowEngine/layout/actions.tsx
index 822de1f4e48d7e..16b422b3fd32d0 100644
--- a/static/app/components/workflowEngine/layout/actions.tsx
+++ b/static/app/components/workflowEngine/layout/actions.tsx
@@ -1,7 +1,8 @@
import {createContext, useContext} from 'react';
-import {ButtonBar} from 'sentry/components/core/button/buttonBar';
+import {Flex} from 'sentry/components/container/flex';
import {HeaderActions} from 'sentry/components/layouts/thirds';
+import {space} from 'sentry/styles/space';
const ActionContext = createContext(undefined);
@@ -26,9 +27,7 @@ export function ActionsFromContext() {
}
return (
-
- {actions}
-
+ {actions}
);
}
diff --git a/static/app/components/workflowEngine/layout/detail.tsx b/static/app/components/workflowEngine/layout/detail.tsx
index 89dbac347111f1..f6e2f45d4b8cb4 100644
--- a/static/app/components/workflowEngine/layout/detail.tsx
+++ b/static/app/components/workflowEngine/layout/detail.tsx
@@ -25,7 +25,7 @@ function DetailLayout({children, project}: WorkflowEngineDetailLayoutProps) {
const title = useDocumentTitle();
return (
-
+
{title}
diff --git a/static/app/components/workflowEngine/layout/edit.tsx b/static/app/components/workflowEngine/layout/edit.tsx
index 4916ea35e15094..f05e7f506ee383 100644
--- a/static/app/components/workflowEngine/layout/edit.tsx
+++ b/static/app/components/workflowEngine/layout/edit.tsx
@@ -24,7 +24,7 @@ function EditLayout({children, onTitleChange}: WorkflowEngineEditLayoutProps) {
const title = useDocumentTitle();
return (
-
+
@@ -37,16 +37,12 @@ function EditLayout({children, onTitleChange}: WorkflowEngineEditLayoutProps) {
-
+
{children}
);
}
-const StyledHeader = styled(Layout.Header)`
- background: ${p => p.theme.background};
-`;
-
const Body = styled('div')`
display: flex;
flex-direction: column;
diff --git a/static/app/components/workflowEngine/layout/list.tsx b/static/app/components/workflowEngine/layout/list.tsx
index 2fd1c8d2336122..3f76bfb2a9e2ea 100644
--- a/static/app/components/workflowEngine/layout/list.tsx
+++ b/static/app/components/workflowEngine/layout/list.tsx
@@ -17,7 +17,7 @@ function WorkflowEngineListLayout({children}: WorkflowEngineListLayoutProps) {
const title = useDocumentTitle();
return (
-
+
{title}
diff --git a/static/app/components/workflowEngine/ui/container.tsx b/static/app/components/workflowEngine/ui/container.tsx
index e148b7baab8705..26d1a329d36f1b 100644
--- a/static/app/components/workflowEngine/ui/container.tsx
+++ b/static/app/components/workflowEngine/ui/container.tsx
@@ -7,12 +7,13 @@ export const Container = styled('div')`
flex-direction: column;
gap: ${space(2)};
justify-content: flex-start;
- background-color: ${p => p.theme.backgroundSecondary};
+ background-color: ${p => p.theme.background};
border: 1px solid ${p => p.theme.translucentBorder};
border-radius: ${p => p.theme.borderRadius};
padding: ${space(1.5)};
@media (max-width: ${p => p.theme.breakpoints.large}) {
- width: fit-content;
+ min-width: fit-content;
+ flex: 1;
}
`;
diff --git a/static/app/components/workflowEngine/ui/footer.tsx b/static/app/components/workflowEngine/ui/footer.tsx
index 7e55d27edfcf80..f2bc96e3a31c26 100644
--- a/static/app/components/workflowEngine/ui/footer.tsx
+++ b/static/app/components/workflowEngine/ui/footer.tsx
@@ -5,6 +5,7 @@ import {space} from 'sentry/styles/space';
export const StickyFooter = styled('div')`
position: sticky;
margin-top: auto;
+ margin-bottom: -56px;
bottom: 0;
right: 0;
width: 100%;
diff --git a/static/app/components/workflowEngine/ui/section.tsx b/static/app/components/workflowEngine/ui/section.tsx
index f8f3c851b2ea2a..92e67a5df4e9fe 100644
--- a/static/app/components/workflowEngine/ui/section.tsx
+++ b/static/app/components/workflowEngine/ui/section.tsx
@@ -6,18 +6,28 @@ import {space} from 'sentry/styles/space';
type SectionProps = {
children: React.ReactNode;
title: string;
+ description?: string;
};
-export default function Section({children, title}: SectionProps) {
+export default function Section({children, title, description}: SectionProps) {
return (
{title}
+ {description && {description}}
{children}
);
}
-const SectionHeading = styled('h4')`
+export const SectionHeading = styled('h4')`
+ font-size: ${p => p.theme.fontSizeLarge};
+ font-weight: ${p => p.theme.fontWeightBold};
+ margin: 0;
+`;
+
+export const SectionDescription = styled('p')`
font-size: ${p => p.theme.fontSizeMedium};
+ font-weight: ${p => p.theme.fontWeightNormal};
+ color: ${p => p.theme.subText};
margin: 0;
`;
diff --git a/static/app/plugins/components/pluginIcon.tsx b/static/app/plugins/components/pluginIcon.tsx
index b2344e9167c85d..fdd8bd77d6d40d 100644
--- a/static/app/plugins/components/pluginIcon.tsx
+++ b/static/app/plugins/components/pluginIcon.tsx
@@ -69,7 +69,7 @@ const PLUGIN_ICONS = {
} satisfies Record;
export interface PluginIconProps extends React.RefAttributes {
- pluginId: string | keyof typeof PLUGIN_ICONS;
+ pluginId: keyof typeof PLUGIN_ICONS | (string & {});
/**
* @default 20
*/
diff --git a/static/app/types/workflowEngine/automations.tsx b/static/app/types/workflowEngine/automations.tsx
index 728801f7bb6cd1..f5f6fc67063258 100644
--- a/static/app/types/workflowEngine/automations.tsx
+++ b/static/app/types/workflowEngine/automations.tsx
@@ -1,6 +1,6 @@
import type {DataConditionGroup} from 'sentry/types/workflowEngine/dataConditions';
-interface NewAutomation {
+export interface NewAutomation {
actionFilters: DataConditionGroup[];
detectorIds: string[];
name: string;
@@ -10,5 +10,5 @@ interface NewAutomation {
export interface Automation extends Readonly {
readonly id: string;
- readonly lastTriggered: Date;
+ readonly lastTriggered: string;
}
diff --git a/static/app/types/workflowEngine/dataConditions.tsx b/static/app/types/workflowEngine/dataConditions.tsx
index bfd62ed54d9381..f34aee4a376c10 100644
--- a/static/app/types/workflowEngine/dataConditions.tsx
+++ b/static/app/types/workflowEngine/dataConditions.tsx
@@ -73,6 +73,10 @@ export interface NewDataCondition {
condition_result?: any;
}
+export interface DataCondition extends Readonly {
+ readonly id: string;
+}
+
export interface DataConditionGroup {
conditions: NewDataCondition[];
id: string;
diff --git a/static/app/types/workflowEngine/detectors.tsx b/static/app/types/workflowEngine/detectors.tsx
index 15de579d255d6e..334310a240efd8 100644
--- a/static/app/types/workflowEngine/detectors.tsx
+++ b/static/app/types/workflowEngine/detectors.tsx
@@ -4,11 +4,12 @@ import type {
} from 'sentry/types/workflowEngine/dataConditions';
export type DetectorType =
- | 'metric'
+ | 'crons'
| 'errors'
+ | 'metric'
| 'performance'
- | 'trace'
| 'replay'
+ | 'trace'
| 'uptime';
interface NewDetector {
@@ -24,8 +25,8 @@ interface NewDetector {
export interface Detector extends Readonly {
readonly createdBy: string;
- readonly dateCreated: Date;
- readonly dateUpdated: Date;
+ readonly dateCreated: string;
+ readonly dateUpdated: string;
readonly id: string;
- readonly lastTriggered: Date;
+ readonly lastTriggered: string;
}
diff --git a/static/app/utils/useParams.tsx b/static/app/utils/useParams.tsx
index ac0add99d69a47..a6bc199570d237 100644
--- a/static/app/utils/useParams.tsx
+++ b/static/app/utils/useParams.tsx
@@ -24,6 +24,7 @@ type ParamKeys =
| 'groupId'
| 'id'
| 'installationId'
+ | 'detectorId'
| 'integrationSlug'
| 'issueId'
| 'memberId'
diff --git a/static/app/views/automations/components/automationListRow.tsx b/static/app/views/automations/components/automationListRow.tsx
index dbbb2cb370c94b..07c4a90bfd01d9 100644
--- a/static/app/views/automations/components/automationListRow.tsx
+++ b/static/app/views/automations/components/automationListRow.tsx
@@ -4,14 +4,19 @@ import styled from '@emotion/styled';
import {Flex} from 'sentry/components/container/flex';
import {Checkbox} from 'sentry/components/core/checkbox';
import InteractionStateLayer from 'sentry/components/interactionStateLayer';
+import {ProjectList} from 'sentry/components/projectList';
import {ActionCell} from 'sentry/components/workflowEngine/gridCell/actionCell';
import AutomationTitleCell from 'sentry/components/workflowEngine/gridCell/automationTitleCell';
import {ConnectionCell} from 'sentry/components/workflowEngine/gridCell/connectionCell';
import {TimeAgoCell} from 'sentry/components/workflowEngine/gridCell/timeAgoCell';
+import ProjectsStore from 'sentry/stores/projectsStore';
import {space} from 'sentry/styles/space';
import type {Automation} from 'sentry/types/workflowEngine/automations';
import useOrganization from 'sentry/utils/useOrganization';
-import {useAutomationActions} from 'sentry/views/automations/hooks/utils';
+import {
+ useAutomationActions,
+ useAutomationProjectIds,
+} from 'sentry/views/automations/hooks/utils';
import {makeAutomationDetailsPathname} from 'sentry/views/automations/pathnames';
type AutomationListRowProps = {
@@ -28,6 +33,10 @@ export function AutomationListRow({
const organization = useOrganization();
const actions = useAutomationActions(automation);
const {id, name, disabled, lastTriggered, detectorIds = []} = automation;
+ const projectIds = useAutomationProjectIds(automation);
+ const projectSlugs = projectIds.map(
+ projectId => ProjectsStore.getById(projectId)?.slug
+ ) as string[];
return (
@@ -51,6 +60,9 @@ export function AutomationListRow({
+
+
+
diff --git a/static/app/views/automations/components/automationListTable.tsx b/static/app/views/automations/components/automationListTable.tsx
index e2f83265b06528..24445a88ceb78a 100644
--- a/static/app/views/automations/components/automationListTable.tsx
+++ b/static/app/views/automations/components/automationListTable.tsx
@@ -43,6 +43,10 @@ function AutomationListTable() {
{t('Actions')}
+
+
+ {t('Projects')}
+
{t('Monitors')}
@@ -50,7 +54,6 @@ function AutomationListTable() {
{isLoading ? : null}
-
{automations.map(automation => (
+
{t('Last Triggered')}
@@ -78,7 +78,7 @@ function Details() {
-
+
);
}
diff --git a/static/app/views/automations/hooks/index.tsx b/static/app/views/automations/hooks/index.tsx
index 5db656790c6d56..a27818e8ab17ca 100644
--- a/static/app/views/automations/hooks/index.tsx
+++ b/static/app/views/automations/hooks/index.tsx
@@ -14,3 +14,8 @@ export function useAutomationsQuery(_options: UseAutomationsQueryOptions = {}) {
retry: false,
});
}
+
+export const makeAutomationQueryKey = (
+ orgSlug: string,
+ automationId = ''
+): [url: string] => [`/organizations/${orgSlug}/workflows/${automationId}/`];
diff --git a/static/app/views/automations/hooks/utils.tsx b/static/app/views/automations/hooks/utils.tsx
index 99a7cb871c6706..ed4818b5c4acfa 100644
--- a/static/app/views/automations/hooks/utils.tsx
+++ b/static/app/views/automations/hooks/utils.tsx
@@ -2,6 +2,7 @@ import {useState} from 'react';
import type {ActionType} from 'sentry/types/workflowEngine/actions';
import type {Automation} from 'sentry/types/workflowEngine/automations';
+import {useDetectorQueriesByIds} from 'sentry/views/detectors/hooks';
export function useAutomationActions(automation: Automation): ActionType[] {
return [
@@ -47,3 +48,9 @@ export function useConnectedIds({storageKey, initialIds = []}: UseConnectedIdsPr
}
export const NEW_AUTOMATION_CONNECTED_IDS_KEY = 'new-automation-connected-ids';
+export function useAutomationProjectIds(automation: Automation): string[] {
+ const queries = useDetectorQueriesByIds(automation.detectorIds);
+ return [
+ ...new Set(queries.map(query => query.data?.projectId).filter(x => x)),
+ ] as string[];
+}
diff --git a/static/app/views/automations/list.tsx b/static/app/views/automations/list.tsx
index ef1af8bc5d1e1c..9bd7dae88e8586 100644
--- a/static/app/views/automations/list.tsx
+++ b/static/app/views/automations/list.tsx
@@ -36,7 +36,7 @@ export default function AutomationsList() {
function TableHeader() {
return (
-
+
diff --git a/static/app/views/detectors/components/detectorListRow.tsx b/static/app/views/detectors/components/detectorListRow.tsx
index 0c6c0d8a2f9a24..8965b0ccade67e 100644
--- a/static/app/views/detectors/components/detectorListRow.tsx
+++ b/static/app/views/detectors/components/detectorListRow.tsx
@@ -23,7 +23,7 @@ interface DetectorListRowProps {
}
export function DetectorListRow({
- detector: {workflowIds, id, name, disabled, projectId},
+ detector: {workflowIds, createdBy, id, projectId, name, disabled, type},
handleSelect,
selected,
}: DetectorListRowProps) {
@@ -51,7 +51,7 @@ export function DetectorListRow({
-
+
-
-
+
+
diff --git a/static/app/views/detectors/components/detectorListTable.tsx b/static/app/views/detectors/components/detectorListTable.tsx
index 36a06e470ea4cf..a7698c37ee6fb3 100644
--- a/static/app/views/detectors/components/detectorListTable.tsx
+++ b/static/app/views/detectors/components/detectorListTable.tsx
@@ -40,7 +40,7 @@ function DetectorListTable({detectors}: DetectorListTableProps) {
{t('Type')}
-
+
{t('Last Issue')}
@@ -86,9 +86,10 @@ const StyledPanelHeader = styled(PanelHeader)`
min-height: 40px;
align-items: center;
display: grid;
+ text-transform: none;
.type,
- .owner,
+ .creator,
.last-issue,
.connected-automations {
display: none;
@@ -113,7 +114,7 @@ const StyledPanelHeader = styled(PanelHeader)`
@media (min-width: ${p => p.theme.breakpoints.medium}) {
grid-template-columns: 3fr 0.8fr 1.5fr 0.8fr;
- .owner {
+ .creator {
display: flex;
}
}
diff --git a/static/app/views/detectors/components/detectorTypeForm.tsx b/static/app/views/detectors/components/detectorTypeForm.tsx
index fc9ab07b6428d0..6ace886117d8c4 100644
--- a/static/app/views/detectors/components/detectorTypeForm.tsx
+++ b/static/app/views/detectors/components/detectorTypeForm.tsx
@@ -9,7 +9,6 @@ import SentryProjectSelectorField from 'sentry/components/forms/fields/sentryPro
import Form from 'sentry/components/forms/form';
import FormModel from 'sentry/components/forms/model';
import {useDocumentTitle} from 'sentry/components/sentryDocumentTitle';
-import {DebugForm} from 'sentry/components/workflowEngine/form/debug';
import {useFormField} from 'sentry/components/workflowEngine/form/hooks';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
@@ -74,7 +73,6 @@ export function DetectorTypeForm() {
-
diff --git a/static/app/views/detectors/components/forms/metric.tsx b/static/app/views/detectors/components/forms/metric.tsx
new file mode 100644
index 00000000000000..5e64a18bf71dbe
--- /dev/null
+++ b/static/app/views/detectors/components/forms/metric.tsx
@@ -0,0 +1,503 @@
+import {useMemo} from 'react';
+import styled from '@emotion/styled';
+
+import {Flex} from 'sentry/components/container/flex';
+import {Button} from 'sentry/components/core/button';
+import NumberField from 'sentry/components/forms/fields/numberField';
+import SegmentedRadioField from 'sentry/components/forms/fields/segmentedRadioField';
+import SelectField from 'sentry/components/forms/fields/selectField';
+import SentryMemberTeamSelectorField from 'sentry/components/forms/fields/sentryMemberTeamSelectorField';
+import Form from 'sentry/components/forms/form';
+import type FormModel from 'sentry/components/forms/model';
+import Spinner from 'sentry/components/forms/spinner';
+import {SearchQueryBuilder} from 'sentry/components/searchQueryBuilder';
+import type {FilterKeySection} from 'sentry/components/searchQueryBuilder/types';
+import PriorityControl from 'sentry/components/workflowEngine/form/control/priorityControl';
+import {useFormField} from 'sentry/components/workflowEngine/form/hooks';
+import {Container} from 'sentry/components/workflowEngine/ui/container';
+import Section from 'sentry/components/workflowEngine/ui/section';
+import {IconAdd} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {TagCollection} from 'sentry/types/group';
+import {
+ ALLOWED_EXPLORE_VISUALIZE_AGGREGATES,
+ FieldKey,
+ FieldKind,
+ MobileVital,
+ WebVital,
+} from 'sentry/utils/fields';
+import {
+ AlertRuleSensitivity,
+ AlertRuleThresholdType,
+} from 'sentry/views/alerts/rules/metric/types';
+
+type MetricDetectorKind = 'threshold' | 'change' | 'dynamic';
+
+export function MetricDetectorForm({model}: {model: FormModel}) {
+ return (
+
+ );
+}
+
+function ResolveSection() {
+ const kind = useFormField('kind')!;
+
+ return (
+
+
+ {kind !== 'dynamic' && (
+
+ )}
+
+
+ );
+}
+
+function AutomateSection() {
+ return (
+
+
+ }
+ >
+ Connect Automations
+
+
+
+ );
+}
+
+function AssignSection() {
+ return (
+
+
+
+ );
+}
+
+function PrioritizeSection() {
+ const kind = useFormField('kind')!;
+ return (
+
+
+ {kind !== 'dynamic' && }
+
+
+ );
+}
+
+function DetectSection() {
+ const kind = useFormField('kind')!;
+ const aggregateOptions: Array<[string, string]> = useMemo(() => {
+ return ALLOWED_EXPLORE_VISUALIZE_AGGREGATES.map(aggregate => {
+ return [aggregate, aggregate];
+ });
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(!kind || kind === 'threshold') && (
+
+
+ {t('An issue will be created when query value exceeds:')}
+
+
+
+ )}
+ {kind === 'change' && (
+
+ {t('An issue will be created when query value is:')}
+
+
+ {t('percent')}
+
+ {t('than the previous')}
+
+
+
+ )}
+ {kind === 'dynamic' && (
+
+
+
+
+ )}
+
+
+
+ );
+}
+
+function OwnerField() {
+ return (
+
+ );
+}
+
+const FormStack = styled(Flex)`
+ max-width: ${p => p.theme.breakpoints.xlarge};
+ flex-direction: column;
+ gap: ${space(4)};
+ padding: ${space(4)};
+`;
+
+const FirstRow = styled('div')`
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ gap: ${space(1)};
+ border-bottom: 1px solid ${p => p.theme.border};
+`;
+
+function DetectColumn(props: React.ComponentProps) {
+ return ;
+}
+
+const StyledSelectField = styled(SelectField)`
+ width: 180px;
+ padding: 0;
+ margin: 0;
+
+ > div {
+ padding-left: 0;
+ }
+`;
+
+const StyledMemberTeamSelectorField = styled(SentryMemberTeamSelectorField)`
+ padding-left: 0;
+`;
+
+const VisualizeField = styled(SelectField)`
+ flex: 2;
+ padding-left: 0;
+ padding-right: 0;
+ margin-left: 0;
+ border-bottom: none;
+`;
+
+const ChartContainer = styled('div')`
+ background: ${p => p.theme.background};
+ width: 100%;
+ border-bottom: 1px solid ${p => p.theme.border};
+ padding: 24px 32px 16px 32px;
+`;
+
+const AggregateField = styled(SelectField)`
+ width: 120px;
+ margin-top: auto;
+ padding-top: 0;
+ padding-left: 0;
+ padding-right: 0;
+ border-bottom: none;
+
+ > div {
+ padding-left: 0;
+ }
+`;
+
+const DirectionField = styled(SelectField)`
+ width: 16ch;
+ padding: 0;
+ margin: 0;
+ border-bottom: none;
+
+ > div {
+ padding-left: 0;
+ }
+`;
+
+const MonitorKindField = styled(SegmentedRadioField)`
+ padding-left: 0;
+ padding-block: ${space(1)};
+ border-bottom: none;
+ max-width: 840px;
+
+ > div {
+ padding: 0;
+ }
+`;
+const ThresholdField = styled(NumberField)`
+ padding: 0;
+ margin: 0;
+ border: none;
+
+ > div {
+ padding: 0;
+ width: 10ch;
+ }
+`;
+
+const ChangePercentField = styled(NumberField)`
+ padding: 0;
+ margin: 0;
+ border: none;
+
+ > div {
+ padding: 0;
+ max-width: 10ch;
+ }
+`;
+
+const MutedText = styled('p')`
+ color: ${p => p.theme.text};
+ padding-top: ${space(1)};
+ margin-bottom: ${space(1)};
+ border-top: 1px solid ${p => p.theme.border};
+`;
+
+function FilterField() {
+ return (
+
+ Filter
+
+
+ );
+}
+
+const getTagValues = (): Promise => {
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve(['foo', 'bar', 'baz']);
+ }, 500);
+ });
+};
+
+// TODO: replace hardcoded tags with data from API
+const FILTER_KEYS: TagCollection = {
+ [FieldKey.ASSIGNED]: {
+ key: FieldKey.ASSIGNED,
+ name: 'Assigned To',
+ kind: FieldKind.FIELD,
+ predefined: true,
+ values: [
+ {
+ title: 'Suggested',
+ type: 'header',
+ icon: null,
+ children: [{value: 'me'}, {value: 'unassigned'}],
+ },
+ {
+ title: 'All',
+ type: 'header',
+ icon: null,
+ children: [{value: 'person1@sentry.io'}, {value: 'person2@sentry.io'}],
+ },
+ ],
+ },
+ [FieldKey.BROWSER_NAME]: {
+ key: FieldKey.BROWSER_NAME,
+ name: 'Browser Name',
+ kind: FieldKind.FIELD,
+ predefined: true,
+ values: ['Chrome', 'Firefox', 'Safari', 'Edge', 'Internet Explorer', 'Opera 1,2'],
+ },
+ [FieldKey.IS]: {
+ key: FieldKey.IS,
+ name: 'is',
+ predefined: true,
+ values: ['resolved', 'unresolved', 'ignored'],
+ },
+ [FieldKey.LAST_SEEN]: {
+ key: FieldKey.LAST_SEEN,
+ name: 'lastSeen',
+ kind: FieldKind.FIELD,
+ },
+ [FieldKey.TIMES_SEEN]: {
+ key: FieldKey.TIMES_SEEN,
+ name: 'timesSeen',
+ kind: FieldKind.FIELD,
+ },
+ [WebVital.LCP]: {
+ key: WebVital.LCP,
+ name: 'lcp',
+ kind: FieldKind.FIELD,
+ },
+ [MobileVital.FRAMES_SLOW_RATE]: {
+ key: MobileVital.FRAMES_SLOW_RATE,
+ name: 'framesSlowRate',
+ kind: FieldKind.FIELD,
+ },
+ custom_tag_name: {
+ key: 'custom_tag_name',
+ name: 'Custom_Tag_Name',
+ },
+};
+
+const FILTER_KEY_SECTIONS: FilterKeySection[] = [
+ {
+ value: 'cat_1',
+ label: 'Category 1',
+ children: [FieldKey.ASSIGNED, FieldKey.IS],
+ },
+ {
+ value: 'cat_2',
+ label: 'Category 2',
+ children: [WebVital.LCP, MobileVital.FRAMES_SLOW_RATE],
+ },
+ {
+ value: 'cat_3',
+ label: 'Category 3',
+ children: [FieldKey.TIMES_SEEN],
+ },
+];
diff --git a/static/app/views/detectors/detail.tsx b/static/app/views/detectors/detail.tsx
index fb42e08764de26..8aa3fa40a75068 100644
--- a/static/app/views/detectors/detail.tsx
+++ b/static/app/views/detectors/detail.tsx
@@ -18,9 +18,11 @@ import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import getDuration from 'sentry/utils/duration/getDuration';
import useOrganization from 'sentry/utils/useOrganization';
+import {useParams} from 'sentry/utils/useParams';
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';
type Priority = {
@@ -36,9 +38,14 @@ const priorities: Priority[] = [
export default function DetectorDetail() {
const organization = useOrganization();
useWorkflowEngineFeatureGate({redirect: true});
+ const {detectorId} = useParams();
+ if (!detectorId) {
+ throw new Error(`Unable to find detector.`);
+ }
+ const {data: detector} = useDetectorQuery(detectorId);
return (
-
+
diff --git a/static/app/views/detectors/edit.tsx b/static/app/views/detectors/edit.tsx
index 30dc7914f16385..e8726c3607d0bb 100644
--- a/static/app/views/detectors/edit.tsx
+++ b/static/app/views/detectors/edit.tsx
@@ -1,7 +1,8 @@
/* eslint-disable no-alert */
-import {Fragment} from 'react';
+import {Fragment, useState} from 'react';
import {Button} from 'sentry/components/core/button';
+import FormModel from 'sentry/components/forms/model';
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
import {ActionsProvider} from 'sentry/components/workflowEngine/layout/actions';
import {BreadcrumbsProvider} from 'sentry/components/workflowEngine/layout/breadcrumbs';
@@ -9,20 +10,23 @@ import EditLayout from 'sentry/components/workflowEngine/layout/edit';
import {useWorkflowEngineFeatureGate} from 'sentry/components/workflowEngine/useWorkflowEngineFeatureGate';
import {t} from 'sentry/locale';
import useOrganization from 'sentry/utils/useOrganization';
+import {MetricDetectorForm} from 'sentry/views/detectors/components/forms/metric';
import {makeMonitorBasePathname} from 'sentry/views/detectors/pathnames';
export default function DetectorEdit() {
const organization = useOrganization();
useWorkflowEngineFeatureGate({redirect: true});
+ const [title, setTitle] = useState(t('Edit Monitor'));
+ const [model] = useState(() => new FormModel());
return (
-
+
}>
-
- Edit Monitor
+
+
diff --git a/static/app/views/detectors/hooks/index.ts b/static/app/views/detectors/hooks/index.ts
new file mode 100644
index 00000000000000..beed29a5d662b9
--- /dev/null
+++ b/static/app/views/detectors/hooks/index.ts
@@ -0,0 +1,69 @@
+import {t} from 'sentry/locale';
+import AlertStore from 'sentry/stores/alertStore';
+import type {Detector} from 'sentry/types/workflowEngine/detectors';
+import {
+ useApiQueries,
+ useApiQuery,
+ useMutation,
+ useQueryClient,
+} from 'sentry/utils/queryClient';
+import useApi from 'sentry/utils/useApi';
+import useOrganization from 'sentry/utils/useOrganization';
+
+export interface UseDetectorsQueryOptions {
+ query?: string;
+ sortBy?: string;
+}
+export function useDetectorsQuery(_options: UseDetectorsQueryOptions = {}) {
+ const org = useOrganization();
+ return useApiQuery(makeDetectorQueryKey(org.slug), {
+ staleTime: 0,
+ retry: false,
+ });
+}
+
+export const makeDetectorQueryKey = (orgSlug: string, detectorId = ''): [url: string] => [
+ `/organizations/${orgSlug}/detectors/${detectorId ? `${detectorId}/` : ''}`,
+];
+
+export function useCreateDetector() {
+ const org = useOrganization();
+ const api = useApi({persistInFlight: true});
+ const queryClient = useQueryClient();
+ const queryKey = makeDetectorQueryKey(org.slug);
+
+ return useMutation({
+ mutationFn: data =>
+ api.requestPromise(queryKey[0], {
+ method: 'POST',
+ data,
+ }),
+ onSuccess: _ => {
+ queryClient.invalidateQueries({queryKey});
+ },
+ onError: _ => {
+ AlertStore.addAlert({type: 'error', message: t('Unable to create monitor')});
+ },
+ });
+}
+
+export function useDetectorQuery(detectorId: string) {
+ const org = useOrganization();
+
+ return useApiQuery(makeDetectorQueryKey(org.slug, detectorId), {
+ staleTime: 0,
+ retry: false,
+ });
+}
+
+export function useDetectorQueriesByIds(detectorId: string[]) {
+ const org = useOrganization();
+
+ return useApiQueries(
+ detectorId.map(id => makeDetectorQueryKey(org.slug, id)),
+ {
+ staleTime: 0,
+ retry: false,
+ }
+ );
+}
diff --git a/static/app/views/detectors/list.tsx b/static/app/views/detectors/list.tsx
index f3978c701a87ad..f710968d74f12e 100644
--- a/static/app/views/detectors/list.tsx
+++ b/static/app/views/detectors/list.tsx
@@ -14,10 +14,12 @@ import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import useOrganization from 'sentry/utils/useOrganization';
import DetectorListTable from 'sentry/views/detectors/components/detectorListTable';
+import {useDetectorsQuery} from 'sentry/views/detectors/hooks';
import {makeMonitorBasePathname} from 'sentry/views/detectors/pathnames';
export default function DetectorsList() {
useWorkflowEngineFeatureGate({redirect: true});
+ const {data: detectors} = useDetectorsQuery();
return (
@@ -25,7 +27,7 @@ export default function DetectorsList() {
}>
-
+
@@ -51,7 +53,7 @@ function Actions() {
}
+ icon={}
>
{t('Create Monitor')}
diff --git a/static/app/views/detectors/new-settings.tsx b/static/app/views/detectors/new-settings.tsx
index 91a270784d7153..9d64d70cbce39e 100644
--- a/static/app/views/detectors/new-settings.tsx
+++ b/static/app/views/detectors/new-settings.tsx
@@ -1,6 +1,9 @@
+import {useCallback, useMemo} from 'react';
+
import {Flex} from 'sentry/components/container/flex';
import {Button} from 'sentry/components/core/button';
import {LinkButton} from 'sentry/components/core/button/linkButton';
+import FormModel from 'sentry/components/forms/model';
import {
StickyFooter,
StickyFooterLabel,
@@ -8,16 +11,34 @@ import {
import {useWorkflowEngineFeatureGate} from 'sentry/components/workflowEngine/useWorkflowEngineFeatureGate';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
+import type {Detector} from 'sentry/types/workflowEngine/detectors';
+import {useNavigate} from 'sentry/utils/useNavigate';
import useOrganization from 'sentry/utils/useOrganization';
+import {MetricDetectorForm} from 'sentry/views/detectors/components/forms/metric';
+import {useCreateDetector} from 'sentry/views/detectors/hooks';
import NewDetectorLayout from 'sentry/views/detectors/layouts/new';
-import {makeMonitorBasePathname} from 'sentry/views/detectors/pathnames';
+import {
+ makeMonitorBasePathname,
+ makeMonitorDetailsPathname,
+} from 'sentry/views/detectors/pathnames';
export default function DetectorNewSettings() {
const organization = useOrganization();
useWorkflowEngineFeatureGate({redirect: true});
+ const navigate = useNavigate();
+ const model = useMemo(() => new FormModel(), []);
+
+ const {mutateAsync: createDetector} = useCreateDetector();
+
+ const handleSubmit = useCallback(async () => {
+ const data = model.getData() as unknown as Detector;
+ const result = await createDetector(data);
+ navigate(makeMonitorDetailsPathname(organization.slug, result.id));
+ }, [createDetector, model, navigate, organization]);
return (
+
{t('Step 2 of 2')}
@@ -27,7 +48,9 @@ export default function DetectorNewSettings() {
>
{t('Back')}
-
+
diff --git a/static/app/views/detectors/routes.tsx b/static/app/views/detectors/routes.tsx
index d686823673d3f6..5fc6029046bf30 100644
--- a/static/app/views/detectors/routes.tsx
+++ b/static/app/views/detectors/routes.tsx
@@ -11,7 +11,7 @@ export const detectorRoutes = (
component={make(() => import('sentry/views/detectors/new-settings'))}
/>
-
+
import('sentry/views/detectors/detail'))} />
import('sentry/views/detectors/edit'))} />
diff --git a/tests/js/fixtures/automations.ts b/tests/js/fixtures/automations.ts
new file mode 100644
index 00000000000000..c06db05ceb361a
--- /dev/null
+++ b/tests/js/fixtures/automations.ts
@@ -0,0 +1,15 @@
+import {DataConditionGroupFixture} from 'sentry-fixture/dataConditions';
+
+import type {Automation} from 'sentry/types/workflowEngine/automations';
+
+export function AutomationFixture(params: Partial): Automation {
+ return {
+ id: '1',
+ name: 'Automation',
+ lastTriggered: '2025-01-01T00:00:00.000Z',
+ actionFilters: [],
+ detectorIds: [],
+ triggers: DataConditionGroupFixture({}),
+ ...params,
+ };
+}
diff --git a/tests/js/fixtures/dataConditions.ts b/tests/js/fixtures/dataConditions.ts
new file mode 100644
index 00000000000000..2106c3d791e087
--- /dev/null
+++ b/tests/js/fixtures/dataConditions.ts
@@ -0,0 +1,29 @@
+import type {
+ DataCondition,
+ DataConditionGroup,
+} from 'sentry/types/workflowEngine/dataConditions';
+import {
+ DataConditionGroupLogicType,
+ DataConditionType,
+} from 'sentry/types/workflowEngine/dataConditions';
+
+export function DataConditionFixture(params: Partial): DataCondition {
+ return {
+ comparison_type: DataConditionType.EQUAL,
+ comparison: '8',
+ id: '1',
+ ...params,
+ };
+}
+
+export function DataConditionGroupFixture(
+ params: Partial
+): DataConditionGroup {
+ return {
+ conditions: [DataConditionFixture({})],
+ id: '1',
+ logicType: DataConditionGroupLogicType.ANY,
+ actions: [],
+ ...params,
+ };
+}
diff --git a/tests/js/fixtures/detectors.ts b/tests/js/fixtures/detectors.ts
new file mode 100644
index 00000000000000..31a151328807e7
--- /dev/null
+++ b/tests/js/fixtures/detectors.ts
@@ -0,0 +1,39 @@
+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';
+
+export function DetectorFixture(params: Partial): Detector {
+ return {
+ id: '1',
+ name: 'detector',
+ projectId: '1',
+ createdBy: UserFixture().id,
+ dateCreated: '2025-01-01T00:00:00.000Z',
+ dateUpdated: '2025-01-01T00:00:00.000Z',
+ lastTriggered: '2025-01-01T00:00:00.000Z',
+ workflowIds: [],
+ config: {},
+ type: 'metric',
+ dataCondition: DataConditionGroupFixture({}),
+ disabled: false,
+ dataSource: params.dataSource ?? DetectorDataSource({}),
+ ...params,
+ };
+}
+
+export function DetectorDataSource(params: Partial): DataSource {
+ return {
+ id: '1',
+ status: 1,
+ snubaQuery: {
+ aggregate: '',
+ dataset: '',
+ id: '',
+ query: '',
+ timeWindow: 60,
+ ...params,
+ },
+ };
+}