From 9a0ef4dd17a3f7af28607077665d5aba3783e16e Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Wed, 5 Nov 2025 16:29:32 -0800 Subject: [PATCH 1/7] ref(aci): Add common components for monitor list views --- static/app/views/detectors/list.tsx | 273 ++---------------- .../views/detectors/list/common/actions.tsx | 43 +++ .../views/detectors/list/common/constants.tsx | 1 + .../list/common/detectorListContent.tsx | 81 ++++++ .../views/detectors/list/common/header.tsx | 53 ++++ .../list/common/useDetectorListQuery.tsx | 42 +++ .../list/common/useDetectorListSort.tsx | 16 + static/app/views/detectors/list/cron.tsx | 91 +++--- static/app/views/detectors/list/error.tsx | 44 ++- static/app/views/detectors/list/metric.tsx | 44 ++- .../app/views/detectors/list/myMonitors.tsx | 42 ++- static/app/views/detectors/list/uptime.tsx | 80 +++-- .../views/detectors/monitorViewContext.tsx | 14 +- 13 files changed, 452 insertions(+), 372 deletions(-) create mode 100644 static/app/views/detectors/list/common/actions.tsx create mode 100644 static/app/views/detectors/list/common/constants.tsx create mode 100644 static/app/views/detectors/list/common/detectorListContent.tsx create mode 100644 static/app/views/detectors/list/common/header.tsx create mode 100644 static/app/views/detectors/list/common/useDetectorListQuery.tsx create mode 100644 static/app/views/detectors/list/common/useDetectorListSort.tsx diff --git a/static/app/views/detectors/list.tsx b/static/app/views/detectors/list.tsx index bdcdf001690710..4fe8ed10d5999d 100644 --- a/static/app/views/detectors/list.tsx +++ b/static/app/views/detectors/list.tsx @@ -1,270 +1,37 @@ -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 WorkflowEngineListLayout 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; -} +import {DetectorListActions} from 'sentry/views/detectors/list/common/actions'; +import {DetectorListContent} from 'sentry/views/detectors/list/common/detectorListContent'; +import {DetectorListHeader} from 'sentry/views/detectors/list/common/header'; +import {useDetectorListQuery} from 'sentry/views/detectors/list/common/useDetectorListQuery'; -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/', - }, -}; +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 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/', - }; + const detectorListQuery = useDetectorListQuery(); return ( - + - } - title={pageTitle} - description={pageDescription} - docsUrl={docsUrl} + } + title={TITLE} + description={DESCRIPTION} + docsUrl={DOCS_URL} > - -
- 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/common/actions.tsx b/static/app/views/detectors/list/common/actions.tsx new file mode 100644 index 00000000000000..49853eb97a9e09 --- /dev/null +++ b/static/app/views/detectors/list/common/actions.tsx @@ -0,0 +1,43 @@ +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; + detectorType?: DetectorType; +} + +export function DetectorListActions({detectorType, children}: DetectorListActionsProps) { + const organization = useOrganization(); + const {selection} = usePageFilters(); + + const createPath = makeMonitorCreatePathname(organization.slug); + // If detectorFilter is set, pass it as a query param to skip type selection + 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/constants.tsx b/static/app/views/detectors/list/common/constants.tsx new file mode 100644 index 00000000000000..4f55d18fe24ada --- /dev/null +++ b/static/app/views/detectors/list/common/constants.tsx @@ -0,0 +1 @@ +export const DETECTOR_LIST_PAGE_LIMIT = 20; 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/header.tsx b/static/app/views/detectors/list/common/header.tsx new file mode 100644 index 00000000000000..209672250522a2 --- /dev/null +++ b/static/app/views/detectors/list/common/header.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..0456173729174f 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,24 @@ 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 PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; +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/actions'; +import {DetectorListContent} from 'sentry/views/detectors/list/common/detectorListContent'; +import {DetectorListHeader} from 'sentry/views/detectors/list/common/header'; +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 +112,59 @@ 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..95876e5da65433 100644 --- a/static/app/views/detectors/list/error.tsx +++ b/static/app/views/detectors/list/error.tsx @@ -1,20 +1,36 @@ -import DetectorsList from 'sentry/views/detectors/list'; -import { - MonitorViewContext, - useMonitorViewContext, -} from 'sentry/views/detectors/monitorViewContext'; +import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; +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/actions'; +import {DetectorListContent} from 'sentry/views/detectors/list/common/detectorListContent'; +import {DetectorListHeader} from 'sentry/views/detectors/list/common/header'; +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..1a13971e984e0b 100644 --- a/static/app/views/detectors/list/metric.tsx +++ b/static/app/views/detectors/list/metric.tsx @@ -1,20 +1,36 @@ -import DetectorsList from 'sentry/views/detectors/list'; -import { - MonitorViewContext, - useMonitorViewContext, -} from 'sentry/views/detectors/monitorViewContext'; +import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; +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/actions'; +import {DetectorListContent} from 'sentry/views/detectors/list/common/detectorListContent'; +import {DetectorListHeader} from 'sentry/views/detectors/list/common/header'; +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..24736e9924ce87 100644 --- a/static/app/views/detectors/list/myMonitors.tsx +++ b/static/app/views/detectors/list/myMonitors.tsx @@ -1,20 +1,34 @@ -import DetectorsList from 'sentry/views/detectors/list'; -import { - MonitorViewContext, - useMonitorViewContext, -} from 'sentry/views/detectors/monitorViewContext'; +import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; +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/actions'; +import {DetectorListContent} from 'sentry/views/detectors/list/common/detectorListContent'; +import {DetectorListHeader} from 'sentry/views/detectors/list/common/header'; +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..904647511bdb84 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,21 @@ 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 PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; +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/actions'; +import {DetectorListContent} from 'sentry/views/detectors/list/common/detectorListContent'; +import {DetectorListHeader} from 'sentry/views/detectors/list/common/header'; +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 +66,56 @@ 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..8d24988c11a30f 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; @@ -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 From 17d53c3970fc3820a70e00844a610b5647b4f42b Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Wed, 5 Nov 2025 16:41:24 -0800 Subject: [PATCH 2/7] Rename some files --- static/app/views/detectors/list.tsx | 4 ++-- .../list/common/{actions.tsx => detectorListActions.tsx} | 4 +++- .../list/common/{header.tsx => detectorListHeader.tsx} | 0 static/app/views/detectors/list/cron.tsx | 4 ++-- static/app/views/detectors/list/error.tsx | 4 ++-- static/app/views/detectors/list/metric.tsx | 4 ++-- static/app/views/detectors/list/myMonitors.tsx | 4 ++-- static/app/views/detectors/list/uptime.tsx | 4 ++-- 8 files changed, 15 insertions(+), 13 deletions(-) rename static/app/views/detectors/list/common/{actions.tsx => detectorListActions.tsx} (94%) rename static/app/views/detectors/list/common/{header.tsx => detectorListHeader.tsx} (100%) diff --git a/static/app/views/detectors/list.tsx b/static/app/views/detectors/list.tsx index 4fe8ed10d5999d..f88c1a965ef2a0 100644 --- a/static/app/views/detectors/list.tsx +++ b/static/app/views/detectors/list.tsx @@ -3,9 +3,9 @@ import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import WorkflowEngineListLayout from 'sentry/components/workflowEngine/layout/list'; import {useWorkflowEngineFeatureGate} from 'sentry/components/workflowEngine/useWorkflowEngineFeatureGate'; import {t} from 'sentry/locale'; -import {DetectorListActions} from 'sentry/views/detectors/list/common/actions'; +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/header'; +import {DetectorListHeader} from 'sentry/views/detectors/list/common/detectorListHeader'; import {useDetectorListQuery} from 'sentry/views/detectors/list/common/useDetectorListQuery'; const TITLE = t('Monitors'); diff --git a/static/app/views/detectors/list/common/actions.tsx b/static/app/views/detectors/list/common/detectorListActions.tsx similarity index 94% rename from static/app/views/detectors/list/common/actions.tsx rename to static/app/views/detectors/list/common/detectorListActions.tsx index 49853eb97a9e09..68e6becc922ba7 100644 --- a/static/app/views/detectors/list/common/actions.tsx +++ b/static/app/views/detectors/list/common/detectorListActions.tsx @@ -11,6 +11,9 @@ 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; } @@ -19,7 +22,6 @@ export function DetectorListActions({detectorType, children}: DetectorListAction const {selection} = usePageFilters(); const createPath = makeMonitorCreatePathname(organization.slug); - // If detectorFilter is set, pass it as a query param to skip type selection const project = selection.projects.find(pid => pid !== ALL_ACCESS_PROJECTS); const createQuery = detectorType ? {project, detectorType} : {project}; diff --git a/static/app/views/detectors/list/common/header.tsx b/static/app/views/detectors/list/common/detectorListHeader.tsx similarity index 100% rename from static/app/views/detectors/list/common/header.tsx rename to static/app/views/detectors/list/common/detectorListHeader.tsx diff --git a/static/app/views/detectors/list/cron.tsx b/static/app/views/detectors/list/cron.tsx index 0456173729174f..dfbc3c6a376358 100644 --- a/static/app/views/detectors/list/cron.tsx +++ b/static/app/views/detectors/list/cron.tsx @@ -17,9 +17,9 @@ 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 {DetectorListActions} from 'sentry/views/detectors/list/common/actions'; +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/header'; +import {DetectorListHeader} from 'sentry/views/detectors/list/common/detectorListHeader'; import {useDetectorListQuery} from 'sentry/views/detectors/list/common/useDetectorListQuery'; import { MonitorViewContext, diff --git a/static/app/views/detectors/list/error.tsx b/static/app/views/detectors/list/error.tsx index 95876e5da65433..9d14b0a76ba4b5 100644 --- a/static/app/views/detectors/list/error.tsx +++ b/static/app/views/detectors/list/error.tsx @@ -2,9 +2,9 @@ import PageFiltersContainer from 'sentry/components/organizations/pageFilters/co 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/actions'; +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/header'; +import {DetectorListHeader} from 'sentry/views/detectors/list/common/detectorListHeader'; import {useDetectorListQuery} from 'sentry/views/detectors/list/common/useDetectorListQuery'; const TITLE = t('Error Monitors'); diff --git a/static/app/views/detectors/list/metric.tsx b/static/app/views/detectors/list/metric.tsx index 1a13971e984e0b..41b8b988d19492 100644 --- a/static/app/views/detectors/list/metric.tsx +++ b/static/app/views/detectors/list/metric.tsx @@ -2,9 +2,9 @@ import PageFiltersContainer from 'sentry/components/organizations/pageFilters/co 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/actions'; +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/header'; +import {DetectorListHeader} from 'sentry/views/detectors/list/common/detectorListHeader'; import {useDetectorListQuery} from 'sentry/views/detectors/list/common/useDetectorListQuery'; const TITLE = t('Metric Monitors'); diff --git a/static/app/views/detectors/list/myMonitors.tsx b/static/app/views/detectors/list/myMonitors.tsx index 24736e9924ce87..2ea91872f065ab 100644 --- a/static/app/views/detectors/list/myMonitors.tsx +++ b/static/app/views/detectors/list/myMonitors.tsx @@ -2,9 +2,9 @@ import PageFiltersContainer from 'sentry/components/organizations/pageFilters/co 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/actions'; +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/header'; +import {DetectorListHeader} from 'sentry/views/detectors/list/common/detectorListHeader'; import {useDetectorListQuery} from 'sentry/views/detectors/list/common/useDetectorListQuery'; const TITLE = t('My Monitors'); diff --git a/static/app/views/detectors/list/uptime.tsx b/static/app/views/detectors/list/uptime.tsx index 904647511bdb84..f567c006d77b28 100644 --- a/static/app/views/detectors/list/uptime.tsx +++ b/static/app/views/detectors/list/uptime.tsx @@ -14,9 +14,9 @@ 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 {DetectorListActions} from 'sentry/views/detectors/list/common/actions'; +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/header'; +import {DetectorListHeader} from 'sentry/views/detectors/list/common/detectorListHeader'; import {useDetectorListQuery} from 'sentry/views/detectors/list/common/useDetectorListQuery'; import { MonitorViewContext, From 0b02e3ad836ce3395b65ac370a46fbc6a639bff0 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Wed, 5 Nov 2025 16:43:34 -0800 Subject: [PATCH 3/7] Hide type from search correctly --- static/app/views/detectors/list/cron.tsx | 2 +- static/app/views/detectors/list/error.tsx | 2 +- static/app/views/detectors/list/metric.tsx | 2 +- static/app/views/detectors/list/uptime.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/static/app/views/detectors/list/cron.tsx b/static/app/views/detectors/list/cron.tsx index dfbc3c6a376358..b7c9a316d9aa4b 100644 --- a/static/app/views/detectors/list/cron.tsx +++ b/static/app/views/detectors/list/cron.tsx @@ -157,7 +157,7 @@ export default function CronDetectorsList() { description={DESCRIPTION} docsUrl={DOCS_URL} > - + } diff --git a/static/app/views/detectors/list/error.tsx b/static/app/views/detectors/list/error.tsx index 9d14b0a76ba4b5..f7a5abf9c3e5de 100644 --- a/static/app/views/detectors/list/error.tsx +++ b/static/app/views/detectors/list/error.tsx @@ -27,7 +27,7 @@ export default function ErrorDetectorsList() { description={DESCRIPTION} docsUrl={DOCS_URL} > - + diff --git a/static/app/views/detectors/list/metric.tsx b/static/app/views/detectors/list/metric.tsx index 41b8b988d19492..bafbfe70651edd 100644 --- a/static/app/views/detectors/list/metric.tsx +++ b/static/app/views/detectors/list/metric.tsx @@ -27,7 +27,7 @@ export default function MetricDetectorsList() { description={DESCRIPTION} docsUrl={DOCS_URL} > - + diff --git a/static/app/views/detectors/list/uptime.tsx b/static/app/views/detectors/list/uptime.tsx index f567c006d77b28..0ac574550835a1 100644 --- a/static/app/views/detectors/list/uptime.tsx +++ b/static/app/views/detectors/list/uptime.tsx @@ -111,7 +111,7 @@ export default function UptimeDetectorsList() { description={DESCRIPTION} docsUrl={DOCS_URL} > - + From be556661583a778fb0d7616a961c3d578fbe1c7e Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Wed, 5 Nov 2025 16:49:55 -0800 Subject: [PATCH 4/7] Add top-level page container for page filters and redirect --- static/app/routes.tsx | 1 + .../views/detectors/detectorViewContainer.tsx | 14 ++++++++++ static/app/views/detectors/list.tsx | 24 +++++++---------- static/app/views/detectors/list/cron.tsx | 27 +++++++++---------- static/app/views/detectors/list/error.tsx | 21 +++++++-------- static/app/views/detectors/list/metric.tsx | 21 +++++++-------- .../app/views/detectors/list/myMonitors.tsx | 21 +++++++-------- static/app/views/detectors/list/uptime.tsx | 21 +++++++-------- .../views/detectors/monitorViewContext.tsx | 2 +- 9 files changed, 73 insertions(+), 79 deletions(-) create mode 100644 static/app/views/detectors/detectorViewContainer.tsx diff --git a/static/app/routes.tsx b/static/app/routes.tsx index cb153d90604c9a..bb6843351e8fdc 100644 --- a/static/app/routes.tsx +++ b/static/app/routes.tsx @@ -1674,6 +1674,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/detectors/detectorViewContainer.tsx b/static/app/views/detectors/detectorViewContainer.tsx new file mode 100644 index 00000000000000..3f320f260f444b --- /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 MonitorViewContainer() { + useWorkflowEngineFeatureGate({redirect: true}); + + return ( + + + + ); +} diff --git a/static/app/views/detectors/list.tsx b/static/app/views/detectors/list.tsx index f88c1a965ef2a0..454a97b744351f 100644 --- a/static/app/views/detectors/list.tsx +++ b/static/app/views/detectors/list.tsx @@ -1,7 +1,5 @@ -import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import WorkflowEngineListLayout from 'sentry/components/workflowEngine/layout/list'; -import {useWorkflowEngineFeatureGate} from 'sentry/components/workflowEngine/useWorkflowEngineFeatureGate'; import {t} from 'sentry/locale'; import {DetectorListActions} from 'sentry/views/detectors/list/common/detectorListActions'; import {DetectorListContent} from 'sentry/views/detectors/list/common/detectorListContent'; @@ -15,23 +13,19 @@ const DESCRIPTION = t( const DOCS_URL = 'https://docs.sentry.io/product/monitors/'; export default function DetectorsList() { - useWorkflowEngineFeatureGate({redirect: true}); - const detectorListQuery = useDetectorListQuery(); return ( - - } - title={TITLE} - description={DESCRIPTION} - docsUrl={DOCS_URL} - > - - - - + } + title={TITLE} + description={DESCRIPTION} + docsUrl={DOCS_URL} + > + + + ); } diff --git a/static/app/views/detectors/list/cron.tsx b/static/app/views/detectors/list/cron.tsx index b7c9a316d9aa4b..74c2ed986b48ad 100644 --- a/static/app/views/detectors/list/cron.tsx +++ b/static/app/views/detectors/list/cron.tsx @@ -7,7 +7,6 @@ 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 PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; import WorkflowEngineListLayout from 'sentry/components/workflowEngine/layout/list'; @@ -150,20 +149,18 @@ export default function CronDetectorsList() { return ( - - } - title={TITLE} - description={DESCRIPTION} - docsUrl={DOCS_URL} - > - - } - /> - - + } + 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 f7a5abf9c3e5de..fee209711886b7 100644 --- a/static/app/views/detectors/list/error.tsx +++ b/static/app/views/detectors/list/error.tsx @@ -1,4 +1,3 @@ -import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import WorkflowEngineListLayout from 'sentry/components/workflowEngine/layout/list'; import {t} from 'sentry/locale'; @@ -20,17 +19,15 @@ export default function ErrorDetectorsList() { return ( - - } - title={TITLE} - description={DESCRIPTION} - docsUrl={DOCS_URL} - > - - - - + } + 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 bafbfe70651edd..25313c3a498648 100644 --- a/static/app/views/detectors/list/metric.tsx +++ b/static/app/views/detectors/list/metric.tsx @@ -1,4 +1,3 @@ -import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import WorkflowEngineListLayout from 'sentry/components/workflowEngine/layout/list'; import {t} from 'sentry/locale'; @@ -20,17 +19,15 @@ export default function MetricDetectorsList() { return ( - - } - title={TITLE} - description={DESCRIPTION} - docsUrl={DOCS_URL} - > - - - - + } + 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 2ea91872f065ab..af56e6d160b57c 100644 --- a/static/app/views/detectors/list/myMonitors.tsx +++ b/static/app/views/detectors/list/myMonitors.tsx @@ -1,4 +1,3 @@ -import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import WorkflowEngineListLayout from 'sentry/components/workflowEngine/layout/list'; import {t} from 'sentry/locale'; @@ -18,17 +17,15 @@ export default function MyMonitorsList() { return ( - - } - title={TITLE} - description={DESCRIPTION} - docsUrl={DOCS_URL} - > - - - - + } + 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 0ac574550835a1..6d6462826b5717 100644 --- a/static/app/views/detectors/list/uptime.tsx +++ b/static/app/views/detectors/list/uptime.tsx @@ -6,7 +6,6 @@ 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 PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle'; import {SimpleTable} from 'sentry/components/tables/simpleTable'; import WorkflowEngineListLayout from 'sentry/components/workflowEngine/layout/list'; @@ -104,17 +103,15 @@ export default function UptimeDetectorsList() { return ( - - } - title={TITLE} - description={DESCRIPTION} - docsUrl={DOCS_URL} - > - - - - + } + title={TITLE} + description={DESCRIPTION} + docsUrl={DOCS_URL} + > + + + ); diff --git a/static/app/views/detectors/monitorViewContext.tsx b/static/app/views/detectors/monitorViewContext.tsx index 8d24988c11a30f..0f6c5362afd2f8 100644 --- a/static/app/views/detectors/monitorViewContext.tsx +++ b/static/app/views/detectors/monitorViewContext.tsx @@ -11,7 +11,7 @@ export interface MonitorListAdditionalColumn { renderPendingCell?: () => React.ReactNode; } -export interface RenderVisualizationParams { +interface RenderVisualizationParams { detector: Detector | null; } From 39fb2c86af73be82852f81b5c07496c38d566f1f Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Wed, 5 Nov 2025 16:51:18 -0800 Subject: [PATCH 5/7] Move list.tsx to list/allMonitors.tsx --- .../allMonitors.spec.tsx} | 26 +++++++++---------- .../{list.tsx => list/allMonitors.tsx} | 2 +- static/app/views/detectors/routes.tsx | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) rename static/app/views/detectors/{list.spec.tsx => list/allMonitors.spec.tsx} (96%) rename static/app/views/detectors/{list.tsx => list/allMonitors.tsx} (96%) 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.tsx b/static/app/views/detectors/list/allMonitors.tsx similarity index 96% rename from static/app/views/detectors/list.tsx rename to static/app/views/detectors/list/allMonitors.tsx index 454a97b744351f..0cfe3dde47f3ee 100644 --- a/static/app/views/detectors/list.tsx +++ b/static/app/views/detectors/list/allMonitors.tsx @@ -12,7 +12,7 @@ const DESCRIPTION = t( ); const DOCS_URL = 'https://docs.sentry.io/product/monitors/'; -export default function DetectorsList() { +export default function AllMonitors() { const detectorListQuery = useDetectorListQuery(); return ( diff --git a/static/app/views/detectors/routes.tsx b/static/app/views/detectors/routes.tsx index b79a98b158db2e..94f6867794e54c 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', From b81b5aae8ed7e820cef23bad2ff6ed9a722e5886 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Thu, 6 Nov 2025 09:52:32 -0800 Subject: [PATCH 6/7] Remove old constants file and remove duplicate page filters container component --- static/app/views/automations/list.tsx | 73 +++++++++---------- .../components/detectorListTable/index.tsx | 2 +- static/app/views/detectors/constants.tsx | 1 - 3 files changed, 36 insertions(+), 40 deletions(-) delete mode 100644 static/app/views/detectors/constants.tsx 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/constants.tsx b/static/app/views/detectors/constants.tsx deleted file mode 100644 index 4f55d18fe24ada..00000000000000 --- a/static/app/views/detectors/constants.tsx +++ /dev/null @@ -1 +0,0 @@ -export const DETECTOR_LIST_PAGE_LIMIT = 20; From 7d2e2d76e9c015ae415e4727df14debd17bcf302 Mon Sep 17 00:00:00 2001 From: Malachi Willey Date: Thu, 6 Nov 2025 10:15:11 -0800 Subject: [PATCH 7/7] Fix test --- static/app/views/automations/list.spec.tsx | 9 ++++++++- static/app/views/detectors/detectorViewContainer.tsx | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) 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/detectors/detectorViewContainer.tsx b/static/app/views/detectors/detectorViewContainer.tsx index 3f320f260f444b..82e2daf9d71387 100644 --- a/static/app/views/detectors/detectorViewContainer.tsx +++ b/static/app/views/detectors/detectorViewContainer.tsx @@ -3,7 +3,7 @@ import {Outlet} from 'react-router-dom'; import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; import {useWorkflowEngineFeatureGate} from 'sentry/components/workflowEngine/useWorkflowEngineFeatureGate'; -export default function MonitorViewContainer() { +export default function DetectorViewContainer() { useWorkflowEngineFeatureGate({redirect: true}); return (