diff --git a/static/app/router/routes.tsx b/static/app/router/routes.tsx
index 0d153ef8755423..9b295217313086 100644
--- a/static/app/router/routes.tsx
+++ b/static/app/router/routes.tsx
@@ -1675,6 +1675,7 @@ function buildRoutes(): RouteObject[] {
const monitorRoutes: SentryRouteObject = {
path: '/monitors/',
withOrgPath: true,
+ component: make(() => import('sentry/views/detectors/detectorViewContainer')),
children: [
...detectorRoutes.children!,
automationRoutes,
diff --git a/static/app/views/automations/list.spec.tsx b/static/app/views/automations/list.spec.tsx
index 5605420cba12f7..f484c44fb2bb28 100644
--- a/static/app/views/automations/list.spec.tsx
+++ b/static/app/views/automations/list.spec.tsx
@@ -18,6 +18,7 @@ import {
within,
} from 'sentry-test/reactTestingLibrary';
+import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
import PageFiltersStore from 'sentry/stores/pageFiltersStore';
import ProjectsStore from 'sentry/stores/projectsStore';
import AutomationsList from 'sentry/views/automations/list';
@@ -394,7 +395,13 @@ describe('AutomationsList', () => {
body: {},
});
- render(, {organization});
+ render(
+ // MonitorViewContainer provides PageFiltersContainer typically
+
+
+ ,
+ {organization}
+ );
renderGlobalModal();
// Mock the filtered search results - this will be used when search is applied
diff --git a/static/app/views/automations/list.tsx b/static/app/views/automations/list.tsx
index 204537cfca89ff..4ddcdc68d78cba 100644
--- a/static/app/views/automations/list.tsx
+++ b/static/app/views/automations/list.tsx
@@ -2,7 +2,6 @@ import {useCallback} from 'react';
import {LinkButton} from 'sentry/components/core/button/linkButton';
import {Flex} from 'sentry/components/core/layout';
-import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
import Pagination from 'sentry/components/pagination';
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
@@ -80,44 +79,42 @@ export default function AutomationsList() {
return (
-
- }
- title={t('Alerts')}
- description={t(
- 'Alerts are triggered when issue changes state, is created, or passes a threshold. They perform external actions like sending notifications, creating tickets, or calling webhooks and integrations.'
- )}
- docsUrl="https://docs.sentry.io/product/automations/"
- >
-
-
-
0}
- id="AutomationsList-Table"
- isLoading={isLoading}
- >
- maxHitsInt ? `${maxHits}+` : hits}
- allResultsVisible={allResultsVisible()}
- />
-
-
{
- navigate({
- pathname: location.pathname,
- query: {...location.query, cursor: newCursor},
- });
- }}
+ }
+ title={t('Alerts')}
+ description={t(
+ 'Alerts are triggered when issue changes state, is created, or passes a threshold. They perform external actions like sending notifications, creating tickets, or calling webhooks and integrations.'
+ )}
+ docsUrl="https://docs.sentry.io/product/automations/"
+ >
+
+
+
0}
+ id="AutomationsList-Table"
+ isLoading={isLoading}
+ >
+ maxHitsInt ? `${maxHits}+` : hits}
+ allResultsVisible={allResultsVisible()}
/>
-
-
-
+
+ {
+ navigate({
+ pathname: location.pathname,
+ query: {...location.query, cursor: newCursor},
+ });
+ }}
+ />
+
+
);
}
diff --git a/static/app/views/detectors/components/detectorListTable/index.tsx b/static/app/views/detectors/components/detectorListTable/index.tsx
index c25049d24db157..a2176c15e2128c 100644
--- a/static/app/views/detectors/components/detectorListTable/index.tsx
+++ b/static/app/views/detectors/components/detectorListTable/index.tsx
@@ -31,7 +31,7 @@ import {
DetectorListRow,
DetectorListRowSkeleton,
} from 'sentry/views/detectors/components/detectorListTable/detectorListRow';
-import {DETECTOR_LIST_PAGE_LIMIT} from 'sentry/views/detectors/constants';
+import {DETECTOR_LIST_PAGE_LIMIT} from 'sentry/views/detectors/list/common/constants';
import {
useMonitorViewContext,
type MonitorListAdditionalColumn,
diff --git a/static/app/views/detectors/detectorViewContainer.tsx b/static/app/views/detectors/detectorViewContainer.tsx
new file mode 100644
index 00000000000000..82e2daf9d71387
--- /dev/null
+++ b/static/app/views/detectors/detectorViewContainer.tsx
@@ -0,0 +1,14 @@
+import {Outlet} from 'react-router-dom';
+
+import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
+import {useWorkflowEngineFeatureGate} from 'sentry/components/workflowEngine/useWorkflowEngineFeatureGate';
+
+export default function DetectorViewContainer() {
+ useWorkflowEngineFeatureGate({redirect: true});
+
+ return (
+
+
+
+ );
+}
diff --git a/static/app/views/detectors/list.tsx b/static/app/views/detectors/list.tsx
deleted file mode 100644
index bdcdf001690710..00000000000000
--- a/static/app/views/detectors/list.tsx
+++ /dev/null
@@ -1,270 +0,0 @@
-import {useCallback} from 'react';
-
-import {LinkButton} from 'sentry/components/core/button/linkButton';
-import {Flex} from 'sentry/components/core/layout';
-import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
-import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
-import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
-import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
-import Pagination from 'sentry/components/pagination';
-import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
-import ListLayout from 'sentry/components/workflowEngine/layout/list';
-import {useWorkflowEngineFeatureGate} from 'sentry/components/workflowEngine/useWorkflowEngineFeatureGate';
-import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
-import {IconAdd} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import type {DetectorType} from 'sentry/types/workflowEngine/detectors';
-import parseLinkHeader from 'sentry/utils/parseLinkHeader';
-import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
-import {decodeScalar, decodeSorts} from 'sentry/utils/queryString';
-import useLocationQuery from 'sentry/utils/url/useLocationQuery';
-import {useLocation} from 'sentry/utils/useLocation';
-import {useNavigate} from 'sentry/utils/useNavigate';
-import useOrganization from 'sentry/utils/useOrganization';
-import usePageFilters from 'sentry/utils/usePageFilters';
-import DetectorListTable from 'sentry/views/detectors/components/detectorListTable';
-import {DetectorSearch} from 'sentry/views/detectors/components/detectorSearch';
-import {MonitorFeedbackButton} from 'sentry/views/detectors/components/monitorFeedbackButton';
-import {DETECTOR_LIST_PAGE_LIMIT} from 'sentry/views/detectors/constants';
-import {useDetectorsQuery} from 'sentry/views/detectors/hooks';
-import {useMonitorViewContext} from 'sentry/views/detectors/monitorViewContext';
-import {makeMonitorCreatePathname} from 'sentry/views/detectors/pathnames';
-
-interface DetectorHeadingInfo {
- description: string;
- docsUrl: string;
- title: string;
-}
-
-const DETECTOR_TYPE_HEADING_MAPPING: Record<
- Exclude,
- DetectorHeadingInfo
-> = {
- error: {
- title: t('Error Monitors'),
- description: t(
- 'Error monitors are created by default for each project based on issue grouping/fingerprint rules.'
- ),
- docsUrl: 'https://docs.sentry.io/product/monitors/',
- },
- metric_issue: {
- title: t('Metric Monitors'),
- description: t(
- 'Metric monitors track errors based on span attributes and custom metrics.'
- ),
- docsUrl: 'https://docs.sentry.io/product/monitors/',
- },
- monitor_check_in_failure: {
- title: t('Cron Monitors'),
- description: t(
- 'Cron monitors check in on recurring jobs and tell you if they’re running on schedule, failing, or succeeding.'
- ),
- docsUrl: 'https://docs.sentry.io/product/crons/',
- },
- uptime_domain_failure: {
- title: t('Uptime Monitors'),
- description: t(
- 'Uptime monitors continuously track configured URLs, delivering alerts and insights to quickly identify downtime and troubleshoot issues.'
- ),
- docsUrl: 'https://docs.sentry.io/product/alerts/uptime-monitoring/',
- },
-};
-
-export default function DetectorsList() {
- useWorkflowEngineFeatureGate({redirect: true});
-
- const location = useLocation();
- const navigate = useNavigate();
- const {selection, isReady} = usePageFilters();
- const {detectorFilter, assigneeFilter, emptyState} = useMonitorViewContext();
-
- const {
- sort: sorts,
- query,
- cursor,
- } = useLocationQuery({
- fields: {
- sort: decodeSorts,
- query: decodeScalar,
- cursor: decodeScalar,
- },
- });
- const sort = sorts[0] ?? {kind: 'desc', field: 'latestGroup'};
-
- // Build the query with detector type and assignee filters if provided
- // Map DetectorType values to query values (e.g., 'monitor_check_in_failure' -> 'cron')
- const typeFilterQuery = detectorFilter ? `type:${detectorFilter}` : undefined;
- const assigneeFilterQuery = assigneeFilter ? `assignee:${assigneeFilter}` : undefined;
- const finalQuery = [typeFilterQuery, assigneeFilterQuery, query]
- .filter(Boolean)
- .join(' ');
-
- const {
- data: detectors,
- isLoading,
- isError,
- isSuccess,
- getResponseHeader,
- } = useDetectorsQuery(
- {
- cursor,
- query: finalQuery,
- sortBy: sort ? `${sort?.kind === 'asc' ? '' : '-'}${sort?.field}` : undefined,
- projects: selection.projects,
- limit: DETECTOR_LIST_PAGE_LIMIT,
- },
- {enabled: isReady}
- );
-
- const hits = getResponseHeader?.('X-Hits') || '';
- const hitsInt = hits ? parseInt(hits, 10) || 0 : 0;
- // If maxHits is not set, we assume there is no max
- const maxHits = getResponseHeader?.('X-Max-Hits') || '';
- const maxHitsInt = maxHits ? parseInt(maxHits, 10) || Infinity : Infinity;
-
- const pageLinks = getResponseHeader?.('Link');
-
- const allResultsVisible = useCallback(() => {
- if (!pageLinks) {
- return false;
- }
- const links = parseLinkHeader(pageLinks);
- return links && !links.previous!.results && !links.next!.results;
- }, [pageLinks]);
-
- // Determine the page heading info based on active filters
- const {
- title: pageTitle,
- description: pageDescription,
- docsUrl,
- } = detectorFilter
- ? DETECTOR_TYPE_HEADING_MAPPING[detectorFilter]
- : assigneeFilter === 'me'
- ? {
- title: t('My Monitors'),
- description: t(
- 'Monitors assigned to you or your team. Monitors are used to customize when to turn errors and performance problems into issues.'
- ),
- docsUrl: 'https://docs.sentry.io/product/monitors/',
- }
- : {
- title: t('Monitors'),
- description: t(
- 'Monitors are used to customize when to turn errors and performance problems into issues.'
- ),
- docsUrl: 'https://docs.sentry.io/product/monitors/',
- };
-
- return (
-
-
- }
- title={pageTitle}
- description={pageDescription}
- docsUrl={docsUrl}
- >
-
-
-
0}
- id="MonitorsList-Table"
- isLoading={isLoading}
- >
- {isSuccess && detectors?.length === 0 ? (
- emptyState
- ) : (
- maxHitsInt ? `${maxHits}+` : hits}
- allResultsVisible={allResultsVisible()}
- />
- )}
-
-
{
- navigate({
- pathname: location.pathname,
- query: {...location.query, cursor: newCursor},
- });
- }}
- />
-
-
-
-
- );
-}
-
-function TableHeader() {
- const location = useLocation();
- const navigate = useNavigate();
- const {detectorFilter, assigneeFilter, showTimeRangeSelector} = useMonitorViewContext();
- const query = typeof location.query.query === 'string' ? location.query.query : '';
-
- const onSearch = (searchQuery: string) => {
- navigate({
- pathname: location.pathname,
- query: {...location.query, query: searchQuery, cursor: undefined},
- });
- };
-
- // Exclude filter keys when they're set in context
- const excludeKeys = [detectorFilter && 'type', assigneeFilter && 'assignee'].filter(
- v => v !== undefined
- );
-
- return (
-
-
-
- {showTimeRangeSelector && }
-
-
-
-
-
- );
-}
-
-function Actions() {
- const organization = useOrganization();
- const {selection} = usePageFilters();
- const {detectorFilter} = useMonitorViewContext();
-
- // Pass the first selected project id that is not the all access project
- const project = selection.projects.find(pid => pid !== ALL_ACCESS_PROJECTS);
-
- // If detectorFilter is set, pass it as a query param to skip type selection
- const createPath = makeMonitorCreatePathname(organization.slug);
-
- const createQuery = detectorFilter
- ? {project, detectorType: detectorFilter}
- : {project};
-
- return (
-
-
- }
- size="sm"
- >
- {t('Create Monitor')}
-
-
- );
-}
diff --git a/static/app/views/detectors/list.spec.tsx b/static/app/views/detectors/list/allMonitors.spec.tsx
similarity index 96%
rename from static/app/views/detectors/list.spec.tsx
rename to static/app/views/detectors/list/allMonitors.spec.tsx
index 72de93c77d7c8e..97fda38969a583 100644
--- a/static/app/views/detectors/list.spec.tsx
+++ b/static/app/views/detectors/list/allMonitors.spec.tsx
@@ -21,7 +21,7 @@ import {
DetectorPriorityLevel,
} from 'sentry/types/workflowEngine/dataConditions';
import {Dataset, EventTypes} from 'sentry/views/alerts/rules/metric/types';
-import DetectorsList from 'sentry/views/detectors/list';
+import AllMonitors from 'sentry/views/detectors/list/allMonitors';
describe('DetectorsList', () => {
const organization = OrganizationFixture({
@@ -93,7 +93,7 @@ describe('DetectorsList', () => {
],
});
- render(, {organization});
+ render(, {organization});
await screen.findByText('Detector 1');
const row = screen.getByTestId('detector-list-row');
@@ -124,7 +124,7 @@ describe('DetectorsList', () => {
body: [AutomationFixture({id: '100', name: 'Automation 1', detectorIds: ['1']})],
});
- render(, {organization});
+ render(, {organization});
const row = await screen.findByTestId('detector-list-row');
expect(within(row).getByText('1 alert')).toBeInTheDocument();
@@ -140,7 +140,7 @@ describe('DetectorsList', () => {
body: [MetricDetectorFixture({name: 'Detector 1'})],
});
- render(, {organization});
+ render(, {organization});
await screen.findByText('Detector 1');
@@ -162,7 +162,7 @@ describe('DetectorsList', () => {
match: [MockApiClient.matchQuery({query: '!type:issue_stream type:error'})],
});
- render(, {organization});
+ render(, {organization});
await screen.findByText('Detector 1');
// Click through menus to select type:error
@@ -198,7 +198,7 @@ describe('DetectorsList', () => {
],
});
- render(, {organization});
+ render(, {organization});
await screen.findByText('Detector 1');
// Click through menus to select assignee
@@ -216,7 +216,7 @@ describe('DetectorsList', () => {
url: '/organizations/org-slug/detectors/',
body: [MetricDetectorFixture({name: 'Detector 1'})],
});
- const {router} = render(, {organization});
+ const {router} = render(, {organization});
await screen.findByText('Detector 1');
// Default sort is latestGroup descending
@@ -300,7 +300,7 @@ describe('DetectorsList', () => {
});
it('can select detectors', async () => {
- render(, {organization});
+ render(, {organization});
await screen.findByText('Enabled Detector');
const rows = screen.getAllByTestId('detector-list-row');
@@ -353,7 +353,7 @@ describe('DetectorsList', () => {
body: {},
});
- render(, {organization});
+ render(, {organization});
renderGlobalModal();
await screen.findByText('Disabled Detector');
@@ -393,7 +393,7 @@ describe('DetectorsList', () => {
body: {},
});
- render(, {organization});
+ render(, {organization});
renderGlobalModal();
await screen.findByText('Enabled Detector');
@@ -432,7 +432,7 @@ describe('DetectorsList', () => {
body: {},
});
- render(, {organization});
+ render(, {organization});
renderGlobalModal();
await screen.findByText('Enabled Detector');
@@ -469,7 +469,7 @@ describe('DetectorsList', () => {
body: {},
});
- render(, {organization});
+ render(, {organization});
renderGlobalModal();
const testUser = UserFixture({id: '2', email: 'test@example.com'});
@@ -543,7 +543,7 @@ describe('DetectorsList', () => {
features: ['workflow-engine-ui'],
access: [],
});
- render(, {organization: noPermsOrganization});
+ render(, {organization: noPermsOrganization});
renderGlobalModal();
await screen.findByText('Disabled Detector');
diff --git a/static/app/views/detectors/list/allMonitors.tsx b/static/app/views/detectors/list/allMonitors.tsx
new file mode 100644
index 00000000000000..0cfe3dde47f3ee
--- /dev/null
+++ b/static/app/views/detectors/list/allMonitors.tsx
@@ -0,0 +1,31 @@
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
+import WorkflowEngineListLayout from 'sentry/components/workflowEngine/layout/list';
+import {t} from 'sentry/locale';
+import {DetectorListActions} from 'sentry/views/detectors/list/common/detectorListActions';
+import {DetectorListContent} from 'sentry/views/detectors/list/common/detectorListContent';
+import {DetectorListHeader} from 'sentry/views/detectors/list/common/detectorListHeader';
+import {useDetectorListQuery} from 'sentry/views/detectors/list/common/useDetectorListQuery';
+
+const TITLE = t('Monitors');
+const DESCRIPTION = t(
+ 'Monitors are used to transform errors, performance problems, and other events into issues.'
+);
+const DOCS_URL = 'https://docs.sentry.io/product/monitors/';
+
+export default function AllMonitors() {
+ const detectorListQuery = useDetectorListQuery();
+
+ return (
+
+ }
+ title={TITLE}
+ description={DESCRIPTION}
+ docsUrl={DOCS_URL}
+ >
+
+
+
+
+ );
+}
diff --git a/static/app/views/detectors/constants.tsx b/static/app/views/detectors/list/common/constants.tsx
similarity index 100%
rename from static/app/views/detectors/constants.tsx
rename to static/app/views/detectors/list/common/constants.tsx
diff --git a/static/app/views/detectors/list/common/detectorListActions.tsx b/static/app/views/detectors/list/common/detectorListActions.tsx
new file mode 100644
index 00000000000000..68e6becc922ba7
--- /dev/null
+++ b/static/app/views/detectors/list/common/detectorListActions.tsx
@@ -0,0 +1,45 @@
+import {LinkButton} from 'sentry/components/core/button/linkButton';
+import {Flex} from 'sentry/components/core/layout';
+import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
+import {IconAdd} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import type {DetectorType} from 'sentry/types/workflowEngine/detectors';
+import useOrganization from 'sentry/utils/useOrganization';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import {MonitorFeedbackButton} from 'sentry/views/detectors/components/monitorFeedbackButton';
+import {makeMonitorCreatePathname} from 'sentry/views/detectors/pathnames';
+
+interface DetectorListActionsProps {
+ children?: React.ReactNode;
+ /**
+ * Pass a detector type to skip type selection on the create monitor page
+ */
+ detectorType?: DetectorType;
+}
+
+export function DetectorListActions({detectorType, children}: DetectorListActionsProps) {
+ const organization = useOrganization();
+ const {selection} = usePageFilters();
+
+ const createPath = makeMonitorCreatePathname(organization.slug);
+ const project = selection.projects.find(pid => pid !== ALL_ACCESS_PROJECTS);
+ const createQuery = detectorType ? {project, detectorType} : {project};
+
+ return (
+
+ {children}
+
+ }
+ size="sm"
+ >
+ {t('Create Monitor')}
+
+
+ );
+}
diff --git a/static/app/views/detectors/list/common/detectorListContent.tsx b/static/app/views/detectors/list/common/detectorListContent.tsx
new file mode 100644
index 00000000000000..5630dcb0c8ed56
--- /dev/null
+++ b/static/app/views/detectors/list/common/detectorListContent.tsx
@@ -0,0 +1,81 @@
+import {useCallback, type ReactNode} from 'react';
+
+import Pagination from 'sentry/components/pagination';
+import type {Detector} from 'sentry/types/workflowEngine/detectors';
+import parseLinkHeader from 'sentry/utils/parseLinkHeader';
+import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
+import {useLocation} from 'sentry/utils/useLocation';
+import {useNavigate} from 'sentry/utils/useNavigate';
+import DetectorListTable from 'sentry/views/detectors/components/detectorListTable';
+import {useDetectorListSort} from 'sentry/views/detectors/list/common/useDetectorListSort';
+
+interface DetectorListContentProps {
+ data: Detector[] | undefined;
+ isError: boolean;
+ isLoading: boolean;
+ isSuccess: boolean;
+ emptyState?: ReactNode;
+ getResponseHeader?: ((header: string) => string | null | undefined) | undefined;
+}
+
+export function DetectorListContent({
+ data,
+ emptyState,
+ isLoading,
+ isError,
+ isSuccess,
+ getResponseHeader,
+}: DetectorListContentProps) {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const sort = useDetectorListSort();
+
+ const hits = getResponseHeader?.('X-Hits') || '';
+ const hitsInt = hits ? parseInt(hits, 10) || 0 : 0;
+ // If maxHits is not set, we assume there is no max
+ const maxHits = getResponseHeader?.('X-Max-Hits') || '';
+ const maxHitsInt = maxHits ? parseInt(maxHits, 10) || Infinity : Infinity;
+
+ const pageLinks = getResponseHeader?.('Link');
+
+ const allResultsVisible = useCallback(() => {
+ if (!pageLinks) {
+ return false;
+ }
+ const links = parseLinkHeader(pageLinks);
+ return links && !links.previous!.results && !links.next!.results;
+ }, [pageLinks]);
+
+ return (
+
+
0}
+ id="MonitorsList-Table"
+ isLoading={isLoading}
+ >
+ {isSuccess && data?.length === 0 && emptyState ? (
+ emptyState
+ ) : (
+ maxHitsInt ? `${maxHits}+` : hits}
+ allResultsVisible={allResultsVisible()}
+ />
+ )}
+
+
{
+ navigate({
+ pathname: location.pathname,
+ query: {...location.query, cursor: newCursor},
+ });
+ }}
+ />
+
+ );
+}
diff --git a/static/app/views/detectors/list/common/detectorListHeader.tsx b/static/app/views/detectors/list/common/detectorListHeader.tsx
new file mode 100644
index 00000000000000..209672250522a2
--- /dev/null
+++ b/static/app/views/detectors/list/common/detectorListHeader.tsx
@@ -0,0 +1,53 @@
+import {Flex} from 'sentry/components/core/layout';
+import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
+import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
+import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
+import {defined} from 'sentry/utils';
+import {useLocation} from 'sentry/utils/useLocation';
+import {useNavigate} from 'sentry/utils/useNavigate';
+import {DetectorSearch} from 'sentry/views/detectors/components/detectorSearch';
+
+interface TableHeaderProps {
+ showAssigneeFilter?: boolean;
+ showTimeRangeSelector?: boolean;
+ showTypeFilter?: boolean;
+}
+
+export function DetectorListHeader({
+ showAssigneeFilter = true,
+ showTypeFilter = true,
+ showTimeRangeSelector = false,
+}: TableHeaderProps) {
+ const location = useLocation();
+ const navigate = useNavigate();
+ const query = typeof location.query.query === 'string' ? location.query.query : '';
+
+ const onSearch = (searchQuery: string) => {
+ navigate({
+ pathname: location.pathname,
+ query: {...location.query, query: searchQuery, cursor: undefined},
+ });
+ };
+
+ // Exclude filter keys when they're set
+ const excludeKeys = [
+ showTypeFilter ? null : 'type',
+ showAssigneeFilter ? null : 'assignee',
+ ].filter(defined);
+
+ return (
+
+
+
+ {showTimeRangeSelector && }
+
+
+
+
+
+ );
+}
diff --git a/static/app/views/detectors/list/common/useDetectorListQuery.tsx b/static/app/views/detectors/list/common/useDetectorListQuery.tsx
new file mode 100644
index 00000000000000..5dfdacd6af5354
--- /dev/null
+++ b/static/app/views/detectors/list/common/useDetectorListQuery.tsx
@@ -0,0 +1,42 @@
+import type {DetectorType} from 'sentry/types/workflowEngine/detectors';
+import {decodeScalar} from 'sentry/utils/queryString';
+import {useLocation} from 'sentry/utils/useLocation';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import {useDetectorsQuery} from 'sentry/views/detectors/hooks';
+import {DETECTOR_LIST_PAGE_LIMIT} from 'sentry/views/detectors/list/common/constants';
+import {useDetectorListSort} from 'sentry/views/detectors/list/common/useDetectorListSort';
+
+type UseDetectorListQueryOptions = {
+ assigneeFilter?: string;
+ detectorFilter?: Exclude;
+};
+
+export function useDetectorListQuery({
+ detectorFilter,
+ assigneeFilter,
+}: UseDetectorListQueryOptions = {}) {
+ const location = useLocation();
+ const {selection, isReady} = usePageFilters();
+ const cursor = decodeScalar(location.query.cursor);
+ const query = decodeScalar(location.query.query);
+ const sort = useDetectorListSort();
+
+ // Build the query with detector type and assignee filters if provided
+ // Map DetectorType values to query values (e.g., 'monitor_check_in_failure' -> 'cron')
+ const typeFilterQuery = detectorFilter ? `type:${detectorFilter}` : undefined;
+ const assigneeFilterQuery = assigneeFilter ? `assignee:${assigneeFilter}` : undefined;
+ const finalQuery = [typeFilterQuery, assigneeFilterQuery, query]
+ .filter(Boolean)
+ .join(' ');
+
+ return useDetectorsQuery(
+ {
+ cursor,
+ query: finalQuery,
+ sortBy: sort ? `${sort.kind === 'asc' ? '' : '-'}${sort.field}` : undefined,
+ projects: selection.projects,
+ limit: DETECTOR_LIST_PAGE_LIMIT,
+ },
+ {enabled: isReady}
+ );
+}
diff --git a/static/app/views/detectors/list/common/useDetectorListSort.tsx b/static/app/views/detectors/list/common/useDetectorListSort.tsx
new file mode 100644
index 00000000000000..912294b914be50
--- /dev/null
+++ b/static/app/views/detectors/list/common/useDetectorListSort.tsx
@@ -0,0 +1,16 @@
+import type {Sort} from 'sentry/utils/discover/fields';
+import {decodeSorts} from 'sentry/utils/queryString';
+import {useLocation} from 'sentry/utils/useLocation';
+
+const DEFAULT_SORT: Sort = {kind: 'desc', field: 'latestGroup'};
+
+export function useDetectorListSort(): Sort {
+ const location = useLocation();
+ const sort = decodeSorts(location.query.sort)[0];
+
+ if (!sort) {
+ return DEFAULT_SORT;
+ }
+
+ return sort;
+}
diff --git a/static/app/views/detectors/list/cron.tsx b/static/app/views/detectors/list/cron.tsx
index 81105e2da5240a..74c2ed986b48ad 100644
--- a/static/app/views/detectors/list/cron.tsx
+++ b/static/app/views/detectors/list/cron.tsx
@@ -1,4 +1,4 @@
-import {useCallback, useMemo, useRef} from 'react';
+import {useMemo, useRef} from 'react';
import styled from '@emotion/styled';
import {Stack} from '@sentry/scraps/layout';
@@ -7,19 +7,23 @@ import {Text} from '@sentry/scraps/text';
import {CheckInPlaceholder} from 'sentry/components/checkInTimeline/checkInPlaceholder';
import {CheckInTimeline} from 'sentry/components/checkInTimeline/checkInTimeline';
import {useTimeWindowConfig} from 'sentry/components/checkInTimeline/hooks/useTimeWindowConfig';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
import {SimpleTable} from 'sentry/components/tables/simpleTable';
+import WorkflowEngineListLayout from 'sentry/components/workflowEngine/layout/list';
+import {t} from 'sentry/locale';
import {fadeIn} from 'sentry/styles/animations';
import type {CronDetector, Detector} from 'sentry/types/workflowEngine/detectors';
import {useDebouncedValue} from 'sentry/utils/useDebouncedValue';
import {useDimensions} from 'sentry/utils/useDimensions';
import {HeaderCell} from 'sentry/views/detectors/components/detectorListTable';
-import DetectorsList from 'sentry/views/detectors/list';
+import {DetectorListActions} from 'sentry/views/detectors/list/common/detectorListActions';
+import {DetectorListContent} from 'sentry/views/detectors/list/common/detectorListContent';
+import {DetectorListHeader} from 'sentry/views/detectors/list/common/detectorListHeader';
+import {useDetectorListQuery} from 'sentry/views/detectors/list/common/useDetectorListQuery';
import {
MonitorViewContext,
- useMonitorViewContext,
type MonitorListAdditionalColumn,
type MonitorViewContextValue,
- type RenderVisualizationParams,
} from 'sentry/views/detectors/monitorViewContext';
import {CronsLandingPanel} from 'sentry/views/insights/crons/components/cronsLandingPanel';
import MonitorEnvironmentLabel from 'sentry/views/insights/crons/components/overviewTimeline/monitorEnvironmentLabel';
@@ -107,43 +111,57 @@ const ADDITIONAL_COLUMNS: MonitorListAdditionalColumn[] = [
},
];
-export default function CronDetectorsList() {
- const parentContext = useMonitorViewContext();
+const TITLE = t('Cron Monitors');
+const DESCRIPTION = t(
+ "Cron monitors check in on recurring jobs and tell you if they're running on schedule, failing, or succeeding."
+);
+const DOCS_URL = 'https://docs.sentry.io/product/crons/';
- const renderVisualization = useCallback(({detector}: RenderVisualizationParams) => {
- if (!detector) {
- return (
-
-
- |
- );
- }
- if (detector.type === 'monitor_check_in_failure') {
- return ;
- }
- return null;
- }, []);
+export default function CronDetectorsList() {
+ const detectorListQuery = useDetectorListQuery({
+ detectorFilter: 'monitor_check_in_failure',
+ });
- const contextValue = useMemo(
- () => ({
- ...parentContext,
- detectorFilter: 'monitor_check_in_failure',
- renderVisualization,
- showTimeRangeSelector: true,
- emptyState: ,
+ const contextValue = useMemo(() => {
+ return {
additionalColumns: ADDITIONAL_COLUMNS,
- }),
- [parentContext, renderVisualization]
- );
+ renderVisualization: ({detector}) => {
+ if (!detector) {
+ return (
+
+
+ |
+ );
+ }
+ if (detector.type === 'monitor_check_in_failure') {
+ return ;
+ }
+ return null;
+ },
+ };
+ }, []);
return (
-
+
+ }
+ title={TITLE}
+ description={DESCRIPTION}
+ docsUrl={DOCS_URL}
+ >
+
+ }
+ />
+
+
);
}
diff --git a/static/app/views/detectors/list/error.tsx b/static/app/views/detectors/list/error.tsx
index 0d76132c22ab26..fee209711886b7 100644
--- a/static/app/views/detectors/list/error.tsx
+++ b/static/app/views/detectors/list/error.tsx
@@ -1,20 +1,33 @@
-import DetectorsList from 'sentry/views/detectors/list';
-import {
- MonitorViewContext,
- useMonitorViewContext,
-} from 'sentry/views/detectors/monitorViewContext';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
+import WorkflowEngineListLayout from 'sentry/components/workflowEngine/layout/list';
+import {t} from 'sentry/locale';
+import {DetectorListActions} from 'sentry/views/detectors/list/common/detectorListActions';
+import {DetectorListContent} from 'sentry/views/detectors/list/common/detectorListContent';
+import {DetectorListHeader} from 'sentry/views/detectors/list/common/detectorListHeader';
+import {useDetectorListQuery} from 'sentry/views/detectors/list/common/useDetectorListQuery';
+
+const TITLE = t('Error Monitors');
+const DESCRIPTION = t(
+ 'Error monitors are created by default for each project based on issue grouping/fingerprint rules.'
+);
+const DOCS_URL = 'https://docs.sentry.io/product/monitors/';
export default function ErrorDetectorsList() {
- const parentContext = useMonitorViewContext();
+ const detectorListQuery = useDetectorListQuery({
+ detectorFilter: 'error',
+ });
return (
-
-
-
+
+ }
+ title={TITLE}
+ description={DESCRIPTION}
+ docsUrl={DOCS_URL}
+ >
+
+
+
+
);
}
diff --git a/static/app/views/detectors/list/metric.tsx b/static/app/views/detectors/list/metric.tsx
index 641f7943c12ab7..25313c3a498648 100644
--- a/static/app/views/detectors/list/metric.tsx
+++ b/static/app/views/detectors/list/metric.tsx
@@ -1,20 +1,33 @@
-import DetectorsList from 'sentry/views/detectors/list';
-import {
- MonitorViewContext,
- useMonitorViewContext,
-} from 'sentry/views/detectors/monitorViewContext';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
+import WorkflowEngineListLayout from 'sentry/components/workflowEngine/layout/list';
+import {t} from 'sentry/locale';
+import {DetectorListActions} from 'sentry/views/detectors/list/common/detectorListActions';
+import {DetectorListContent} from 'sentry/views/detectors/list/common/detectorListContent';
+import {DetectorListHeader} from 'sentry/views/detectors/list/common/detectorListHeader';
+import {useDetectorListQuery} from 'sentry/views/detectors/list/common/useDetectorListQuery';
+
+const TITLE = t('Metric Monitors');
+const DESCRIPTION = t(
+ 'Metric monitors track errors based on span attributes and custom metrics.'
+);
+const DOCS_URL = 'https://docs.sentry.io/product/monitors/';
export default function MetricDetectorsList() {
- const parentContext = useMonitorViewContext();
+ const detectorListQuery = useDetectorListQuery({
+ detectorFilter: 'metric_issue',
+ });
return (
-
-
-
+
+ }
+ title={TITLE}
+ description={DESCRIPTION}
+ docsUrl={DOCS_URL}
+ >
+
+
+
+
);
}
diff --git a/static/app/views/detectors/list/myMonitors.tsx b/static/app/views/detectors/list/myMonitors.tsx
index 3be67662eac0c5..af56e6d160b57c 100644
--- a/static/app/views/detectors/list/myMonitors.tsx
+++ b/static/app/views/detectors/list/myMonitors.tsx
@@ -1,20 +1,31 @@
-import DetectorsList from 'sentry/views/detectors/list';
-import {
- MonitorViewContext,
- useMonitorViewContext,
-} from 'sentry/views/detectors/monitorViewContext';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
+import WorkflowEngineListLayout from 'sentry/components/workflowEngine/layout/list';
+import {t} from 'sentry/locale';
+import {DetectorListActions} from 'sentry/views/detectors/list/common/detectorListActions';
+import {DetectorListContent} from 'sentry/views/detectors/list/common/detectorListContent';
+import {DetectorListHeader} from 'sentry/views/detectors/list/common/detectorListHeader';
+import {useDetectorListQuery} from 'sentry/views/detectors/list/common/useDetectorListQuery';
+
+const TITLE = t('My Monitors');
+const DESCRIPTION = t('View monitors assigned to you or your teams.');
+const DOCS_URL = 'https://docs.sentry.io/product/monitors/';
export default function MyMonitorsList() {
- const parentContext = useMonitorViewContext();
+ const detectorListQuery = useDetectorListQuery({
+ assigneeFilter: '[me,my_teams]',
+ });
return (
-
-
-
+
+ }
+ title={TITLE}
+ description={DESCRIPTION}
+ docsUrl={DOCS_URL}
+ >
+
+
+
+
);
}
diff --git a/static/app/views/detectors/list/uptime.tsx b/static/app/views/detectors/list/uptime.tsx
index 231176f5c2766b..6d6462826b5717 100644
--- a/static/app/views/detectors/list/uptime.tsx
+++ b/static/app/views/detectors/list/uptime.tsx
@@ -1,4 +1,4 @@
-import {useCallback, useMemo, useRef} from 'react';
+import {useMemo, useRef} from 'react';
import styled from '@emotion/styled';
import {Flex} from '@sentry/scraps/layout';
@@ -6,16 +6,20 @@ import {Flex} from '@sentry/scraps/layout';
import {CheckInPlaceholder} from 'sentry/components/checkInTimeline/checkInPlaceholder';
import {CheckInTimeline} from 'sentry/components/checkInTimeline/checkInTimeline';
import {useTimeWindowConfig} from 'sentry/components/checkInTimeline/hooks/useTimeWindowConfig';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
import {SimpleTable} from 'sentry/components/tables/simpleTable';
+import WorkflowEngineListLayout from 'sentry/components/workflowEngine/layout/list';
+import {t} from 'sentry/locale';
import type {UptimeDetector} from 'sentry/types/workflowEngine/detectors';
import {useDebouncedValue} from 'sentry/utils/useDebouncedValue';
import {useDimensions} from 'sentry/utils/useDimensions';
-import DetectorsList from 'sentry/views/detectors/list';
+import {DetectorListActions} from 'sentry/views/detectors/list/common/detectorListActions';
+import {DetectorListContent} from 'sentry/views/detectors/list/common/detectorListContent';
+import {DetectorListHeader} from 'sentry/views/detectors/list/common/detectorListHeader';
+import {useDetectorListQuery} from 'sentry/views/detectors/list/common/useDetectorListQuery';
import {
MonitorViewContext,
- useMonitorViewContext,
type MonitorViewContextValue,
- type RenderVisualizationParams,
} from 'sentry/views/detectors/monitorViewContext';
import {
checkStatusPrecedent,
@@ -61,41 +65,54 @@ function VisualizationCell({detector}: {detector: UptimeDetector}) {
);
}
-export default function UptimeDetectorsList() {
- const parentContext = useMonitorViewContext();
+const TITLE = t('Uptime Monitors');
+const DESCRIPTION = t(
+ 'Uptime monitors continuously track configured URLs, delivering alerts and insights to quickly identify downtime and troubleshoot issues.'
+);
+const DOCS_URL = 'https://docs.sentry.io/product/alerts/uptime-monitoring/';
- const renderVisualization = useCallback(({detector}: RenderVisualizationParams) => {
- if (!detector) {
- return (
-
-
- |
- );
- }
- if (detector.type === 'uptime_domain_failure') {
- return ;
- }
- return null;
- }, []);
+export default function UptimeDetectorsList() {
+ const detectorListQuery = useDetectorListQuery({
+ detectorFilter: 'uptime_domain_failure',
+ });
const contextValue = useMemo(
() => ({
- ...parentContext,
- detectorFilter: 'uptime_domain_failure',
- renderVisualization,
- showTimeRangeSelector: true,
+ renderVisualization: ({detector}) => {
+ if (!detector) {
+ return (
+
+
+ |
+ );
+ }
+ if (detector.type === 'uptime_domain_failure') {
+ return ;
+ }
+ return null;
+ },
}),
- [parentContext, renderVisualization]
+ []
);
return (
-
+
+ }
+ title={TITLE}
+ description={DESCRIPTION}
+ docsUrl={DOCS_URL}
+ >
+
+
+
+
);
}
diff --git a/static/app/views/detectors/monitorViewContext.tsx b/static/app/views/detectors/monitorViewContext.tsx
index 3e14418bf8f5cf..0f6c5362afd2f8 100644
--- a/static/app/views/detectors/monitorViewContext.tsx
+++ b/static/app/views/detectors/monitorViewContext.tsx
@@ -1,6 +1,6 @@
import {createContext, useContext} from 'react';
-import type {Detector, DetectorType} from 'sentry/types/workflowEngine/detectors';
+import type {Detector} from 'sentry/types/workflowEngine/detectors';
export interface MonitorListAdditionalColumn {
id: string;
@@ -11,7 +11,7 @@ export interface MonitorListAdditionalColumn {
renderPendingCell?: () => React.ReactNode;
}
-export interface RenderVisualizationParams {
+interface RenderVisualizationParams {
detector: Detector | null;
}
@@ -21,20 +21,10 @@ export interface MonitorViewContextValue {
* These appear to the right of the default columns and to the left of the visualization.
*/
additionalColumns?: MonitorListAdditionalColumn[];
- assigneeFilter?: string;
- detectorFilter?: Exclude;
- emptyState?: React.ReactNode;
renderVisualization?: (params: RenderVisualizationParams) => React.ReactNode;
- showTimeRangeSelector?: boolean;
}
-const DEFAULT_MONITOR_VIEW_CONTEXT: MonitorViewContextValue = {
- assigneeFilter: undefined,
- detectorFilter: undefined,
- showTimeRangeSelector: false,
- emptyState: null,
- additionalColumns: [],
-};
+const DEFAULT_MONITOR_VIEW_CONTEXT: MonitorViewContextValue = {};
export const MonitorViewContext = createContext(
DEFAULT_MONITOR_VIEW_CONTEXT
diff --git a/static/app/views/detectors/routes.tsx b/static/app/views/detectors/routes.tsx
index 28a05afe3a88df..6c9bdb09939c17 100644
--- a/static/app/views/detectors/routes.tsx
+++ b/static/app/views/detectors/routes.tsx
@@ -6,7 +6,7 @@ export const detectorRoutes: SentryRouteObject = {
children: [
{
index: true,
- component: make(() => import('sentry/views/detectors/list')),
+ component: make(() => import('sentry/views/detectors/list/allMonitors')),
},
{
path: 'new',