diff --git a/x-pack/solutions/observability/packages/kbn-observability-schema/related_dashboards/schema/related_dashboard/v1.ts b/x-pack/solutions/observability/packages/kbn-observability-schema/related_dashboards/schema/related_dashboard/v1.ts index e26f7ebf65ba6..7b0f9033b1a73 100644 --- a/x-pack/solutions/observability/packages/kbn-observability-schema/related_dashboards/schema/related_dashboard/v1.ts +++ b/x-pack/solutions/observability/packages/kbn-observability-schema/related_dashboards/schema/related_dashboard/v1.ts @@ -11,6 +11,7 @@ import { relevantPanelSchema } from '../relevant_panel/latest'; export const relatedDashboardSchema = z.object({ id: z.string(), title: z.string(), + description: z.string(), matchedBy: z.object({ fields: z.array(z.string()).optional(), index: z.array(z.string()).optional(), @@ -23,6 +24,7 @@ export const relatedDashboardSchema = z.object({ export const suggestedDashboardSchema = z.object({ id: z.string(), title: z.string(), + description: z.string(), matchedBy: z.object({ fields: z.array(z.string()).optional(), index: z.array(z.string()).optional(), diff --git a/x-pack/solutions/observability/plugins/observability/common/constants.ts b/x-pack/solutions/observability/plugins/observability/common/constants.ts index 785f1caf4cb6d..8bc12f8737bfe 100644 --- a/x-pack/solutions/observability/plugins/observability/common/constants.ts +++ b/x-pack/solutions/observability/plugins/observability/common/constants.ts @@ -39,3 +39,7 @@ export const OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES = [ ...OBSERVABILITY_RULE_TYPE_IDS, ...STACK_RULE_TYPE_IDS_SUPPORTED_BY_OBSERVABILITY, ]; + +export enum ALERTS_API_URLS { + INTERNAL_RELATED_DASHBOARDS = '/internal/observability/alerts/related_dashboards', +} diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.test.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.test.tsx index 2980187723a4e..7e253735d3927 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.test.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.test.tsx @@ -13,7 +13,6 @@ import { observabilityAIAssistantPluginMock } from '@kbn/observability-ai-assist import { useBreadcrumbs, TagsList } from '@kbn/observability-shared-plugin/public'; import { RuleTypeModel, ValidationResult } from '@kbn/triggers-actions-ui-plugin/public'; import { ruleTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/rule_type_registry.mock'; -import { dashboardServiceProvider } from '@kbn/response-ops-rule-form/src/common'; import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Chance } from 'chance'; @@ -49,12 +48,36 @@ const ruleType: RuleTypeModel = { ruleParamsExpression: () => , alertDetailsAppSection: () => , }; + +jest.mock('./hooks/use_add_suggested_dashboard', () => ({ + useAddSuggestedDashboards: () => ({ + onClickAddSuggestedDashboard: jest.fn(), + addingDashboardId: undefined, + }), +})); + +jest.mock('./hooks/use_related_dashboards', () => ({ + useRelatedDashboards: () => ({ + isLoadingSuggestedDashboards: false, + suggestedDashboards: [ + { + id: 'suggested-dashboard-1', + title: 'Suggested Dashboard 1', + description: 'A suggested dashboard for testing', + }, + ], + linkedDashboards: [ + { + id: 'dashboard-1', + }, + ], + }), +})); + const ruleTypeRegistry = ruleTypeRegistryMock.create(); const useKibanaMock = useKibana as jest.Mock; -const dashboardServiceProviderMock = dashboardServiceProvider as jest.Mock; - const mockObservabilityAIAssistant = observabilityAIAssistantPluginMock.createStartContract(); const mockKibana = () => { @@ -76,26 +99,30 @@ const mockKibana = () => { }); }; +const MOCK_RULE_TYPE_ID = 'observability.rules.custom_threshold'; + +const MOCK_RULE = { + id: 'ruleId', + name: 'ruleName', + ruleTypeId: MOCK_RULE_TYPE_ID, + consumer: 'logs', + artifacts: { + dashboards: [ + { + id: 'dashboard-1', + }, + { + id: 'dashboard-2', + }, + ], + }, +}; jest.mock('../../hooks/use_fetch_alert_detail'); jest.mock('../../hooks/use_fetch_rule', () => { return { useFetchRule: () => ({ reloadRule: jest.fn(), - rule: { - id: 'ruleId', - name: 'ruleName', - consumer: 'logs', - artifacts: { - dashboards: [ - { - id: 'dashboard-1', - }, - { - id: 'dashboard-2', - }, - ], - }, - }, + rule: MOCK_RULE, }), }; }); @@ -112,14 +139,6 @@ const TagsListMock = TagsList as jest.Mock; usePerformanceContextMock.mockReturnValue({ onPageReady: jest.fn() }); -dashboardServiceProviderMock.mockReturnValue({ - fetchValidDashboards: jest.fn().mockResolvedValue([ - { - id: 'dashboard-1', - }, - ]), -}); - const chance = new Chance(); const params = { alertId: chance.guid(), @@ -168,7 +187,7 @@ describe('Alert details', () => { expect(alertDetails.queryByTestId('alertDetails')).toBeTruthy(); expect(alertDetails.queryByTestId('alertDetailsError')).toBeFalsy(); - expect(alertDetails.queryByTestId('alertDetailsPageTitle')).toBeTruthy(); + expect(alertDetails.queryByTestId(MOCK_RULE_TYPE_ID)).toBeTruthy(); expect(alertDetails.queryByTestId('alertDetailsTabbedContent')).toBeTruthy(); expect(alertDetails.queryByTestId('alert-summary-container')).toBeFalsy(); expect(alertDetails.queryByTestId('overviewTab')).toBeTruthy(); @@ -211,4 +230,33 @@ describe('Alert details', () => { expect(alertDetails.queryByTestId('alertDetailsError')).toBeFalsy(); expect(alertDetails.queryByTestId('alertDetails')).toBeFalsy(); }); + + it('should navigate to Related Dashboards tab and display linked and suggested dashboards', async () => { + useFetchAlertDetailMock.mockReturnValue([false, alertDetail]); + + const alertDetails = renderComponent(); + + await waitFor(() => expect(alertDetails.queryByTestId('centerJustifiedSpinner')).toBeFalsy()); + + // Find and click the Related Dashboards tab + const relatedDashboardsTab = alertDetails.getByText(/Related dashboards/); + expect(relatedDashboardsTab).toBeTruthy(); + expect(relatedDashboardsTab.textContent).toContain('2'); + + // Click on the Related Dashboards tab + await userEvent.click(relatedDashboardsTab); + + // Check that linked dashboards section is displayed + expect(alertDetails.queryByTestId('linked-dashboards')).toBeTruthy(); + + // Check that suggested dashboards section is displayed + expect(alertDetails.queryByTestId('suggested-dashboards')).toBeTruthy(); + + // Verify the suggested dashboard from our mock is displayed + expect(alertDetails.queryByText('Suggested Dashboard 1')).toBeTruthy(); + expect(alertDetails.queryByText('A suggested dashboard for testing')).toBeTruthy(); + expect( + alertDetails.queryByTestId('addSuggestedDashboard_alertDetailsPage_custom_threshold') + ).toBeTruthy(); + }); }); diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx index 494f5c86d06e0..84bedeafa5091 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/alert_details.tsx @@ -10,7 +10,6 @@ import { useHistory, useLocation, useParams } from 'react-router-dom'; import { usePerformanceContext } from '@kbn/ebt-tools'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { FindDashboardsByIdResponse } from '@kbn/dashboard-plugin/public'; import { EuiEmptyPrompt, EuiPanel, @@ -35,7 +34,6 @@ import { RuleTypeModel } from '@kbn/triggers-actions-ui-plugin/public'; import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; import dedent from 'dedent'; import { AlertFieldsTable } from '@kbn/alerts-ui-shared/src/alert_fields_table'; -import { dashboardServiceProvider } from '@kbn/response-ops-rule-form/src/common'; import { css } from '@emotion/react'; import { omit } from 'lodash'; import { RelatedAlerts } from './components/related_alerts/related_alerts'; @@ -61,6 +59,7 @@ import StaleAlert from './components/stale_alert'; import { RelatedDashboards } from './components/related_dashboards'; import { getAlertTitle } from '../../utils/format_alert_title'; import { AlertSubtitle } from './components/alert_subtitle'; +import { useRelatedDashboards } from './hooks/use_related_dashboards'; interface AlertDetailsPathParams { alertId: string; @@ -74,18 +73,21 @@ const defaultBreadcrumb = i18n.translate('xpack.observability.breadcrumbs.alertD export const LOG_DOCUMENT_COUNT_RULE_TYPE_ID = 'logs.alert.document.count'; export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold'; export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold'; - -const OVERVIEW_TAB_ID = 'overview'; -const METADATA_TAB_ID = 'metadata'; -const RELATED_ALERTS_TAB_ID = 'related_alerts'; -const INVESTIGATION_GUIDE_TAB_ID = 'investigation_guide'; const ALERT_DETAILS_TAB_URL_STORAGE_KEY = 'tabId'; -const RELATED_DASHBOARDS_TAB_ID = 'related_dashboards'; -type TabId = - | typeof OVERVIEW_TAB_ID - | typeof METADATA_TAB_ID - | typeof RELATED_ALERTS_TAB_ID - | typeof INVESTIGATION_GUIDE_TAB_ID; + +const TAB_IDS = [ + 'overview', + 'metadata', + 'related_alerts', + 'investigation_guide', + 'related_dashboards', +] as const; + +type TabId = (typeof TAB_IDS)[number]; + +const isTabId = (value: string): value is TabId => { + return Object.values(TAB_IDS).includes(value); +}; export function AlertDetails() { const { @@ -98,7 +100,6 @@ export function AlertDetails() { observabilityAIAssistant, uiSettings, serverless, - contentManagement, } = useKibana().services; const { onPageReady } = usePerformanceContext(); @@ -106,6 +107,13 @@ export function AlertDetails() { const history = useHistory(); const { ObservabilityPageTemplate, config } = usePluginContext(); const { alertId } = useParams(); + const { + isLoadingRelatedDashboards, + suggestedDashboards, + linkedDashboards, + refetchRelatedDashboards, + } = useRelatedDashboards(alertId); + const [isLoading, alertDetail] = useFetchAlertDetail(alertId); const [ruleTypeModel, setRuleTypeModel] = useState(null); const CasesContext = getCasesContext(); @@ -114,30 +122,24 @@ export function AlertDetails() { const { rule, refetch } = useFetchRule({ ruleId, }); + + const onSuccessAddSuggestedDashboard = useCallback(async () => { + await Promise.all([refetchRelatedDashboards(), refetch()]); + }, [refetch, refetchRelatedDashboards]); + const [alertStatus, setAlertStatus] = useState(); const { euiTheme } = useEuiTheme(); const [sources, setSources] = useState(); const [activeTabId, setActiveTabId] = useState(() => { const searchParams = new URLSearchParams(search); const urlTabId = searchParams.get(ALERT_DETAILS_TAB_URL_STORAGE_KEY); - - return urlTabId && - [ - OVERVIEW_TAB_ID, - METADATA_TAB_ID, - RELATED_ALERTS_TAB_ID, - INVESTIGATION_GUIDE_TAB_ID, - ].includes(urlTabId) - ? (urlTabId as TabId) - : OVERVIEW_TAB_ID; + return urlTabId && isTabId(urlTabId) ? urlTabId : 'overview'; }); - const [validDashboards, setValidDashboards] = useState([]); - const linkedDashboards = React.useMemo(() => rule?.artifacts?.dashboards ?? [], [rule]); const handleSetTabId = async (tabId: TabId) => { setActiveTabId(tabId); let searchParams = new URLSearchParams(search); - if (tabId === RELATED_ALERTS_TAB_ID) { + if (tabId === 'related_alerts') { searchParams.set(ALERT_DETAILS_TAB_URL_STORAGE_KEY, tabId); } else { searchParams = new URLSearchParams(); @@ -169,7 +171,6 @@ export function AlertDetails() { if (alertDetail) { setRuleTypeModel(ruleTypeRegistry.get(alertDetail?.formatted.fields[ALERT_RULE_TYPE_ID]!)); setAlertStatus(alertDetail?.formatted?.fields[ALERT_STATUS] as AlertStatus); - setActiveTabId(OVERVIEW_TAB_ID); } }, [alertDetail, ruleTypeRegistry]); @@ -196,23 +197,11 @@ export function AlertDetails() { }, []); useEffect(() => { - if (!isLoading && !!alertDetail && activeTabId === OVERVIEW_TAB_ID) { + if (!isLoading && !!alertDetail && activeTabId === 'overview') { onPageReady(); } }, [onPageReady, alertDetail, isLoading, activeTabId]); - useEffect(() => { - const fetchValidDashboards = async () => { - const dashboardIds = linkedDashboards.map((dashboard: { id: string }) => dashboard.id); - const findDashboardsService = dashboardServiceProvider(contentManagement); - const existingDashboards = await findDashboardsService.fetchValidDashboards(dashboardIds); - - setValidDashboards(existingDashboards.length ? existingDashboards : []); - }; - - fetchValidDashboards(); - }, [rule, contentManagement, linkedDashboards]); - if (isLoading) { return ; } @@ -299,15 +288,22 @@ export function AlertDetails() { ); - const relatedDashboardsTab = alertDetail ? ( - - ) : ( - - ); + const relatedDashboardsTab = + alertDetail && rule ? ( + + ) : ( + + ); - const tabs: EuiTabbedContentTab[] = [ + const tabs: Array & { id: TabId }> = [ { - id: OVERVIEW_TAB_ID, + id: 'overview', name: i18n.translate('xpack.observability.alertDetails.tab.overviewLabel', { defaultMessage: 'Overview', }), @@ -315,7 +311,7 @@ export function AlertDetails() { content: overviewTab, }, { - id: METADATA_TAB_ID, + id: 'metadata', name: i18n.translate('xpack.observability.alertDetails.tab.metadataLabel', { defaultMessage: 'Metadata', }), @@ -346,7 +342,7 @@ export function AlertDetails() { ), }, { - id: RELATED_ALERTS_TAB_ID, + id: 'related_alerts', name: ( <> , }, - ...(validDashboards?.length - ? [ - { - id: RELATED_DASHBOARDS_TAB_ID, - name: ( - <> - {' '} - - {validDashboards?.length} - - - ), - 'data-test-subj': 'relatedDashboardsTab', - content: relatedDashboardsTab, - }, - ] - : []), + { + id: 'related_dashboards', + name: ( + <> + + {isLoadingRelatedDashboards ? ( + + ) : ( + + {(linkedDashboards?.length || 0) + (suggestedDashboards?.length || 0)} + + )} + + ), + 'data-test-subj': 'relatedDashboardsTab', + content: relatedDashboardsTab, + }, ]; return ( diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_dashboards.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_dashboards.tsx index 19f2223803665..0032a2436ccb8 100644 --- a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_dashboards.tsx +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_dashboards.tsx @@ -5,127 +5,74 @@ * 2.0. */ -import React, { useState, useEffect } from 'react'; +import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; -import { DashboardLocatorParams } from '@kbn/dashboard-plugin/common'; -import { - EuiTitle, - EuiText, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiHorizontalRule, -} from '@elastic/eui'; -import { useKibana } from '../../../utils/kibana_react'; -import { TopAlert } from '../../..'; +import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; +import { DashboardTiles } from './related_dashboards/dashboard_tiles'; +import { DashboardMetadata } from './related_dashboards/dashboard_tile'; +import { useAddSuggestedDashboards } from '../hooks/use_add_suggested_dashboard'; interface RelatedDashboardsProps { - alert: TopAlert; - relatedDashboards: Array<{ id: string }>; + rule: Rule; + suggestedDashboards?: DashboardMetadata[]; + linkedDashboards?: DashboardMetadata[]; + isLoadingRelatedDashboards: boolean; + onSuccessAddSuggestedDashboard: () => Promise; } -export function RelatedDashboards({ alert, relatedDashboards }: RelatedDashboardsProps) { - const [dashboardsMeta, setDashboardsMeta] = useState< - Array<{ id: string; title: string; description: string }> - >([]); +export function RelatedDashboards({ + rule, + isLoadingRelatedDashboards, + linkedDashboards, + suggestedDashboards, + onSuccessAddSuggestedDashboard, +}: RelatedDashboardsProps) { + const { onClickAddSuggestedDashboard, addingDashboardId } = useAddSuggestedDashboards({ + rule, + onSuccessAddSuggestedDashboard, + }); - const { - services: { - share: { url: urlService }, - dashboard: dashboardService, - }, - } = useKibana(); - - const dashboardLocator = urlService.locators.get(DASHBOARD_APP_LOCATOR); - - useEffect(() => { - if (!relatedDashboards?.length || !dashboardService) { - return; - } - - const fetchDashboards = async () => { - const dashboardPromises = relatedDashboards.map(async (dashboard) => { - try { - const findDashboardsService = await dashboardService.findDashboardsService(); - const response = await findDashboardsService.findById(dashboard.id); - - if (response.status === 'error') { - return null; - } - - return { - id: dashboard.id, - title: response.attributes.title, - description: response.attributes.description, - }; - } catch (dashboardError) { - return null; - } - }); - - const results = await Promise.all(dashboardPromises); - - // Filter out null results (failed dashboard fetches) - const validDashboards = results.filter(Boolean) as Array<{ - id: string; - title: string; - description: string; - }>; - - setDashboardsMeta(validDashboards); - }; - - fetchDashboards(); - }, [relatedDashboards, dashboardService, setDashboardsMeta]); + const suggestedDashboardsWithButton = useMemo( + () => + suggestedDashboards?.map((d) => { + const ruleType = rule.ruleTypeId.split('.').pop(); + return { + ...d, + actionButtonProps: { + isDisabled: addingDashboardId !== undefined && addingDashboardId !== d.id, + isLoading: addingDashboardId === d.id, + label: i18n.translate( + 'xpack.observability.alertDetails.suggestedDashboards.buttonLabel', + { + defaultMessage: 'Add to linked dashboards', + } + ), + onClick: onClickAddSuggestedDashboard, + ruleType: ruleType || 'unknown', + }, + }; + }), + [addingDashboardId, onClickAddSuggestedDashboard, rule.ruleTypeId, suggestedDashboards] + ); return (
- - - - -

- {i18n.translate('xpack.observability.alertDetails.relatedDashboards', { - defaultMessage: 'Linked dashboards', - })} -

-
- -
-
- - - {dashboardsMeta.map((dashboard) => ( - <> - - - - { - e.preventDefault(); - if (dashboardLocator) { - const url = await dashboardLocator.getUrl({ - dashboardId: dashboard.id, - }); - window.open(url, '_blank'); - } else { - console.error('Dashboard locator is not available'); - } - }} - > - {dashboard.title} - - - - {dashboard.description} - - - - - - ))} + +
); } diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_dashboards/dashboard_tile.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_dashboards/dashboard_tile.tsx new file mode 100644 index 0000000000000..10793fb2326eb --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_dashboards/dashboard_tile.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; +import { DashboardLocatorParams } from '@kbn/dashboard-plugin/common'; +import { + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiButtonEmpty, +} from '@elastic/eui'; +import { useKibana } from '../../../../utils/kibana_react'; +export interface DashboardMetadata { + id: string; + title: string; + description: string; +} + +export interface ActionButtonProps { + onClick: (dashboard: DashboardMetadata) => void; + label: string; + isLoading: boolean; + isDisabled: boolean; + ruleType: string; +} + +export function DashboardTile({ + dashboard, + actionButtonProps, +}: { + dashboard: DashboardMetadata; + actionButtonProps?: ActionButtonProps; +}) { + const { + services: { + share: { url: urlService }, + }, + } = useKibana(); + const dashboardLocator = urlService.locators.get(DASHBOARD_APP_LOCATOR); + + return ( + <> + + + + { + e.preventDefault(); + if (dashboardLocator) { + const url = await dashboardLocator.getUrl({ + dashboardId: dashboard.id, + }); + window.open(url, '_blank'); + } else { + console.error('Dashboard locator is not available'); + } + }} + > + {dashboard.title} + + + + {dashboard.description} + + + {actionButtonProps ? ( + + actionButtonProps.onClick(dashboard)} + isLoading={actionButtonProps.isLoading} + isDisabled={actionButtonProps.isDisabled} + iconType="plus" + > + {actionButtonProps.label} + + + ) : null} + + + + ); +} diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_dashboards/dashboard_tiles.tsx b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_dashboards/dashboard_tiles.tsx new file mode 100644 index 0000000000000..212f7886816e0 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/components/related_dashboards/dashboard_tiles.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiTitle, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLoadingSpinner, + EuiText, +} from '@elastic/eui'; +import { ActionButtonProps, DashboardTile, DashboardMetadata } from './dashboard_tile'; + +export function DashboardTiles({ + title, + isLoadingDashboards, + dashboards, + dataTestSubj, +}: { + title: string; + isLoadingDashboards: boolean; + dashboards?: Array; + dataTestSubj: string; +}) { + const wrapWithHeader = (component: React.ReactNode) => { + return ( + <> + + + + +

{title}

+
+ +
+
+ + {component} + + ); + }; + + if (isLoadingDashboards) return wrapWithHeader(); + + if (!dashboards || !dashboards.length) + return wrapWithHeader( + + {i18n.translate('xpack.observability.relatedDashboards.noDashboardsTextLabel', { + defaultMessage: 'No dashboards', + })} + + ); + + return wrapWithHeader( + dashboards.map(({ actionButtonProps, ...rest }) => ( + + )) + ); +} diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.test.ts b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.test.ts new file mode 100644 index 0000000000000..35e3ee84fccc5 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.test.ts @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react'; +import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; +import { useAddSuggestedDashboards } from './use_add_suggested_dashboard'; +import { kibanaStartMock } from '../../../utils/kibana_react.mock'; +import { DashboardMetadata } from '../components/related_dashboards/dashboard_tile'; + +const mockUseKibanaReturnValue = kibanaStartMock.startContract(); +let capturedOnSuccess: (data: Rule) => Promise | undefined; +let capturedOnError: ((error: any) => void) | undefined; + +// Test constants +const TEST_RULE_ID = 'test-rule-id'; +const EXISTING_DASHBOARD_ID = 'existing-dashboard-id'; +const TEST_RULE_NAME = 'Test Rule'; + +const TEST_DASHBOARD = { + id: 'new-dashboard-id', + title: 'Test Dashboard', + description: 'Test Description', +}; + +jest.mock('@kbn/triggers-actions-ui-plugin/public', () => ({ + __esModule: true, + useKibana: jest.fn(() => mockUseKibanaReturnValue), +})); + +const mockUpdateRule = jest.fn(); + +jest.mock('@kbn/response-ops-rule-form/src/common/hooks', () => ({ + useUpdateRule: jest.fn( + (params: { onSuccess: (data: Rule) => Promise; onError: (error: any) => void }) => { + capturedOnSuccess = params.onSuccess; + capturedOnError = params.onError; + return { mutateAsync: mockUpdateRule }; + } + ), +})); + +const mockRule = { + id: TEST_RULE_ID, + ruleTypeId: 'apm', + name: TEST_RULE_NAME, + artifacts: { + dashboards: [{ id: EXISTING_DASHBOARD_ID }], + }, +} as unknown as Rule; + +const mockOnSuccessAddSuggestedDashboard = jest.fn(); + +const mockDashboard: DashboardMetadata = { + id: TEST_DASHBOARD.id, + title: TEST_DASHBOARD.title, + description: TEST_DASHBOARD.description, +}; + +describe('useAddSuggestedDashboards', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should have addingDashboardId as undefined when initially rendered', () => { + const { result } = renderHook(() => + useAddSuggestedDashboards({ + rule: mockRule, + onSuccessAddSuggestedDashboard: mockOnSuccessAddSuggestedDashboard, + }) + ); + + expect(result.current.addingDashboardId).toBeUndefined(); + }); + + it('should update addingDashboardId and call updateRule when onClickAddSuggestedDashboard is called', () => { + const { result } = renderHook(() => + useAddSuggestedDashboards({ + rule: mockRule, + onSuccessAddSuggestedDashboard: mockOnSuccessAddSuggestedDashboard, + }) + ); + + act(() => { + result.current.onClickAddSuggestedDashboard(mockDashboard); + }); + + // Check that addingDashboardId is updated + expect(result.current.addingDashboardId).toBe(TEST_DASHBOARD.id); + + // Check that updateRule is called with correct parameters + expect(mockUpdateRule).toHaveBeenCalledWith({ + id: TEST_RULE_ID, + formData: { + id: TEST_RULE_ID, + ruleTypeId: 'apm', + name: TEST_RULE_NAME, + artifacts: { + dashboards: [{ id: EXISTING_DASHBOARD_ID }, { id: TEST_DASHBOARD.id }], + }, + }, + }); + }); + + it('should call onSuccessAddSuggestedDashboard and reset addingDashboardId on success', async () => { + const { result } = renderHook(() => + useAddSuggestedDashboards({ + rule: mockRule, + onSuccessAddSuggestedDashboard: mockOnSuccessAddSuggestedDashboard, + }) + ); + + // First, trigger onClickAddSuggestedDashboard to set addingDashboardId + act(() => { + result.current.onClickAddSuggestedDashboard(mockDashboard); + }); + + // Verify addingDashboardId is set + expect(result.current.addingDashboardId).toBe(TEST_DASHBOARD.id); + + // Now trigger the onSuccess callback + const updatedRule = { + id: TEST_RULE_ID, + ruleTypeId: 'apm', + name: TEST_RULE_NAME, + artifacts: { + dashboards: [{ id: EXISTING_DASHBOARD_ID }, { id: TEST_DASHBOARD.id }], + }, + } as unknown as Rule; + + await act(async () => { + await capturedOnSuccess!(updatedRule); + }); + + expect(mockOnSuccessAddSuggestedDashboard).toHaveBeenCalled(); + + // Check that addingDashboardId is reset to undefined + expect(result.current.addingDashboardId).toBeUndefined(); + + // Check that notifications.toasts.addSuccess was called + expect(mockUseKibanaReturnValue.services.notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: 'Added to linked dashboard', + text: `From now on this dashboard will be linked to all alerts related to ${TEST_RULE_NAME}`, + }); + }); + + it('should reset addingDashboardId and show error notification on error', () => { + const { result } = renderHook(() => + useAddSuggestedDashboards({ + rule: mockRule, + onSuccessAddSuggestedDashboard: mockOnSuccessAddSuggestedDashboard, + }) + ); + + // First, trigger onClickAddSuggestedDashboard to set addingDashboardId + act(() => { + result.current.onClickAddSuggestedDashboard(mockDashboard); + }); + + // Verify addingDashboardId is set + expect(result.current.addingDashboardId).toBe(TEST_DASHBOARD.id); + + // Create a mock error + const mockError = { + message: 'Failed to update rule', + body: { message: 'Server error' }, + }; + + // Now trigger the onError callback + act(() => { + capturedOnError!(mockError); + }); + + // Check that addingDashboardId is reset to undefined + expect(result.current.addingDashboardId).toBeUndefined(); + + // Check that notifications.toasts.addError was called + expect(mockUseKibanaReturnValue.services.notifications.toasts.addError).toHaveBeenCalledWith( + mockError, + { + title: 'Error adding suggested dashboard', + } + ); + }); +}); diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.ts b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.ts new file mode 100644 index 0000000000000..881b59acd043a --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_add_suggested_dashboard.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useUpdateRule } from '@kbn/response-ops-rule-form/src/common/hooks'; +import { UpdateRuleBody } from '@kbn/response-ops-rule-form/src/common/apis'; +import { Rule, useKibana } from '@kbn/triggers-actions-ui-plugin/public'; +import { IHttpFetchError } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; +import { useState } from 'react'; +import { DashboardMetadata } from '../components/related_dashboards/dashboard_tile'; + +export const useAddSuggestedDashboards = ({ + rule, + onSuccessAddSuggestedDashboard, +}: { + rule: Rule; + onSuccessAddSuggestedDashboard: () => Promise; +}) => { + const { + services: { http, notifications }, + } = useKibana(); + + const [addingDashboardId, setAddingDashboardId] = useState(); + + const onError = useCallback( + (error: IHttpFetchError<{ message: string }>) => { + setAddingDashboardId(undefined); + notifications.toasts.addError(error, { + title: i18n.translate('xpack.observability.alertDetails.addSuggestedDashboardError', { + defaultMessage: 'Error adding suggested dashboard', + }), + }); + }, + [notifications.toasts] + ); + + const onSuccess = useCallback( + async (data: Rule) => { + if (!addingDashboardId) + throw new Error('Adding dashboard id not defined, this should never occur'); + await onSuccessAddSuggestedDashboard(); + setAddingDashboardId(undefined); + notifications.toasts.addSuccess({ + title: i18n.translate( + 'xpack.observability.alertDetails.addSuggestedDashboardSuccess.title', + { + defaultMessage: 'Added to linked dashboard', + } + ), + text: i18n.translate('xpack.observability.alertDetails.addSuggestedDashboardSuccess.text', { + defaultMessage: + 'From now on this dashboard will be linked to all alerts related to {ruleName}', + values: { + ruleName: data.name, + }, + }), + }); + }, + [addingDashboardId, notifications.toasts, onSuccessAddSuggestedDashboard] + ); + + const { mutateAsync: updateRule } = useUpdateRule({ http, onError, onSuccess }); + + const onClickAddSuggestedDashboard = useCallback( + (d: DashboardMetadata) => { + const updatedRule: UpdateRuleBody = { + ...rule, + artifacts: { + ...(rule.artifacts || {}), + dashboards: [...(rule.artifacts?.dashboards || []), { id: d.id }], + }, + }; + updateRule({ id: rule.id, formData: updatedRule }); + setAddingDashboardId(d.id); + }, + [rule, updateRule] + ); + + return { onClickAddSuggestedDashboard, addingDashboardId }; +}; diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_related_dashboards.test.ts b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_related_dashboards.test.ts new file mode 100644 index 0000000000000..6ebf96e914f2d --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_related_dashboards.test.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react'; +import { useRelatedDashboards } from './use_related_dashboards'; +import { kibanaStartMock } from '../../../utils/kibana_react.mock'; + +const mockUseKibanaReturnValue = kibanaStartMock.startContract(); +let capturedQueryFn: (() => Promise) | undefined; + +// Test constants +const TEST_ALERT_ID = 'test-alert-id'; +const API_ENDPOINT = '/internal/observability/alerts/related_dashboards'; + +const TEST_DASHBOARD_1 = { + id: 'dashboard-1', + title: 'Dashboard 1', + description: 'This is dashboard 1', +}; + +const TEST_DASHBOARD_2 = { + id: 'dashboard-2', + title: 'Dashboard 2', + description: 'This is dashboard 2', +}; + +const TEST_DASHBOARD_3 = { + id: 'dashboard-3', + title: 'Dashboard 3', + description: 'This is dashboard 3', +}; + +jest.mock('@kbn/triggers-actions-ui-plugin/public', () => ({ + __esModule: true, + useKibana: jest.fn(() => mockUseKibanaReturnValue), +})); + +const mockUseQuery = jest.fn(); +jest.mock('@tanstack/react-query', () => ({ + useQuery: (params: { queryKey: string[]; queryFn: () => Promise }) => mockUseQuery(params), +})); + +describe('useRelatedDashboards', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockUseKibanaReturnValue.services.http.get.mockClear(); + + // Default mock setup for loading state + mockUseQuery.mockImplementation( + (params: { queryKey: string[]; queryFn: () => Promise }) => { + capturedQueryFn = params.queryFn; + return { + data: undefined, + isLoading: true, + }; + } + ); + }); + + it('should have isLoadingSuggestedDashboards as true and suggestedDashboards as undefined when loading', () => { + const { result } = renderHook(() => useRelatedDashboards(TEST_ALERT_ID)); + + expect(result.current.isLoadingRelatedDashboards).toBe(true); + expect(result.current.suggestedDashboards).toBeUndefined(); + }); + + it('should call http.get with the correct URL and parameters', async () => { + renderHook(() => useRelatedDashboards(TEST_ALERT_ID)); + + // Call the captured queryFn to trigger the HTTP request + await capturedQueryFn!(); + + expect(mockUseKibanaReturnValue.services.http.get).toHaveBeenCalledWith(API_ENDPOINT, { + query: { alertId: TEST_ALERT_ID }, + }); + }); + + it('should filter suggested dashboards to only return id, title, description', () => { + const mockApiResponse = { + suggestedDashboards: [ + { + id: TEST_DASHBOARD_1.id, + title: TEST_DASHBOARD_1.title, + description: TEST_DASHBOARD_1.description, + extraProperty: 'extra value', + createdAt: '2023-01-01', + }, + { + id: TEST_DASHBOARD_2.id, + title: TEST_DASHBOARD_2.title, + description: TEST_DASHBOARD_2.description, + anotherExtraProperty: 'another extra value', + updatedAt: '2023-01-02', + }, + ], + linkedDashboards: [ + { + id: TEST_DASHBOARD_3.id, + title: TEST_DASHBOARD_3.title, + description: TEST_DASHBOARD_3.description, + extraProperty: 'extra value', + createdAt: '2023-01-01', + }, + ], + }; + + mockUseQuery.mockReturnValue({ + data: mockApiResponse, + isLoading: false, + }); + + const { result } = renderHook(() => useRelatedDashboards(TEST_ALERT_ID)); + + expect(result.current.isLoadingRelatedDashboards).toBe(false); + expect(result.current.suggestedDashboards).toEqual([ + { + id: TEST_DASHBOARD_1.id, + title: TEST_DASHBOARD_1.title, + description: TEST_DASHBOARD_1.description, + }, + { + id: TEST_DASHBOARD_2.id, + title: TEST_DASHBOARD_2.title, + description: TEST_DASHBOARD_2.description, + }, + ]); + expect(result.current.linkedDashboards).toEqual([ + { + id: TEST_DASHBOARD_3.id, + title: TEST_DASHBOARD_3.title, + description: TEST_DASHBOARD_3.description, + }, + ]); + }); +}); diff --git a/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_related_dashboards.ts b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_related_dashboards.ts new file mode 100644 index 0000000000000..99a6fd1cc0d95 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/pages/alert_details/hooks/use_related_dashboards.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpSetup } from '@kbn/core/public'; +import { useKibana } from '@kbn/triggers-actions-ui-plugin/public'; +import { GetRelatedDashboardsResponse } from '@kbn/observability-schema'; +import { useQuery } from '@tanstack/react-query'; +import { ALERTS_API_URLS } from '../../../../common/constants'; +import { DashboardMetadata } from '../components/related_dashboards/dashboard_tile'; + +export const fetchRelatedDashboards = async ({ + alertId, + http, +}: { + alertId: string; + http: HttpSetup; +}): Promise => { + return http.get(ALERTS_API_URLS.INTERNAL_RELATED_DASHBOARDS, { + query: { alertId }, + }); +}; + +const getDashboardMetadata = ({ description, id, title }: T) => ({ + description, + id, + title, +}); + +export const getRelatedDashboardsQueryKey = (alertId: string) => ['relatedDashboards', alertId]; + +export const useRelatedDashboards = (alertId: string) => { + const { http } = useKibana().services; + + const { data, isLoading, refetch } = useQuery({ + queryKey: getRelatedDashboardsQueryKey(alertId), + queryFn: () => fetchRelatedDashboards({ alertId, http }), + refetchOnWindowFocus: false, // Disable window focus refetching + }); + + return { + isLoadingRelatedDashboards: isLoading, + suggestedDashboards: data?.suggestedDashboards?.map(getDashboardMetadata), + linkedDashboards: data?.linkedDashboards?.map(getDashboardMetadata), + refetchRelatedDashboards: refetch, + }; +}; diff --git a/x-pack/solutions/observability/plugins/observability/server/routes/alerts/route.ts b/x-pack/solutions/observability/plugins/observability/server/routes/alerts/route.ts index bbc2238c09083..b0e8e4833c85a 100644 --- a/x-pack/solutions/observability/plugins/observability/server/routes/alerts/route.ts +++ b/x-pack/solutions/observability/plugins/observability/server/routes/alerts/route.ts @@ -12,13 +12,14 @@ import { import { IKibanaResponse } from '@kbn/core-http-server'; import type { SavedObjectsFindResult } from '@kbn/core/server'; import type { DashboardAttributes } from '@kbn/dashboard-plugin/server'; +import { ALERTS_API_URLS } from '../../../common/constants'; import { createObservabilityServerRoute } from '../create_observability_server_route'; import { RelatedDashboardsClient } from '../../services/related_dashboards_client'; import { InvestigateAlertsClient } from '../../services/investigate_alerts_client'; import { AlertNotFoundError } from '../../common/errors/alert_not_found_error'; const alertsDynamicDashboardSuggestions = createObservabilityServerRoute({ - endpoint: 'GET /internal/observability/alerts/related_dashboards', + endpoint: `GET ${ALERTS_API_URLS.INTERNAL_RELATED_DASHBOARDS}`, security: { authz: { enabled: false, diff --git a/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.test.ts b/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.test.ts index 4d63c1200488a..89ff06631765a 100644 --- a/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.test.ts +++ b/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import Boom from '@hapi/boom'; import { RelatedDashboardsClient } from './related_dashboards_client'; import { Logger } from '@kbn/core/server'; import { IContentClient } from '@kbn/content-management-plugin/server/types'; @@ -604,6 +604,38 @@ describe('RelatedDashboardsClient', () => { { id: 'dashboard2', title: 'Dashboard 2', matchedBy: { linked: true } }, ]); }); + + it('should handle linked dashboards not found gracefully', async () => { + const mockAlert = { + getRuleId: jest.fn().mockReturnValue('rule-id'), + } as unknown as AlertData; + + // @ts-ignore next-line + client.setAlert(mockAlert); + + alertsClient.getRuleById = jest.fn().mockResolvedValue({ + artifacts: { + dashboards: [{ id: 'dashboard1' }, { id: 'dashboard2' }], + }, + }); + + dashboardClient.get = jest + .fn() + .mockResolvedValueOnce({ + result: { item: { id: 'dashboard1', attributes: { title: 'Dashboard 1' } } }, + }) + .mockRejectedValueOnce(new Boom.Boom('Dashboard not found', { statusCode: 404 })); + + // @ts-ignore next-line + const result = await client.getLinkedDashboards(); + + expect(result).toEqual([ + { id: 'dashboard1', title: 'Dashboard 1', matchedBy: { linked: true } }, + ]); + expect(logger.warn).toHaveBeenCalledWith( + 'Linked dashboard with id dashboard2 not found. Skipping.' + ); + }); }); describe('getLinkedDashboardsByIds', () => { @@ -638,7 +670,9 @@ describe('RelatedDashboardsClient', () => { }); it('should handle errors when fetching dashboards', async () => { - dashboardClient.get = jest.fn().mockRejectedValue(new Error('Dashboard fetch failed')); + dashboardClient.get = jest + .fn() + .mockRejectedValue(new Boom.Boom('Dashboard fetch failed', { statusCode: 500 })); // @ts-ignore next-line await expect(client.getLinkedDashboardsByIds(['dashboard1'])).rejects.toThrow( diff --git a/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts b/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts index 8dd158356b45b..5a5c9dc2ab332 100644 --- a/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts +++ b/x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts @@ -73,7 +73,7 @@ export class RelatedDashboardsClient { const alert = this.checkAlert(); const allSuggestedDashboards = new Set(); const relevantDashboardsById = new Map(); - const index = await this.getRuleQueryIndex(); + const index = this.getRuleQueryIndex(); const allRelevantFields = alert.getAllRelevantFields(); if (index) { @@ -155,6 +155,7 @@ export class RelatedDashboardsClient { relevantDashboards.push({ id: d.id, title: d.attributes.title, + description: d.attributes.description, matchedBy: { index: [index] }, relevantPanelCount: matchingPanels.length, relevantPanels: matchingPanels.map((p) => ({ @@ -204,6 +205,7 @@ export class RelatedDashboardsClient { relevantDashboards.push({ id: d.id, title: d.attributes.title, + description: d.attributes.description, matchedBy: { fields: Array.from(allMatchingFields) }, relevantPanelCount: matchingPanels.length, relevantPanels: matchingPanels.map((p) => ({ @@ -344,15 +346,30 @@ export class RelatedDashboardsClient { } private async getLinkedDashboardsByIds(ids: string[]): Promise { - const dashboardsResponse = await Promise.all(ids.map((id) => this.dashboardClient.get(id))); - const linkedDashboards: Dashboard[] = dashboardsResponse.map((d) => { - return d.result.item; - }); - return linkedDashboards.map((d) => ({ - id: d.id, - title: d.attributes.title, - matchedBy: { linked: true }, - })); + const linkedDashboardsResponse = await Promise.all( + ids.map((id) => this.getLinkedDashboardById(id)) + ); + return linkedDashboardsResponse.filter((dashboard): dashboard is RelatedDashboard => + Boolean(dashboard) + ); + } + + private async getLinkedDashboardById(id: string): Promise { + try { + const dashboardResponse = await this.dashboardClient.get(id); + return { + id: dashboardResponse.result.item.id, + title: dashboardResponse.result.item.attributes.title, + matchedBy: { linked: true }, + description: dashboardResponse.result.item.attributes.description, + }; + } catch (error) { + if (error.output.statusCode === 404) { + this.logger.warn(`Linked dashboard with id ${id} not found. Skipping.`); + return null; + } + throw new Error(`Error fetching dashboard with id ${id}: ${error.message || error}`); + } } private getMatchingFields(dashboard: RelatedDashboard): string[] { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/incident_management/suggested_dashboards.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/incident_management/suggested_dashboards.ts index ad4e9b4c0057d..a6c2d386b2a1c 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/incident_management/suggested_dashboards.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/incident_management/suggested_dashboards.ts @@ -12,6 +12,7 @@ import type { RoleCredentials } from '@kbn/ftr-common-functional-services'; import { Aggregators } from '@kbn/observability-plugin/common/custom_threshold_rule/types'; import { COMPARATORS } from '@kbn/alerting-comparators'; import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils'; +import { ALERTS_API_URLS } from '@kbn/observability-plugin/common/constants'; import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { @@ -207,7 +208,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const alertId = firstHit._id; const { body } = await supertestWithoutAuth - .get(`/internal/observability/alerts/related_dashboards?alertId=${alertId}`) + .get(`${ALERTS_API_URLS.INTERNAL_RELATED_DASHBOARDS}?alertId=${alertId}`) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) .expect(200);