diff --git a/web/cypress/fixtures/monitoring/constants.ts b/web/cypress/fixtures/monitoring/constants.ts index 816a0b3a0..c338a9e54 100644 --- a/web/cypress/fixtures/monitoring/constants.ts +++ b/web/cypress/fixtures/monitoring/constants.ts @@ -165,6 +165,7 @@ export enum MetricsPageQueryKebabDropdown { DELETE_QUERY = 'Delete query', DUPLICATE_QUERY = 'Duplicate query', EXPORT_AS_CSV = 'Export as CSV', + CREATE_ALERT = 'Create alert', } export enum LegacyDashboardsTimeRange { diff --git a/web/cypress/support/monitoring/02.reg_metrics_1.cy.ts b/web/cypress/support/monitoring/02.reg_metrics_1.cy.ts index e8cfce3a7..3888aab19 100644 --- a/web/cypress/support/monitoring/02.reg_metrics_1.cy.ts +++ b/web/cypress/support/monitoring/02.reg_metrics_1.cy.ts @@ -153,6 +153,15 @@ export function testMetricsRegression1(perspective: PerspectiveConfig) { metricsPage.shouldBeLoaded(); }); + it(`${perspective.name} perspective - Metrics > Kebab > Create alert`, () => { + cy.log('3b.1 Load a predefined query'); + metricsPage.clickPredefinedQuery(MetricsPagePredefinedQueries.FILESYSTEM_USAGE); + metricsPage.shouldBeLoadedWithGraph(); + + cy.log('3b.2 Create alert kebab item renders and is clickable'); + metricsPage.createAlertKebabItemAssertion(0); + }); + it(`${perspective.name} perspective - Metrics > Insert Example Query`, () => { cy.log('4.1 Insert Example Query'); metricsPage.clickInsertExampleQuery(); diff --git a/web/cypress/views/metrics.ts b/web/cypress/views/metrics.ts index e16de0598..914edb667 100644 --- a/web/cypress/views/metrics.ts +++ b/web/cypress/views/metrics.ts @@ -553,6 +553,9 @@ export const metricsPage = { cy.byTestID(DataTestIDs.MetricsPageDuplicateQueryDropdownItem) .contains(MetricsPageQueryKebabDropdown.DUPLICATE_QUERY) .should('be.visible'); + cy.byTestID(DataTestIDs.MetricsPageCreateAlertRuleDropdownItem) + .contains(MetricsPageQueryKebabDropdown.CREATE_ALERT) + .should('be.visible'); cy.byTestID(DataTestIDs.MetricsPageExportCsvDropdownItem).should('not.exist'); cy.byTestID(DataTestIDs.KebabDropdownButton) @@ -579,12 +582,26 @@ export const metricsPage = { cy.byTestID(DataTestIDs.MetricsPageExportCsvDropdownItem) .contains(MetricsPageQueryKebabDropdown.EXPORT_AS_CSV) .should('be.visible'); + cy.byTestID(DataTestIDs.MetricsPageCreateAlertRuleDropdownItem) + .contains(MetricsPageQueryKebabDropdown.CREATE_ALERT) + .should('be.visible'); cy.byTestID(DataTestIDs.KebabDropdownButton) .eq(0) .should('have.attr', 'aria-expanded', 'true') .click(); }, + createAlertKebabItemAssertion: (index: number) => { + cy.log('metricsPage.createAlertKebabItemAssertion'); + metricsPage.clickKebabDropdown(index); + cy.byTestID(DataTestIDs.MetricsPageCreateAlertRuleDropdownItem) + .contains(MetricsPageQueryKebabDropdown.CREATE_ALERT) + .should('be.visible') + .and('not.have.attr', 'aria-disabled', 'true') + .click(); + cy.url().should('include', '/v2/alertrule/create?query='); + }, + clickKebabDropdownItem: (option: MetricsPageQueryKebabDropdown, index: number) => { cy.log('metricsPage.clickKebabDropdownItem'); metricsPage.clickKebabDropdown(index); diff --git a/web/locales/en/plugin__monitoring-plugin.json b/web/locales/en/plugin__monitoring-plugin.json index 49b7a3101..69e075138 100644 --- a/web/locales/en/plugin__monitoring-plugin.json +++ b/web/locales/en/plugin__monitoring-plugin.json @@ -323,6 +323,7 @@ "Query must be enabled": "Query must be enabled", "Delete query": "Delete query", "Duplicate query": "Duplicate query", + "Create alert": "Create alert", "Error loading values": "Error loading values", "Unselect all": "Unselect all", "Select all": "Select all", diff --git a/web/src/features/metrics/pages/MetricsPage.tsx b/web/src/features/metrics/pages/MetricsPage.tsx index 8f799e1e0..0825eb01d 100644 --- a/web/src/features/metrics/pages/MetricsPage.tsx +++ b/web/src/features/metrics/pages/MetricsPage.tsx @@ -76,7 +76,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { FC, MouseEvent as ReactMouseEvent, Ref } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; -import { useSearchParams } from 'react-router'; +import { useNavigate, useSearchParams } from 'react-router'; import { StringParam, useQueryParam } from 'use-query-params'; import { DropDownPollInterval } from '@/shared/components/DropdownPollInterval'; @@ -97,7 +97,11 @@ import { MonitoringProvider } from '@/shared/contexts/MonitoringContext'; import { useBoolean } from '@/shared/hooks/useBoolean'; import { useMonitoring } from '@/shared/hooks/useMonitoring'; import { useMonitoringNamespace } from '@/shared/hooks/useMonitoringNamespace'; -import { getObserveState } from '@/shared/hooks/usePerspective'; +import { + getCreateAlertRuleUrl, + getObserveState, + usePerspective, +} from '@/shared/hooks/usePerspective'; import { queryBrowserAddQuery, queryBrowserDeleteAllQueries, @@ -385,6 +389,9 @@ const SeriesButton: FC = ({ index, labels }) => { const QueryKebab: FC<{ index: number }> = ({ index }) => { const { t } = useTranslation(process.env.I18N_NAMESPACE); const { plugin } = useMonitoring(); + const { perspective } = usePerspective(); + const navigate = useNavigate(); + const [activeNamespace] = useActiveNamespace(); const isDisabledSeriesEmpty = useSelector((state: MonitoringState) => _.isEmpty(getObserveState(plugin, state).queryBrowser?.queries[index]?.disabledSeries), @@ -427,6 +434,10 @@ const QueryKebab: FC<{ index: number }> = ({ index }) => { dispatch(queryBrowserDuplicateQuery(index)); }, [dispatch, index]); + const doCreateAlert = useCallback(() => { + navigate(getCreateAlertRuleUrl(perspective, query ?? '', activeNamespace)); + }, [navigate, perspective, query, activeNamespace]); + const isSpan = (item) => item?.title?.props?.children; const getSpanText = (item) => item.title.props.children; @@ -549,6 +560,14 @@ const QueryKebab: FC<{ index: number }> = ({ index }) => { > {t('Duplicate query')} , + + {t('Create alert')} + , ]; const hasQueryTableData = () => { diff --git a/web/src/shared/constants/data-test.ts b/web/src/shared/constants/data-test.ts index 5c8704c34..74f14e9f3 100644 --- a/web/src/shared/constants/data-test.ts +++ b/web/src/shared/constants/data-test.ts @@ -37,6 +37,7 @@ export const DataTestIDs = { MetricsPageActionsDropdownButton: 'actions-dropdown-button', MetricsPageAddQueryButton: 'add-query-button', MetricsPageAddQueryDropdownItem: 'add-query-dropdown-item', + MetricsPageCreateAlertRuleDropdownItem: 'create-alert-rule-dropdown-item', MetricsPageDeleteAllQueriesDropdownItem: 'delete-all-queries-dropdown-item', MetricsPageDeleteQueryDropdownItem: 'delete-query-dropdown-item', MetricsPageDisableEnableQuerySwitch: 'disable-enable-query-switch', diff --git a/web/src/shared/constants/query-params.ts b/web/src/shared/constants/query-params.ts index e8cabb172..cae4eb88c 100644 --- a/web/src/shared/constants/query-params.ts +++ b/web/src/shared/constants/query-params.ts @@ -13,4 +13,5 @@ export enum QueryParams { Refresh = 'refresh', Start = 'start', Edit = 'edit', + Query = 'query', } diff --git a/web/src/shared/hooks/get-create-alert-rule-url.spec.ts b/web/src/shared/hooks/get-create-alert-rule-url.spec.ts new file mode 100644 index 000000000..85b81d580 --- /dev/null +++ b/web/src/shared/hooks/get-create-alert-rule-url.spec.ts @@ -0,0 +1,50 @@ +jest.mock('@openshift-console/dynamic-plugin-sdk', () => ({ + ...jest.requireActual('@openshift-console/dynamic-plugin-sdk/lib/api/common-types'), +})); + +import { getCreateAlertRuleUrl } from '@/shared/hooks/usePerspective'; + +describe('getCreateAlertRuleUrl', () => { + const query = 'up{job="prometheus"}'; + const encodedQuery = 'query=up%7Bjob%3D%22prometheus%22%7D'; + + it('builds the admin url', () => { + expect(getCreateAlertRuleUrl('admin', query)).toBe( + `/monitoring/v2/alertrule/create?${encodedQuery}`, + ); + }); + + it('builds the virtualization url', () => { + expect(getCreateAlertRuleUrl('virtualization-perspective', query)).toBe( + `/virt-monitoring/v2/alertrule/create?${encodedQuery}`, + ); + }); + + it('builds the acm url', () => { + expect(getCreateAlertRuleUrl('acm', query)).toBe( + `/multicloud/monitoring/v2/alertrule/create?${encodedQuery}`, + ); + }); + + it('builds the dev url with the namespace in the path', () => { + expect(getCreateAlertRuleUrl('dev', query, 'my-project')).toBe( + `/dev-monitoring/ns/my-project/v2/alertrule/create?${encodedQuery}`, + ); + }); + + it('falls back to the admin url for an unknown perspective', () => { + expect(getCreateAlertRuleUrl('unknown' as never, query)).toBe( + `/monitoring/v2/alertrule/create?${encodedQuery}`, + ); + }); + + it('defaults to an empty query when none is provided', () => { + expect(getCreateAlertRuleUrl('admin')).toBe('/monitoring/v2/alertrule/create?query='); + }); + + it('url-encodes the query parameter', () => { + expect(getCreateAlertRuleUrl('admin', 'a b&c=d')).toBe( + '/monitoring/v2/alertrule/create?query=a+b%26c%3Dd', + ); + }); +}); diff --git a/web/src/shared/hooks/usePerspective.tsx b/web/src/shared/hooks/usePerspective.tsx index 00080e674..857bf464c 100644 --- a/web/src/shared/hooks/usePerspective.tsx +++ b/web/src/shared/hooks/usePerspective.tsx @@ -319,3 +319,21 @@ export const getDashboardsListUrl = (perspective: Perspective) => { return ''; } }; + +// TODO: The dev and acm routes below are best-guesses based on the existing perspective URL +// conventions. Confirm them with the new alert management UI once its routing is finalized. +export const getCreateAlertRuleUrl = (perspective: Perspective, query = '', namespace?: string) => { + const params = new URLSearchParams({ [QueryParams.Query]: query }); + + switch (perspective) { + case 'acm': + return `/multicloud/monitoring/v2/alertrule/create?${params.toString()}`; + case 'dev': + return `/dev-monitoring/ns/${namespace}/v2/alertrule/create?${params.toString()}`; + case 'virtualization-perspective': + return `/virt-monitoring/v2/alertrule/create?${params.toString()}`; + case 'admin': + default: + return `/monitoring/v2/alertrule/create?${params.toString()}`; + } +};