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',