diff --git a/src/platform/packages/shared/deeplinks/management/deep_links.ts b/src/platform/packages/shared/deeplinks/management/deep_links.ts index cdb7d1e31a431..204720c4f2828 100644 --- a/src/platform/packages/shared/deeplinks/management/deep_links.ts +++ b/src/platform/packages/shared/deeplinks/management/deep_links.ts @@ -79,6 +79,7 @@ export type ManagementId = | 'action_policies' | 'execution_history' | 'rules' + | 'rule_library' | 'triggersActions' | 'triggersActionsAlerts' | 'triggersActionsConnectors' diff --git a/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts b/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts index dbbd683f6b521..a7a526922ca77 100644 --- a/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts +++ b/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts @@ -74,6 +74,8 @@ export const AGENT_BUILDER_TRACING_USER_DATA_SETTING_ID = 'agentBuilder:tracing: // Alerting settings export const ALERTING_V2_ENABLED_SETTING_ID = 'alerting:v2:enabled'; +export const ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID = + 'alerting:v2:showClassicAlertsTable'; // Context engine settings export const CONTEXT_ENGINE_ENABLED_SETTING_ID = 'contextEngine:enabled'; diff --git a/src/platform/packages/shared/serverless/settings/observability_project/index.ts b/src/platform/packages/shared/serverless/settings/observability_project/index.ts index 16254b3b48669..ac6b0bdae1cd6 100644 --- a/src/platform/packages/shared/serverless/settings/observability_project/index.ts +++ b/src/platform/packages/shared/serverless/settings/observability_project/index.ts @@ -16,6 +16,7 @@ import { export const OBSERVABILITY_PROJECT_SETTINGS = [ settings.ALERTING_V2_ENABLED_SETTING_ID, + settings.ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID, settings.DEFAULT_ROUTE_ID, settings.OBSERVABILITY_ENABLE_COMPARISON_BY_DEFAULT_ID, settings.OBSERVABILITY_APM_DEFAULT_SERVICE_ENVIRONMENT_ID, diff --git a/x-pack/platform/packages/shared/response-ops/alerting-v2-constants/src/advanced_settings.ts b/x-pack/platform/packages/shared/response-ops/alerting-v2-constants/src/advanced_settings.ts index d582138fec237..24e6d83c73fd3 100644 --- a/x-pack/platform/packages/shared/response-ops/alerting-v2-constants/src/advanced_settings.ts +++ b/x-pack/platform/packages/shared/response-ops/alerting-v2-constants/src/advanced_settings.ts @@ -7,6 +7,13 @@ export const ALERTING_V2_ENABLED_SETTING_ID = 'alerting:v2:enabled'; +/** + * Space-scoped. When Alerting v2 is enabled, controls whether the classic + * Observability alerts table remains in solution navigation. + */ +export const ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID = + 'alerting:v2:showClassicAlertsTable'; + export interface AlertingAdvancedSettingValueMap { [ALERTING_V2_ENABLED_SETTING_ID]: boolean; } diff --git a/x-pack/platform/packages/shared/response-ops/alerting-v2-utils/src/index.ts b/x-pack/platform/packages/shared/response-ops/alerting-v2-utils/src/index.ts index 191298337a41a..62dfc2e3826ed 100644 --- a/x-pack/platform/packages/shared/response-ops/alerting-v2-utils/src/index.ts +++ b/x-pack/platform/packages/shared/response-ops/alerting-v2-utils/src/index.ts @@ -9,7 +9,9 @@ export { getAlertingV2ManagementNavPanel } from './get_management_nav_panel'; export { isAlertingV2Enabled, shouldShowAlertingV2CreateRuleFlyout, + shouldShowClassicObservabilityAlertsTable, } from './is_alerting_v2_enabled'; + export { normalizeTags } from './normalize_tags'; export { resolveArtifactId } from './resolve_artifact_id'; export { resolveTimeField, type ResolveTimeFieldParams } from './time_field'; diff --git a/x-pack/platform/packages/shared/response-ops/alerting-v2-utils/src/is_alerting_v2_enabled.test.ts b/x-pack/platform/packages/shared/response-ops/alerting-v2-utils/src/is_alerting_v2_enabled.test.ts index 5eb80dbd932ab..b8384546c698c 100644 --- a/x-pack/platform/packages/shared/response-ops/alerting-v2-utils/src/is_alerting_v2_enabled.test.ts +++ b/x-pack/platform/packages/shared/response-ops/alerting-v2-utils/src/is_alerting_v2_enabled.test.ts @@ -10,6 +10,7 @@ import { coreMock } from '@kbn/core/public/mocks'; import { isAlertingV2Enabled, shouldShowAlertingV2CreateRuleFlyout, + shouldShowClassicObservabilityAlertsTable, } from './is_alerting_v2_enabled'; describe('isAlertingV2Enabled', () => { @@ -87,3 +88,37 @@ describe('shouldShowAlertingV2CreateRuleFlyout', () => { expect(shouldShowAlertingV2CreateRuleFlyout(core)).toBe(false); }); }); + +describe('shouldShowClassicObservabilityAlertsTable', () => { + let core: CoreStart; + + beforeEach(() => { + core = coreMock.createStart(); + core.settings.globalClient.get = (_key: string) => false as T; + core.settings.client.get = (_key: string) => false as T; + }); + + it('returns true when alerting v2 is disabled', () => { + expect(shouldShowClassicObservabilityAlertsTable(core)).toBe(true); + }); + + it('returns false when alerting v2 is enabled and the space setting is off', () => { + core.settings.globalClient.get = (_key: string) => true as T; + + expect(shouldShowClassicObservabilityAlertsTable(core)).toBe(false); + }); + + it('returns true when alerting v2 is enabled and the space setting is on', () => { + core.settings.globalClient.get = (_key: string) => true as T; + core.settings.client.get = (_key: string) => true as T; + + expect(shouldShowClassicObservabilityAlertsTable(core)).toBe(true); + }); + + it('returns false when alerting v2 is enabled and the space setting is unset', () => { + core.settings.globalClient.get = (_key: string) => true as T; + core.settings.client.get = (_key: string) => undefined as T; + + expect(shouldShowClassicObservabilityAlertsTable(core)).toBe(false); + }); +}); diff --git a/x-pack/platform/packages/shared/response-ops/alerting-v2-utils/src/is_alerting_v2_enabled.ts b/x-pack/platform/packages/shared/response-ops/alerting-v2-utils/src/is_alerting_v2_enabled.ts index 295cf29c3d958..3f8eebb50c385 100644 --- a/x-pack/platform/packages/shared/response-ops/alerting-v2-utils/src/is_alerting_v2_enabled.ts +++ b/x-pack/platform/packages/shared/response-ops/alerting-v2-utils/src/is_alerting_v2_enabled.ts @@ -6,7 +6,10 @@ */ import type { CoreStart } from '@kbn/core-lifecycle-browser'; -import { ALERTING_V2_ENABLED_SETTING_ID } from '@kbn/alerting-v2-constants'; +import { + ALERTING_V2_ENABLED_SETTING_ID, + ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID, +} from '@kbn/alerting-v2-constants'; /** Feature id from `@kbn/alerting-v2-plugin/common/feature_privileges`. */ const ALERTING_V2_RULES_FEATURE_ID = 'alerting_v2_rules'; @@ -46,3 +49,21 @@ export const isAlertingV2Enabled = (core: CoreStart): boolean => { export const shouldShowAlertingV2CreateRuleFlyout = (core: CoreStart): boolean => { return isAlertingV2Enabled(core) && hasAlertingV2RulesWriteCapability(core); }; + +/** + * Returns whether the classic Observability alerts table should appear in + * solution navigation. + * + * Always shown while Alerting v2 is disabled. When v2 is enabled, shown only + * if the space-scoped `alerting:v2:showClassicAlertsTable` setting is true. + */ +export const shouldShowClassicObservabilityAlertsTable = (core: CoreStart): boolean => { + if (!isAlertingV2Enabled(core)) { + return true; + } + + return ( + core.settings.client.get(ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID, false) === + true + ); +}; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/settings/advanced_settings.ts b/x-pack/platform/plugins/shared/alerting_v2/server/settings/advanced_settings.ts index edae4f4f6daa7..941d293d01bed 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/settings/advanced_settings.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/settings/advanced_settings.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import type { UiSettingsParams } from '@kbn/core/types'; import { ALERTING_V2_ENABLED_SETTING_ID, + ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID, type AlertingAdvancedSettingId, type AlertingAdvancedSettingValueMap, } from '@kbn/alerting-v2-constants'; @@ -43,3 +44,25 @@ export const alertingAdvancedSettings = { experimental: true, }, } satisfies AlertingV2AdvancedSettingsRegistration; + +/** + * Space-scoped settings. Registered via `uiSettings.register` (not + * `registerGlobal`) so each Kibana space can opt in independently. + */ +export const alertingSpaceAdvancedSettings: Record> = { + [ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID]: { + category: [ALERTING_V2_CATEGORY], + name: i18n.translate('xpack.alertingVTwo.showClassicAlertsTableSettingName', { + defaultMessage: 'Show classic alerts table', + }), + type: 'boolean', + value: false, + description: i18n.translate('xpack.alertingVTwo.showClassicAlertsTableSettingDescription', { + defaultMessage: + 'Show the classic Observability alerts table in navigation. Only displays alerts from v1 alerting rules.', + }), + schema: schema.boolean(), + requiresPageReload: true, + experimental: true, + }, +}; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_on_setup.ts b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_on_setup.ts index 2a6762b332ff7..c27f2c5b56738 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_on_setup.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/setup/bind_on_setup.ts @@ -28,7 +28,10 @@ import { ACTION_POLICY_EVENT_ACTIONS, ACTION_POLICY_EVENT_PROVIDER, } from '../lib/dispatcher/steps/constants'; -import { alertingAdvancedSettings } from '../settings/advanced_settings'; +import { + alertingAdvancedSettings, + alertingSpaceAdvancedSettings, +} from '../settings/advanced_settings'; /** * Core platform setup-phase registrations (feature privileges, saved objects, @@ -56,6 +59,7 @@ export function bindOnSetup({ bind }: ContainerModuleLoadOptions) { const uiSettingsSetup = container.get(CoreSetup('uiSettings')); uiSettingsSetup.registerGlobal(alertingAdvancedSettings); + uiSettingsSetup.register(alertingSpaceAdvancedSettings); const eventLogService = container.get( PluginSetup('eventLog') diff --git a/x-pack/solutions/observability/packages/kbn-scout-oblt/src/playwright/page_objects/observability_navigation.ts b/x-pack/solutions/observability/packages/kbn-scout-oblt/src/playwright/page_objects/observability_navigation.ts index 906b2b1ba24a3..6b23392e75716 100644 --- a/x-pack/solutions/observability/packages/kbn-scout-oblt/src/playwright/page_objects/observability_navigation.ts +++ b/x-pack/solutions/observability/packages/kbn-scout-oblt/src/playwright/page_objects/observability_navigation.ts @@ -34,7 +34,7 @@ export class ObservabilityNavigation { /** `goto*` does not call `waitForLoad()` — sidenav may be absent (e.g. classic chrome); call `waitForLoad()` when interacting with nav. */ async goto() { - await this.page.gotoApp('observability'); + await this.page.gotoApp('observability/overview'); } async gotoLanding() { @@ -105,6 +105,14 @@ export class ObservabilityNavigation { return this.sidenav.locator(`[data-test-subj~="nav-item-deepLinkId-${deepLinkId}"]`); } + /** + * In-app Observability solution sidenav item (classic chrome only). + * Hidden when chrome style is `project` — use the chrome nav helpers there. + */ + classicSidebarNavItem(appId: string, navId: string): Locator { + return this.page.testSubj.locator(`observability-nav-${appId}-${navId}`); + } + navItemInSidenavById(id: string): Locator { return this.sidenav.locator(`[data-test-subj~="nav-item-id-${id}"]`); } @@ -126,6 +134,14 @@ export class ObservabilityNavigation { return this.page.testSubj.locator(`~kbnChromeNav-sidePanel_${id}`); } + navItemInPanelByDeepLinkId(panelId: string, deepLinkId: string): Locator { + return this.sidePanel(panelId).locator(`[data-test-subj~="nav-item-deepLinkId-${deepLinkId}"]`); + } + + navItemInPanelById(panelId: string, id: string): Locator { + return this.sidePanel(panelId).locator(`[data-test-subj~="nav-item-id-${id}"]`); + } + nestedPanel(id: string): Locator { return this.morePopover.locator(`[data-test-subj="kbnChromeNav-nestedPanel-${id}"]`); } diff --git a/x-pack/solutions/observability/plugins/observability/moon.yml b/x-pack/solutions/observability/plugins/observability/moon.yml index 168e26059b48b..30bb328734922 100644 --- a/x-pack/solutions/observability/plugins/observability/moon.yml +++ b/x-pack/solutions/observability/plugins/observability/moon.yml @@ -154,6 +154,7 @@ dependsOn: - '@kbn/ai-assistant-common' - '@kbn/management-settings-ids' - '@kbn/alerting-v2-utils' + - '@kbn/alerting-v2-constants' - '@kbn/deeplinks-workflows' - '@kbn/deeplinks-evals' - '@kbn/observability-agent-builder-plugin' @@ -169,6 +170,7 @@ dependsOn: - '@kbn/app-header' - '@kbn/core-chrome-app-menu-components' - '@kbn/global-search-plugin' + - '@kbn/core-lifecycle-browser' tags: - plugin - prod diff --git a/x-pack/solutions/observability/plugins/observability/public/index.ts b/x-pack/solutions/observability/plugins/observability/public/index.ts index d9951181a1994..be40b7d67d004 100644 --- a/x-pack/solutions/observability/plugins/observability/public/index.ts +++ b/x-pack/solutions/observability/plugins/observability/public/index.ts @@ -46,6 +46,7 @@ export { getCoreVitalsComponent } from './pages/overview/components/sections/ux/ export { ObservabilityAlertSearchBar } from './components/alert_search_bar/get_alert_search_bar_lazy'; export { DatePicker } from './pages/overview/components/date_picker'; export { NightshiftNavigationIcon } from '@kbn/observability-shared-plugin/public'; +export { getAlertsNavPanel } from './nav/get_alerts_nav_panel'; export type { Stat, diff --git a/x-pack/solutions/observability/plugins/observability/public/nav/get_alerts_nav_panel.test.ts b/x-pack/solutions/observability/plugins/observability/public/nav/get_alerts_nav_panel.test.ts new file mode 100644 index 0000000000000..1b454bea606f2 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/nav/get_alerts_nav_panel.test.ts @@ -0,0 +1,61 @@ +/* + * 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 type { Location } from 'history'; +import type { RootNodeDefinition } from '@kbn/core-chrome-browser'; +import { coreMock } from '@kbn/core/public/mocks'; +import { getAlertsNavPanel } from './get_alerts_nav_panel'; + +const location = { pathname: '', search: '', hash: '', state: undefined } as Location; +const prepend = (path: string) => path; +const prependWithBasePath = (path: string) => `/s/oblt${path}`; + +const isActive = ( + getIsActive: RootNodeDefinition['getIsActive'], + pathNameSerialized: string, + pathPrepend: (path: string) => string = prepend +): boolean => { + if (!getIsActive) { + throw new Error('expected getIsActive'); + } + + return getIsActive({ pathNameSerialized, location, prepend: pathPrepend }); +}; + +describe('getAlertsNavPanel', () => { + it('preserves serverless startsWith active matching on the plain Alerts link', () => { + const core = coreMock.createStart(); + core.settings.globalClient.get = (_key: string) => false as T; + + const [alertsLink] = getAlertsNavPanel(core); + + expect(alertsLink.getIsActive).toEqual(expect.any(Function)); + expect(isActive(alertsLink.getIsActive, '/app/observability/alerts')).toBe(true); + expect(isActive(alertsLink.getIsActive, '/app/observability/alerts/abc-123')).toBe(true); + expect( + isActive( + alertsLink.getIsActive, + '/s/oblt/app/observability/alerts/abc-123', + prependWithBasePath + ) + ).toBe(true); + expect(isActive(alertsLink.getIsActive, '/app/discover')).toBe(false); + expect(isActive(alertsLink.getIsActive, '/app/observability/overview')).toBe(false); + }); + + it('uses the same active matcher on the v2 panel opener', () => { + const core = coreMock.createStart(); + core.settings.globalClient.get = (_key: string) => true as T; + + const [alertsPanel] = getAlertsNavPanel(core); + + expect(alertsPanel.getIsActive).toEqual(expect.any(Function)); + expect(isActive(alertsPanel.getIsActive, '/app/observability/alerts')).toBe(true); + expect(isActive(alertsPanel.getIsActive, '/app/observability/alerts/abc-123')).toBe(true); + expect(isActive(alertsPanel.getIsActive, '/app/discover')).toBe(false); + }); +}); diff --git a/x-pack/solutions/observability/plugins/observability/public/nav/get_alerts_nav_panel.ts b/x-pack/solutions/observability/plugins/observability/public/nav/get_alerts_nav_panel.ts new file mode 100644 index 0000000000000..286815bf4a921 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/public/nav/get_alerts_nav_panel.ts @@ -0,0 +1,105 @@ +/* + * 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 type { RootNodeDefinition } from '@kbn/core-chrome-browser'; +import type { CoreStart } from '@kbn/core-lifecycle-browser'; +import { i18n } from '@kbn/i18n'; +import { + isAlertingV2Enabled, + shouldShowClassicObservabilityAlertsTable, +} from '@kbn/alerting-v2-utils'; + +const PANEL_ID = 'alerting'; +const ALERTS_LINK = 'observability-overview:alerts' as const; +const ALERTS_ICON = 'warning'; + +/** Matches `/app/observability/alerts` and sub-routes (serverless active-state behavior). */ +const getAlertsIsActive: NonNullable = ({ + pathNameSerialized, + prepend, +}) => pathNameSerialized.startsWith(prepend('/app/observability/alerts')); + +/** + * Returns the solution-nav Alerts entry for Observability. + * + * When `alerting:v2:enabled` is false, returns a plain Alerts link. + * When true, returns a panel opener whose flyout contains Inbox + * (alerting v2 episodes), Alerts V1 (classic alerts), Notifications + * and Suppressions, and Operations. + * + * Spread into a navigation tree `body`: + * + * ```ts + * ...getAlertsNavPanel(core), + * ``` + */ +export const getAlertsNavPanel = (core: CoreStart): RootNodeDefinition[] => { + if (!isAlertingV2Enabled(core)) { + return [{ link: ALERTS_LINK, icon: ALERTS_ICON, getIsActive: getAlertsIsActive }]; + } + + return [ + { + id: PANEL_ID, + link: ALERTS_LINK, + icon: ALERTS_ICON, + renderAs: 'panelOpener', + getIsActive: getAlertsIsActive, + children: [ + { + breadcrumbStatus: 'hidden' as const, + children: [ + { + link: 'management:episodes' as const, + title: i18n.translate('xpack.observability.nav.inbox', { + defaultMessage: 'Inbox', + }), + badgeType: 'new' as const, + }, + ...(shouldShowClassicObservabilityAlertsTable(core) + ? [ + { + link: ALERTS_LINK, + title: i18n.translate('xpack.observability.nav.alertsV1', { + defaultMessage: 'Alerts V1', + }), + }, + ] + : []), + ], + }, + { + title: i18n.translate('xpack.observability.nav.ruleManagement', { + defaultMessage: 'Rule Management', + }), + breadcrumbStatus: 'hidden' as const, + children: [ + { link: 'management:rules' as const }, + { link: 'management:rule_library' as const, badgeType: 'new' as const }, + ], + }, + { + title: i18n.translate('xpack.observability.nav.notificationsAndSuppressions', { + defaultMessage: 'Notifications and Suppressions', + }), + breadcrumbStatus: 'hidden' as const, + children: [ + { link: 'management:action_policies' as const, badgeType: 'new' as const }, + { link: 'management:maintenanceWindows' as const }, + ], + }, + { + title: i18n.translate('xpack.observability.nav.operations', { + defaultMessage: 'Operations', + }), + breadcrumbStatus: 'hidden' as const, + children: [{ link: 'management:execution_history' as const, badgeType: 'new' as const }], + }, + ], + }, + ]; +}; diff --git a/x-pack/solutions/observability/plugins/observability/public/navigation_tree.ts b/x-pack/solutions/observability/plugins/observability/public/navigation_tree.ts index 05871d89cba54..df07431f6909e 100644 --- a/x-pack/solutions/observability/plugins/observability/public/navigation_tree.ts +++ b/x-pack/solutions/observability/plugins/observability/public/navigation_tree.ts @@ -13,11 +13,13 @@ import { combineLatest, map, of } from 'rxjs'; import { AIChatExperience } from '@kbn/ai-assistant-common'; import { AI_CHAT_EXPERIENCE_TYPE } from '@kbn/management-settings-ids'; import { getAlertingV2ManagementNavPanel } from '@kbn/alerting-v2-utils'; + import { getWorkflowsNavPanel } from '@kbn/deeplinks-workflows'; import { EVALS_APP_ID } from '@kbn/deeplinks-evals'; import { STREAMS_SIGNIFICANT_EVENTS_AVAILABLE_FLAG } from '@kbn/significant-events-plugin/common'; import type { Location } from 'history'; import { NightshiftNavigationIcon } from '@kbn/observability-shared-plugin/public'; +import { getAlertsNavPanel } from './nav/get_alerts_nav_panel'; import type { ObservabilityPublicPluginsStart } from './plugin'; const title = i18n.translate( @@ -96,12 +98,10 @@ function createNavTree({ icon: 'flask', }, ...getWorkflowsNavPanel(coreStart), - { - link: 'observability-overview:alerts', - icon: 'warning', - }, + ...getAlertsNavPanel(coreStart), { link: 'observability-overview:cases', + children: [ { link: 'observability-overview:cases_configure', diff --git a/x-pack/solutions/observability/plugins/observability/test/scout/ui/tests/navigation_alerts_v2.spec.ts b/x-pack/solutions/observability/plugins/observability/test/scout/ui/tests/navigation_alerts_v2.spec.ts new file mode 100644 index 0000000000000..444ee24a1c947 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/test/scout/ui/tests/navigation_alerts_v2.spec.ts @@ -0,0 +1,173 @@ +/* + * 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. + */ + +/** + * This suite runs sequentially (in `tests/`, not `parallel_tests/`) because it + * toggles `alerting:v2:enabled`, a server-wide global setting. Placing it in + * `parallel_tests/` would leak the setting change into other workers. + */ + +import { + ALERTING_V2_ENABLED_SETTING_ID, + ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID, +} from '@kbn/alerting-v2-constants'; +import { spaceTest as test, tags } from '@kbn/scout-oblt'; +import { expect } from '@kbn/scout-oblt/ui'; + +const ALERTS_PANEL_ID = 'alerting'; +const ALERTS_DEEP_LINK = 'observability-overview:alerts'; + +test.describe( + 'Observability Alerts nav — alerting v2 feature flag', + { tag: [...tags.stateful.classic, ...tags.serverless.observability.complete] }, + () => { + test.beforeAll(async ({ scoutSpace, kbnClient }) => { + await scoutSpace.setSolutionView('oblt'); + await kbnClient.uiSettings.updateGlobal({ [ALERTING_V2_ENABLED_SETTING_ID]: false }); + await scoutSpace.uiSettings.set({ + [ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID]: false, + }); + }); + + test.afterAll(async ({ scoutSpace, kbnClient }) => { + await kbnClient.uiSettings.updateGlobal({ [ALERTING_V2_ENABLED_SETTING_ID]: false }); + await scoutSpace.uiSettings.set({ + [ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID]: false, + }); + }); + + test('shows a plain Alerts link when alerting v2 is disabled', async ({ + browserAuth, + pageObjects, + }) => { + await browserAuth.loginAsAdmin(); + await pageObjects.observabilityNavigation.goto(); + await pageObjects.observabilityNavigation.waitForLoad(); + + const nav = pageObjects.observabilityNavigation; + + const alertsLink = nav.navItemInPrimaryByDeepLinkId(ALERTS_DEEP_LINK); + await expect(alertsLink).toBeVisible(); + await expect(alertsLink).toHaveAttribute('href', /\/app\/observability\/alerts/); + + const alertsPanel = nav.navItemInPrimaryById(ALERTS_PANEL_ID); + await expect(alertsPanel).not.toBeVisible(); + }); + + test('hides Alerts V1 link when showClassicAlertsTable is off', async ({ + browserAuth, + pageObjects, + kbnClient, + scoutSpace, + }) => { + await kbnClient.uiSettings.updateGlobal({ [ALERTING_V2_ENABLED_SETTING_ID]: true }); + await scoutSpace.uiSettings.set({ + [ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID]: false, + }); + + await browserAuth.loginAsAdmin(); + await pageObjects.observabilityNavigation.goto(); + await pageObjects.observabilityNavigation.waitForLoad(); + + const nav = pageObjects.observabilityNavigation; + + await test.step('Alerts panel opener is visible', async () => { + await expect(nav.navItemInPrimaryById(ALERTS_PANEL_ID)).toBeVisible(); + }); + + await test.step('panel contains Inbox but not Alerts V1', async () => { + await nav.navItemInPrimaryById(ALERTS_PANEL_ID).click(); + await expect(nav.sidePanel(ALERTS_PANEL_ID)).toBeVisible(); + + await expect( + nav.navItemInPanelByDeepLinkId(ALERTS_PANEL_ID, 'management:episodes') + ).toBeVisible(); + await expect( + nav.navItemInPanelByDeepLinkId(ALERTS_PANEL_ID, ALERTS_DEEP_LINK) + ).not.toBeVisible(); + }); + }); + + test('shows Alerts V1 link when showClassicAlertsTable is on', async ({ + browserAuth, + pageObjects, + kbnClient, + scoutSpace, + }) => { + await kbnClient.uiSettings.updateGlobal({ [ALERTING_V2_ENABLED_SETTING_ID]: true }); + await scoutSpace.uiSettings.set({ + [ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID]: true, + }); + + await browserAuth.loginAsAdmin(); + await pageObjects.observabilityNavigation.goto(); + await pageObjects.observabilityNavigation.waitForLoad(); + + const nav = pageObjects.observabilityNavigation; + + await test.step('panel contains both Inbox and Alerts V1', async () => { + await nav.navItemInPrimaryById(ALERTS_PANEL_ID).click(); + await expect(nav.sidePanel(ALERTS_PANEL_ID)).toBeVisible(); + + await expect( + nav.navItemInPanelByDeepLinkId(ALERTS_PANEL_ID, 'management:episodes') + ).toBeVisible(); + await expect( + nav.navItemInPanelByDeepLinkId(ALERTS_PANEL_ID, ALERTS_DEEP_LINK) + ).toBeVisible(); + }); + + await test.step('panel contains Rule Management section', async () => { + await expect( + nav.navItemInPanelByDeepLinkId(ALERTS_PANEL_ID, 'management:rules') + ).toBeVisible(); + await expect( + nav.navItemInPanelByDeepLinkId(ALERTS_PANEL_ID, 'management:rule_library') + ).toBeVisible(); + }); + + await test.step('panel contains Notifications and Suppressions section', async () => { + await expect( + nav.navItemInPanelByDeepLinkId(ALERTS_PANEL_ID, 'management:action_policies') + ).toBeVisible(); + await expect( + nav.navItemInPanelByDeepLinkId(ALERTS_PANEL_ID, 'management:maintenanceWindows') + ).toBeVisible(); + }); + + await test.step('panel contains Operations section', async () => { + await expect( + nav.navItemInPanelByDeepLinkId(ALERTS_PANEL_ID, 'management:execution_history') + ).toBeVisible(); + }); + }); + + test('reverts to plain Alerts link after disabling alerting v2', async ({ + browserAuth, + pageObjects, + kbnClient, + scoutSpace, + }) => { + await kbnClient.uiSettings.updateGlobal({ [ALERTING_V2_ENABLED_SETTING_ID]: false }); + await scoutSpace.uiSettings.set({ + [ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID]: false, + }); + + await browserAuth.loginAsAdmin(); + await pageObjects.observabilityNavigation.goto(); + await pageObjects.observabilityNavigation.waitForLoad(); + + const nav = pageObjects.observabilityNavigation; + + const alertsLink = nav.navItemInPrimaryByDeepLinkId(ALERTS_DEEP_LINK); + await expect(alertsLink).toBeVisible(); + + const alertsPanel = nav.navItemInPrimaryById(ALERTS_PANEL_ID); + await expect(alertsPanel).not.toBeVisible(); + }); + } +); diff --git a/x-pack/solutions/observability/plugins/observability/test/scout/ui/tests/navigation_alerts_v2_classic.spec.ts b/x-pack/solutions/observability/plugins/observability/test/scout/ui/tests/navigation_alerts_v2_classic.spec.ts new file mode 100644 index 0000000000000..1c4dd3ef8665c --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability/test/scout/ui/tests/navigation_alerts_v2_classic.spec.ts @@ -0,0 +1,109 @@ +/* + * 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. + */ + +/** + * This suite runs sequentially (in `tests/`, not `parallel_tests/`) because it + * toggles `alerting:v2:enabled`, a server-wide global setting. Placing it in + * `parallel_tests/` would leak the setting change into other workers. + */ + +import { + ALERTING_V2_ENABLED_SETTING_ID, + ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID, +} from '@kbn/alerting-v2-constants'; +import { spaceTest as test, tags, OBSERVABILITY_SPA_SHELL_TIMEOUT_MS } from '@kbn/scout-oblt'; +import type { ObservabilityNavigation, ScoutPage } from '@kbn/scout-oblt'; +import { expect } from '@kbn/scout-oblt/ui'; + +const ALERTS_PANEL_ID = 'alerting'; +const CLASSIC_ALERTS_APP_ID = 'observability-overview'; +const CLASSIC_ALERTS_NAV_ID = 'alerts'; + +const expectClassicAlertsLinkUnchanged = async ( + nav: ObservabilityNavigation, + page: ScoutPage +): Promise => { + const alertsLink = nav.classicSidebarNavItem(CLASSIC_ALERTS_APP_ID, CLASSIC_ALERTS_NAV_ID); + + await test.step('Alerts link is a plain item in the classic Observability sidenav', async () => { + await expect(alertsLink).toBeVisible({ timeout: OBSERVABILITY_SPA_SHELL_TIMEOUT_MS }); + await expect(alertsLink).toHaveAttribute('href', /\/app\/observability\/alerts/); + }); + + await test.step('clicking Alerts navigates to the alerts page', async () => { + await alertsLink.click(); + await expect(page).toHaveURL(/\/app\/observability\/alerts/); + }); + + await test.step('v2 Alerts panel opener is not rendered', async () => { + await expect(nav.navItemInSidenavById(ALERTS_PANEL_ID)).not.toBeVisible(); + }); +}; + +test.describe( + 'Observability Alerts nav — classic sidebar', + { tag: [...tags.stateful.classic] }, + () => { + test.beforeAll(async ({ scoutSpace, kbnClient }) => { + await scoutSpace.setSolutionView('classic'); + await kbnClient.uiSettings.updateGlobal({ [ALERTING_V2_ENABLED_SETTING_ID]: false }); + await scoutSpace.uiSettings.set({ + [ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID]: false, + }); + }); + + test.afterAll(async ({ scoutSpace, kbnClient }) => { + await kbnClient.uiSettings.updateGlobal({ [ALERTING_V2_ENABLED_SETTING_ID]: false }); + await scoutSpace.uiSettings.set({ + [ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID]: false, + }); + }); + + test('Alerts link navigates to observability alerts with no feature flags', async ({ + browserAuth, + pageObjects, + page, + }) => { + await browserAuth.loginAsAdmin(); + await pageObjects.observabilityNavigation.goto(); + + await expectClassicAlertsLinkUnchanged(pageObjects.observabilityNavigation, page); + }); + + test('Alerts link is unchanged when alerting v2 is enabled', async ({ + browserAuth, + pageObjects, + page, + kbnClient, + }) => { + await kbnClient.uiSettings.updateGlobal({ [ALERTING_V2_ENABLED_SETTING_ID]: true }); + + await browserAuth.loginAsAdmin(); + await pageObjects.observabilityNavigation.goto(); + + await expectClassicAlertsLinkUnchanged(pageObjects.observabilityNavigation, page); + }); + + test('Alerts link is unchanged when both flags are enabled', async ({ + browserAuth, + pageObjects, + page, + kbnClient, + scoutSpace, + }) => { + await kbnClient.uiSettings.updateGlobal({ [ALERTING_V2_ENABLED_SETTING_ID]: true }); + await scoutSpace.uiSettings.set({ + [ALERTING_V2_SHOW_CLASSIC_ALERTS_TABLE_SETTING_ID]: true, + }); + + await browserAuth.loginAsAdmin(); + await pageObjects.observabilityNavigation.goto(); + + await expectClassicAlertsLinkUnchanged(pageObjects.observabilityNavigation, page); + }); + } +); diff --git a/x-pack/solutions/observability/plugins/observability/tsconfig.json b/x-pack/solutions/observability/plugins/observability/tsconfig.json index 61e976b69dfad..9a0389cd29ae9 100644 --- a/x-pack/solutions/observability/plugins/observability/tsconfig.json +++ b/x-pack/solutions/observability/plugins/observability/tsconfig.json @@ -150,6 +150,7 @@ "@kbn/ai-assistant-common", "@kbn/management-settings-ids", "@kbn/alerting-v2-utils", + "@kbn/alerting-v2-constants", "@kbn/deeplinks-workflows", "@kbn/deeplinks-evals", "@kbn/observability-agent-builder-plugin", @@ -164,7 +165,8 @@ "@kbn/core-spaces-common", "@kbn/app-header", "@kbn/core-chrome-app-menu-components", - "@kbn/global-search-plugin" + "@kbn/global-search-plugin", + "@kbn/core-lifecycle-browser" ], "exclude": ["target/**/*"] } diff --git a/x-pack/solutions/observability/plugins/serverless_observability/public/navigation_tree.test.ts b/x-pack/solutions/observability/plugins/serverless_observability/public/navigation_tree.test.ts index cf6731090b6e6..91f9fb0028483 100644 --- a/x-pack/solutions/observability/plugins/serverless_observability/public/navigation_tree.test.ts +++ b/x-pack/solutions/observability/plugins/serverless_observability/public/navigation_tree.test.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { Location } from 'history'; import { createNavigationTree, filterForFeatureAvailability } from './navigation_tree'; import type { NavigationTreeDefinition, NodeDefinition } from '@kbn/core-chrome-browser'; import type { CoreStart } from '@kbn/core/public'; @@ -149,9 +150,7 @@ describe('Navigation Tree', () => { ); }); - it('uses a single Alerts link to classic Observability alerts even when alerting v2 is enabled', () => { - core.settings.globalClient.get = (_key: string) => true as T; - + it('uses a single Alerts link when alerting v2 is disabled', () => { const { body } = createNavigationTree({ core }) as NavigationTreeDefinition; const alertsPanel = body.find( (item) => 'id' in item && item.id === 'alerting' && item.renderAs === 'panelOpener' @@ -163,6 +162,95 @@ describe('Navigation Tree', () => { expect.objectContaining({ link: 'observability-overview:alerts', icon: 'warning', + getIsActive: expect.any(Function), + }) + ); + expect( + flatAlerts?.getIsActive?.({ + pathNameSerialized: '/app/observability/alerts/abc-123', + location: { pathname: '', search: '', hash: '', state: undefined } as Location, + prepend: (path) => path, + }) + ).toBe(true); + }); + + it('opens Alerts as a flyout with notifications and operations when alerting v2 is enabled', () => { + core.settings.globalClient.get = (_key: string) => true as T; + + const { body } = createNavigationTree({ core }) as NavigationTreeDefinition; + const alertsPanel = body.find( + (item) => 'id' in item && item.id === 'alerting' && item.renderAs === 'panelOpener' + ); + + expect(alertsPanel).toEqual( + expect.objectContaining({ + id: 'alerting', + link: 'observability-overview:alerts', + icon: 'warning', + renderAs: 'panelOpener', + getIsActive: expect.any(Function), + children: [ + { + breadcrumbStatus: 'hidden', + children: [ + expect.objectContaining({ + link: 'management:episodes', + title: 'Inbox', + badgeType: 'new', + }), + ], + }, + { + title: 'Rule Management', + breadcrumbStatus: 'hidden', + children: [ + { link: 'management:rules' }, + { link: 'management:rule_library', badgeType: 'new' }, + ], + }, + { + title: 'Notifications and Suppressions', + breadcrumbStatus: 'hidden', + children: [ + { link: 'management:action_policies', badgeType: 'new' }, + { link: 'management:maintenanceWindows' }, + ], + }, + { + title: 'Operations', + breadcrumbStatus: 'hidden', + children: [{ link: 'management:execution_history', badgeType: 'new' }], + }, + ], + }) + ); + }); + + it('includes Alerts V1 in the Alerts panel when showClassicAlertsTable is enabled', () => { + core.settings.globalClient.get = (_key: string) => true as T; + core.settings.client.get = (_key: string) => true as T; + + const { body } = createNavigationTree({ core }) as NavigationTreeDefinition; + const alertsPanel = body.find( + (item) => 'id' in item && item.id === 'alerting' && item.renderAs === 'panelOpener' + ); + + expect(alertsPanel).toEqual( + expect.objectContaining({ + children: expect.arrayContaining([ + expect.objectContaining({ + children: expect.arrayContaining([ + expect.objectContaining({ + link: 'management:episodes', + title: 'Inbox', + }), + expect.objectContaining({ + link: 'observability-overview:alerts', + title: 'Alerts V1', + }), + ]), + }), + ]), }) ); }); diff --git a/x-pack/solutions/observability/plugins/serverless_observability/public/navigation_tree.ts b/x-pack/solutions/observability/plugins/serverless_observability/public/navigation_tree.ts index 07cfd2509c5fc..1a006a4d749e2 100644 --- a/x-pack/solutions/observability/plugins/serverless_observability/public/navigation_tree.ts +++ b/x-pack/solutions/observability/plugins/serverless_observability/public/navigation_tree.ts @@ -15,9 +15,10 @@ import type { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { DATA_MANAGEMENT_NAV_ID } from '@kbn/deeplinks-management'; import { getAlertingV2ManagementNavPanel } from '@kbn/alerting-v2-utils'; + import { getWorkflowsNavPanel } from '@kbn/deeplinks-workflows'; import { EVALS_APP_ID } from '@kbn/deeplinks-evals'; -import { NightshiftNavigationIcon } from '@kbn/observability-plugin/public'; +import { getAlertsNavPanel, NightshiftNavigationIcon } from '@kbn/observability-plugin/public'; export function filterForFeatureAvailability< T extends RootNodeDefinition | PanelOpenerChildDefinition @@ -87,12 +88,7 @@ export const createNavigationTree = ({ icon: 'flask', }, ...getWorkflowsNavPanel(core), - { - link: 'observability-overview:alerts', - icon: 'warning', - getIsActive: ({ pathNameSerialized, prepend }) => - pathNameSerialized.startsWith(prepend('/app/observability/alerts')), - }, + ...getAlertsNavPanel(core), ...filterForFeatureAvailability( { link: 'observability-overview:cases' as const,