From c0e6169d3a72f9acf4162e0103f1ea1c1160f18a Mon Sep 17 00:00:00 2001 From: Panagiota Mitsopoulou Date: Mon, 16 Mar 2026 16:05:23 +0200 Subject: [PATCH] remove show_all_group_by_instances from schema and change UI to select first slo definition and then instances --- .../transforms/transform_alerts_out.test.ts | 56 ++-- .../alerts/transforms/transform_alerts_out.ts | 34 ++- .../alerts/components/slo_alerts_summary.tsx | 4 +- .../alerts/components/slo_alerts_table.tsx | 22 +- .../alerts/slo_alerts_embeddable_factory.tsx | 42 +-- .../slo/alerts/slo_alerts_wrapper.tsx | 14 +- .../slo/alerts/slo_configuration.tsx | 254 ++++++++++++------ .../slo/overview/slo_definition_selector.tsx | 33 ++- .../slo/overview/slo_instance_selector.tsx | 26 +- .../server/lib/embeddables/alerts_schema.ts | 4 - 10 files changed, 288 insertions(+), 201 deletions(-) diff --git a/x-pack/solutions/observability/plugins/slo/common/embeddables/alerts/transforms/transform_alerts_out.test.ts b/x-pack/solutions/observability/plugins/slo/common/embeddables/alerts/transforms/transform_alerts_out.test.ts index 2121f74a4513a..b65f76424705c 100644 --- a/x-pack/solutions/observability/plugins/slo/common/embeddables/alerts/transforms/transform_alerts_out.test.ts +++ b/x-pack/solutions/observability/plugins/slo/common/embeddables/alerts/transforms/transform_alerts_out.test.ts @@ -24,7 +24,6 @@ describe('transformAlertsOut', () => { } as unknown as AlertsEmbeddableState) ).toMatchInlineSnapshot(` Object { - "show_all_group_by_instances": true, "slos": Array [ Object { "group_by": Array [ @@ -32,7 +31,7 @@ describe('transformAlertsOut', () => { ], "name": "Legacy SLO", "slo_id": "legacy-slo-id", - "slo_instance_id": "legacy-instance-id", + "slo_instance_id": "*", }, ], } @@ -42,7 +41,6 @@ describe('transformAlertsOut', () => { it('should return state unchanged when already in snake_case', () => { expect( transformAlertsOut({ - show_all_group_by_instances: false, slos: [ { slo_id: 'new-slo-id', @@ -54,7 +52,6 @@ describe('transformAlertsOut', () => { }) ).toMatchInlineSnapshot(` Object { - "show_all_group_by_instances": false, "slos": Array [ Object { "group_by": Array [ @@ -88,7 +85,6 @@ describe('transformAlertsOut', () => { } as unknown as AlertsEmbeddableState) ).toMatchInlineSnapshot(` Object { - "show_all_group_by_instances": true, "slos": Array [ Object { "group_by": Array [ @@ -103,52 +99,53 @@ describe('transformAlertsOut', () => { `); }); - it('should migrate legacy showAllGroupByInstances to show_all_group_by_instances', () => { + it('should migrate legacy showAllGroupByInstances by expanding instance to * for grouped SLOs', () => { expect( transformAlertsOut({ showAllGroupByInstances: true, - slos: [], + slos: [ + { + slo_id: 'slo-1', + slo_instance_id: 'instance-1', + name: 'SLO', + group_by: ['host.name'], + }, + ], } as unknown as AlertsEmbeddableState) ).toMatchInlineSnapshot(` Object { - "show_all_group_by_instances": true, - "slos": Array [], + "slos": Array [ + Object { + "group_by": Array [ + "host.name", + ], + "name": "SLO", + "slo_id": "slo-1", + "slo_instance_id": "*", + }, + ], } `); }); - it('should not include legacy showAllGroupByInstances in output', () => { + it('should not include show_all_group_by_instances in output', () => { const result = transformAlertsOut({ showAllGroupByInstances: false, + show_all_group_by_instances: true, slos: [], } as unknown as AlertsEmbeddableState); expect(result).not.toHaveProperty('showAllGroupByInstances'); - expect(result).toHaveProperty('show_all_group_by_instances', false); - }); - - it('should default show_all_group_by_instances to false when missing', () => { - expect(transformAlertsOut({ slos: [] } as unknown as AlertsEmbeddableState)).toMatchObject({ - show_all_group_by_instances: false, - slos: [], - }); + expect(result).not.toHaveProperty('show_all_group_by_instances'); + expect(result).toMatchObject({ slos: [] }); }); it('should handle empty slos array', () => { - expect( - transformAlertsOut({ - show_all_group_by_instances: false, - slos: [], - }) - ).toEqual({ - show_all_group_by_instances: false, - slos: [], - }); + expect(transformAlertsOut({ slos: [] })).toEqual({ slos: [] }); }); it('should handle mixed legacy and snake_case slo items', () => { expect( transformAlertsOut({ - show_all_group_by_instances: false, slos: [ { slo_id: 'snake-slo', @@ -166,7 +163,6 @@ describe('transformAlertsOut', () => { } as unknown as AlertsEmbeddableState) ).toMatchInlineSnapshot(` Object { - "show_all_group_by_instances": false, "slos": Array [ Object { "group_by": Array [], @@ -191,12 +187,10 @@ describe('transformAlertsOut', () => { expect( transformAlertsOut({ title: 'My Alerts Panel', - show_all_group_by_instances: false, slos: [], } as unknown as AlertsEmbeddableState) ).toMatchObject({ title: 'My Alerts Panel', - show_all_group_by_instances: false, slos: [], }); }); diff --git a/x-pack/solutions/observability/plugins/slo/common/embeddables/alerts/transforms/transform_alerts_out.ts b/x-pack/solutions/observability/plugins/slo/common/embeddables/alerts/transforms/transform_alerts_out.ts index 78e4ae5f7eaa0..2943d92964325 100644 --- a/x-pack/solutions/observability/plugins/slo/common/embeddables/alerts/transforms/transform_alerts_out.ts +++ b/x-pack/solutions/observability/plugins/slo/common/embeddables/alerts/transforms/transform_alerts_out.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { ALL_VALUE } from '@kbn/slo-schema'; import type { AlertsEmbeddableState, SloItem, @@ -18,18 +19,25 @@ export interface LegacyAlertsSloItem { } /** Maps legacy camelCase slo item to current snake_case schema. */ -function mapLegacySloItem(slo: Record): SloItem { +function mapLegacySloItem( + slo: Record, + legacyShowAllGroupByInstances: boolean +): SloItem { const rawGroupBy = (slo.group_by ?? slo.groupBy) as string[] | undefined; + let sloInstanceId = (slo.slo_instance_id as string) ?? (slo.instanceId as string) ?? ''; + if (legacyShowAllGroupByInstances && sloInstanceId && sloInstanceId !== ALL_VALUE) { + sloInstanceId = ALL_VALUE; + } return { slo_id: (slo.slo_id as string) ?? (slo.id as string) ?? '', - slo_instance_id: (slo.slo_instance_id as string) ?? (slo.instanceId as string) ?? '', + slo_instance_id: sloInstanceId, name: (slo.name as string) ?? '', group_by: Array.isArray(rawGroupBy) ? rawGroupBy : [], }; } export interface LegacyAlertsState { - showAllGroupByInstances: boolean; + showAllGroupByInstances?: boolean; slos: LegacyAlertsSloItem[]; } @@ -38,14 +46,24 @@ export function transformAlertsOut(storedState: AlertsEmbeddableState): AlertsEm const state = storedState as AlertsEmbeddableState & { slos?: Array>; showAllGroupByInstances?: boolean; + show_all_group_by_instances?: boolean; }; - const { showAllGroupByInstances: _legacy, ...rest } = state; + const legacyShowAll = + state.show_all_group_by_instances ?? state.showAllGroupByInstances ?? false; + const { showAllGroupByInstances: _legacy1, show_all_group_by_instances: _legacy2, ...rest } = + state; const slos = state.slos?.map((slo) => { const hasLegacy = 'id' in slo || 'instanceId' in slo || 'groupBy' in slo; - return hasLegacy ? mapLegacySloItem(slo) : (slo as SloItem); + return hasLegacy + ? mapLegacySloItem(slo, legacyShowAll) + : (() => { + const item = slo as SloItem; + if (legacyShowAll && item.slo_instance_id && item.slo_instance_id !== ALL_VALUE) { + return { ...item, slo_instance_id: ALL_VALUE }; + } + return item; + })(); }) ?? []; - const showAllGroupByInstances = - state.show_all_group_by_instances ?? state.showAllGroupByInstances ?? false; - return { ...rest, slos, show_all_group_by_instances: showAllGroupByInstances }; + return { ...rest, slos }; } diff --git a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/components/slo_alerts_summary.tsx b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/components/slo_alerts_summary.tsx index 8771703ecd2c2..e21319baf109a 100644 --- a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/components/slo_alerts_summary.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/components/slo_alerts_summary.tsx @@ -23,7 +23,6 @@ interface Props { slos: SloItem[]; timeRange: TimeRange; onLoaded?: () => void; - showAllGroupByInstances?: boolean; } export function SloAlertsSummary({ @@ -31,13 +30,12 @@ export function SloAlertsSummary({ deps, timeRange, onLoaded, - showAllGroupByInstances, }: Props) { const { triggersActionsUi: { getAlertSummaryWidget: AlertSummaryWidget }, } = deps; - const esQuery = useSloAlertsQuery(slos, timeRange, showAllGroupByInstances); + const esQuery = useSloAlertsQuery(slos, timeRange); const timeBuckets = useTimeBuckets(); const bucketSize = useMemo( () => diff --git a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/components/slo_alerts_table.tsx b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/components/slo_alerts_table.tsx index bdec04711d011..94ccfd6789b54 100644 --- a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/components/slo_alerts_table.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/components/slo_alerts_table.tsx @@ -78,14 +78,9 @@ interface Props { timeRange: TimeRange; onLoaded?: () => void; lastReloadRequestTime: number | undefined; - showAllGroupByInstances?: boolean; } -export const getSloInstanceFilter = ( - sloId: string, - sloInstanceId: string, - showAllGroupByInstances = false -) => { +export const getSloInstanceFilter = (sloId: string, sloInstanceId: string) => { return { bool: { must: [ @@ -94,7 +89,7 @@ export const getSloInstanceFilter = ( 'slo.id': sloId, }, }, - ...(sloInstanceId !== ALL_VALUE && !showAllGroupByInstances + ...(sloInstanceId !== ALL_VALUE ? [ { term: { @@ -108,11 +103,7 @@ export const getSloInstanceFilter = ( }; }; -export const useSloAlertsQuery = ( - slos: SloItem[], - timeRange: TimeRange, - showAllGroupByInstances?: boolean -) => { +export const useSloAlertsQuery = (slos: SloItem[], timeRange: TimeRange) => { return useMemo(() => { if (slos.length === 0) { return { @@ -140,7 +131,7 @@ export const useSloAlertsQuery = ( { bool: { should: slos.map((slo) => - getSloInstanceFilter(slo.slo_id, slo.slo_instance_id, showAllGroupByInstances) + getSloInstanceFilter(slo.slo_id, slo.slo_instance_id) ), }, }, @@ -149,7 +140,7 @@ export const useSloAlertsQuery = ( }; return query; - }, [showAllGroupByInstances, slos, timeRange.from]); + }, [slos, timeRange.from]); }; export function SloAlertsTable({ @@ -158,7 +149,6 @@ export function SloAlertsTable({ timeRange, onLoaded, lastReloadRequestTime, - showAllGroupByInstances, }: Props) { const ref = useRef(null); @@ -172,7 +162,7 @@ export function SloAlertsTable({ id={ALERTS_TABLE_ID} ruleTypeIds={[SLO_BURN_RATE_RULE_TYPE_ID]} consumers={[AlertConsumers.SLO, AlertConsumers.ALERTS, AlertConsumers.OBSERVABILITY]} - query={useSloAlertsQuery(slos, timeRange, showAllGroupByInstances)} + query={useSloAlertsQuery(slos, timeRange)} columns={columns} hideLazyLoader pageSize={ALERTS_PER_PAGE} diff --git a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.tsx b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.tsx index 5cc8ffa29c152..d2efd9e97122d 100644 --- a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_alerts_embeddable_factory.tsx @@ -8,7 +8,6 @@ import type { CoreStart } from '@kbn/core-lifecycle-browser'; import type { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; import { i18n } from '@kbn/i18n'; -import { ALL_VALUE } from '@kbn/slo-schema'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import type { FetchContext } from '@kbn/presentation-publishing'; @@ -77,22 +76,9 @@ export function getAlertsEmbeddableFactory({ } const titleManager = initializeTitleManager(initialState); - const hasSlosWithAllInstances = initialState?.slos?.some( - (slo) => slo.slo_instance_id === ALL_VALUE - ); - const normalizedInitialState: AlertsCustomState = { - ...initialState, - show_all_group_by_instances: hasSlosWithAllInstances - ? true - : initialState?.show_all_group_by_instances ?? false, - }; - const sloAlertsStateManager = initializeStateManager( - normalizedInitialState, - { - slos: [], - show_all_group_by_instances: false, - } - ); + const sloAlertsStateManager = initializeStateManager(initialState, { + slos: [], + }); const defaultTitle$ = new BehaviorSubject(getAlertsPanelTitle()); const reload$ = new Subject(); @@ -117,7 +103,6 @@ export function getAlertsEmbeddableFactory({ ...titleComparators, ...drilldownsManager.comparators, slos: 'referenceEquality', - show_all_group_by_instances: 'referenceEquality', }), onReset: (lastSaved) => { drilldownsManager.reinitializeState(lastSaved ?? {}); @@ -141,17 +126,10 @@ export function getAlertsEmbeddableFactory({ onEdit(); }, serializeState, - getSloAlertsConfig: () => { - return { - slos: sloAlertsStateManager.api.slos$.getValue(), - show_all_group_by_instances: - sloAlertsStateManager.api.showAllGroupByInstances$.getValue(), - }; - }, - updateSloAlertsConfig: (update) => { - sloAlertsStateManager.api.setSlos(update.slos); - sloAlertsStateManager.api.setShowAllGroupByInstances(update.show_all_group_by_instances); - }, + getSloAlertsConfig: () => ({ + slos: sloAlertsStateManager.api.slos$.getValue(), + }), + updateSloAlertsConfig: (update) => sloAlertsStateManager.api.setSlos(update.slos), }); const fetchSubscription = fetch$(api) @@ -163,10 +141,7 @@ export function getAlertsEmbeddableFactory({ return { api, Component: () => { - const [slos, showAllGroupByInstances] = useBatchedPublishingSubjects( - sloAlertsStateManager.api.slos$, - sloAlertsStateManager.api.showAllGroupByInstances$ - ); + const [slos] = useBatchedPublishingSubjects(sloAlertsStateManager.api.slos$); const fetchContext = useFetchContext(api); const I18nContext = deps.i18n.Context; @@ -202,7 +177,6 @@ export function getAlertsEmbeddableFactory({ slos={slos} timeRange={fetchContext.timeRange ?? { from: 'now-15m/m', to: 'now' }} reloadSubject={reload$} - showAllGroupByInstances={showAllGroupByInstances} /> diff --git a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_alerts_wrapper.tsx b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_alerts_wrapper.tsx index 1b1c80922e9f9..8011a9585c66d 100644 --- a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_alerts_wrapper.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_alerts_wrapper.tsx @@ -13,6 +13,7 @@ import type { Subject } from 'rxjs'; import { css } from '@emotion/react'; import { observabilityPaths } from '@kbn/observability-plugin/common'; import type { FetchContext } from '@kbn/presentation-publishing'; +import { ALL_VALUE } from '@kbn/slo-schema'; import { SloIncludedCount } from './components/slo_included_count'; import { SloAlertsSummary } from './components/slo_alerts_summary'; import { SloAlertsTable } from './components/slo_alerts_table'; @@ -24,7 +25,6 @@ interface Props { timeRange: TimeRange; onRenderComplete?: () => void; reloadSubject: Subject; - showAllGroupByInstances?: boolean; onEdit: () => void; } @@ -34,9 +34,9 @@ export function SloAlertsWrapper({ timeRange: initialTimeRange, onRenderComplete, reloadSubject, - showAllGroupByInstances, onEdit, }: Props) { + const showSloIncludedCount = slos.some((s) => s.slo_instance_id === ALL_VALUE); const { application: { navigateToUrl }, http: { basePath }, @@ -73,7 +73,11 @@ export function SloAlertsWrapper({ }, [isSummaryLoaded, isTableLoaded, onRenderComplete]); const handleGoToAlertsClick = () => { const kuery = slos - .map((slo) => `(slo.id:"${slo.slo_id}" and slo.instanceId:"${slo.slo_instance_id}")`) + .map((slo) => + slo.slo_instance_id === ALL_VALUE + ? `slo.id:"${slo.slo_id}"` + : `(slo.id:"${slo.slo_id}" and slo.instanceId:"${slo.slo_instance_id}")` + ) .join(' or '); navigateToUrl( @@ -104,7 +108,7 @@ export function SloAlertsWrapper({ }} data-test-subj="o11ySloAlertsWrapperSlOsIncludedLink" > - {showAllGroupByInstances ? ( + {showSloIncludedCount ? ( ) : ( i18n.translate('xpack.slo.sloAlertsWrapper.sLOsIncludedFlexItemLabel', { @@ -137,7 +141,6 @@ export function SloAlertsWrapper({ deps={deps} timeRange={timeRange} onLoaded={() => setIsSummaryLoaded(true)} - showAllGroupByInstances={showAllGroupByInstances} /> @@ -147,7 +150,6 @@ export function SloAlertsWrapper({ timeRange={timeRange} onLoaded={() => setIsTableLoaded(true)} lastReloadRequestTime={lastRefreshTime} - showAllGroupByInstances={showAllGroupByInstances} /> diff --git a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_configuration.tsx b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_configuration.tsx index 7df2b7285688c..7da3e57c7e514 100644 --- a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_configuration.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/alerts/slo_configuration.tsx @@ -14,19 +14,18 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, - EuiSpacer, - EuiSwitch, + EuiFormRow, EuiTitle, useGeneratedHtmlId, } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { SLOWithSummaryResponse } from '@kbn/slo-schema'; import { ALL_VALUE } from '@kbn/slo-schema'; -import React, { useEffect, useState } from 'react'; -import { useFetchSloList } from '../../../hooks/use_fetch_slo_list'; -import { SloSelector } from './slo_selector'; +import type { SearchSLODefinitionItem } from '@kbn/slo-schema'; +import React, { useState } from 'react'; +import { SloDefinitionSelector } from '../overview/slo_definition_selector'; +import { SloInstanceSelector } from '../overview/slo_instance_selector'; import type { AlertsCustomState, SloItem } from './types'; interface SloConfigurationProps { @@ -35,65 +34,98 @@ interface SloConfigurationProps { onCancel: () => void; } -function toSloItem(slo: SLOWithSummaryResponse): SloItem { +interface SloRow { + id: string; + sloDefinition?: SearchSLODefinitionItem; + instanceId?: string; +} + +function toSloItem(row: SloRow): SloItem | undefined { + if (!row.sloDefinition) return undefined; + const hasGroupBy = + row.sloDefinition.groupBy && + row.sloDefinition.groupBy.length > 0 && + !row.sloDefinition.groupBy.includes(ALL_VALUE); + return { - slo_id: slo.id, - slo_instance_id: slo.instanceId, - name: slo.name, - group_by: [slo.groupBy].flat().filter(Boolean) as string[], + slo_id: row.sloDefinition.id, + slo_instance_id: hasGroupBy ? row.instanceId ?? ALL_VALUE : ALL_VALUE, + name: row.sloDefinition.name, + group_by: [row.sloDefinition.groupBy].flat().filter(Boolean) as string[], }; } export function SloConfiguration({ initialInput, onCreate, onCancel }: SloConfigurationProps) { - const hasSlosWithAllInstances = initialInput?.slos?.some( - (slo) => slo.slo_instance_id === ALL_VALUE - ); - const sloIdsToExpand = - initialInput?.slos - ?.filter((slo) => slo.slo_instance_id === ALL_VALUE) - .map((slo) => slo.slo_id) ?? []; - - const { data: expandedSloList } = useFetchSloList({ - kqlQuery: sloIdsToExpand.map((id) => `slo.id:"${id}"`).join(' or '), - perPage: 100, - disabled: sloIdsToExpand.length === 0, - }); - - const [showAllGroupByInstances, setShowAllGroupByInstances] = useState(() => { - if (hasSlosWithAllInstances) { - return initialInput?.show_all_group_by_instances ?? true; + const [rows, setRows] = useState(() => { + const initial = initialInput?.slos ?? []; + if (initial.length === 0) { + return [{ id: 'row-0' }]; } - return initialInput?.show_all_group_by_instances ?? false; + return initial.map((slo, idx) => ({ + id: `row-${idx}`, + sloDefinition: { + id: slo.slo_id, + name: slo.name, + groupBy: slo.group_by, + } as SearchSLODefinitionItem, + instanceId: slo.slo_instance_id, + })); }); - const [selectedSlos, setSelectedSlos] = useState(initialInput?.slos ?? []); - const [hasExpandedSlos, setHasExpandedSlos] = useState(false); - - useEffect(() => { - if ( - sloIdsToExpand.length > 0 && - expandedSloList?.results && - expandedSloList.results.length > 0 && - !hasExpandedSlos - ) { - const instancesOnly = expandedSloList.results.filter((r) => r.instanceId !== ALL_VALUE); - if (instancesOnly.length > 0) { - const expandedItems = instancesOnly.map((r) => toSloItem(r)); - const slosWithSpecificInstances = initialInput!.slos!.filter( - (slo) => slo.slo_instance_id !== ALL_VALUE - ); - setSelectedSlos([...slosWithSpecificInstances, ...expandedItems]); - setShowAllGroupByInstances(true); - setHasExpandedSlos(true); - } - } - }, [expandedSloList?.results, initialInput, sloIdsToExpand.length, hasExpandedSlos]); const [hasError, setHasError] = useState(false); + const [rowErrors, setRowErrors] = useState>({}); + + const addRow = () => { + setRows((prev) => [...prev, { id: `row-${Date.now()}` }]); + }; - const onConfirmClick = () => - onCreate({ slos: selectedSlos, show_all_group_by_instances: showAllGroupByInstances }); + const removeRow = (id: string) => { + setRows((prev) => prev.filter((r) => r.id !== id)); + setRowErrors((prev) => { + const next = { ...prev }; + delete next[id]; + return next; + }); + }; + + const updateRow = (id: string, update: Partial) => { + setRows((prev) => prev.map((r) => (r.id === id ? { ...r, ...update } : r))); + }; - const hasGroupBy = (selectedSlos?.length ?? 0) > 0; + const onConfirmClick = () => { + const slos: SloItem[] = []; + const errors: Record = {}; + + for (const row of rows) { + const item = toSloItem(row); + if (!item) { + if (row.sloDefinition !== undefined || row.instanceId !== undefined) { + errors[row.id] = true; + } + continue; + } + if (!row.sloDefinition) { + errors[row.id] = true; + continue; + } + const hasGroupBy = + row.sloDefinition.groupBy && + row.sloDefinition.groupBy.length > 0 && + !row.sloDefinition.groupBy.includes(ALL_VALUE); + if (hasGroupBy && !row.instanceId) { + errors[row.id] = true; + continue; + } + slos.push(item); + } + + setRowErrors(errors); + setHasError(Object.keys(errors).length > 0); + + if (Object.keys(errors).length === 0 && slos.length > 0) { + onCreate({ slos }); + } + }; const flyoutTitleId = useGeneratedHtmlId({ prefix: 'alertsConfigurationFlyout', @@ -117,42 +149,86 @@ export function SloConfiguration({ initialInput, onCreate, onCancel }: SloConfig - - - { - setHasError(slos === undefined); - if (Array.isArray(slos)) { - setSelectedSlos( - slos?.map((slo) => ({ - slo_id: slo?.id ?? '', - slo_instance_id: slo?.instanceId ?? '', - name: slo?.name ?? '', - group_by: [slo?.groupBy].flat().filter(Boolean) as string[], - })) as SloItem[] - ); - } - }} - /> + + {rows.map((row) => { + const hasGroupBy = + row.sloDefinition?.groupBy && + row.sloDefinition.groupBy.length > 0 && + !row.sloDefinition.groupBy.includes(ALL_VALUE); + const showInstanceSelector = hasGroupBy && !!row.sloDefinition; + const rowHasError = rowErrors[row.id]; + + return ( + + + + + + { + updateRow(row.id, { + sloDefinition: slo, + instanceId: undefined, + }); + setRowErrors((prev) => ({ ...prev, [row.id]: slo === undefined })); + }} + hasError={rowHasError && !row.sloDefinition} + /> + + {showInstanceSelector && ( + + { + updateRow(row.id, { instanceId }); + }} + hasError={rowHasError && hasGroupBy && !row.instanceId} + /> + + )} + + + + removeRow(row.id)} + aria-label={i18n.translate('xpack.slo.sloConfiguration.removeRowAriaLabel', { + defaultMessage: 'Remove SLO', + })} + data-test-subj={`sloAlertsConfigRemoveRow-${row.id}`} + /> + + + + ); + })} + + + + {i18n.translate('xpack.slo.sloConfiguration.addSloButton', { + defaultMessage: 'Add SLO', + })} + + - {hasGroupBy && ( - <> - - { - setShowAllGroupByInstances(e.target.checked); - }} - /> - - )} @@ -165,7 +241,7 @@ export function SloConfiguration({ initialInput, onCreate, onCancel }: SloConfig !r.sloDefinition)} onClick={onConfirmClick} fill > diff --git a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/overview/slo_definition_selector.tsx b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/overview/slo_definition_selector.tsx index d20a120bdf793..123d3d05750f5 100644 --- a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/overview/slo_definition_selector.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/overview/slo_definition_selector.tsx @@ -17,15 +17,25 @@ interface Props { onSelected: (slo: SearchSLODefinitionItem | undefined) => void; hasError?: boolean; remoteName?: string; + initialSelected?: SearchSLODefinitionItem; } const SLO_REQUIRED = i18n.translate('xpack.slo.sloEmbeddable.config.errors.sloRequired', { defaultMessage: 'SLO is required.', }); -export function SloDefinitionSelector({ onSelected, hasError, remoteName }: Props) { - const [selectedOptions, setSelectedOptions] = useState>>( - [] +export function SloDefinitionSelector({ + onSelected, + hasError, + remoteName, + initialSelected, +}: Props) { + const [selectedOptions, setSelectedOptions] = useState< + Array> + >(() => + initialSelected + ? [{ label: initialSelected.name, value: initialSelected.id }] + : [] ); const [searchValue, setSearchValue] = useState(''); const search = searchValue.trim(); @@ -37,13 +47,22 @@ export function SloDefinitionSelector({ onSelected, hasError, remoteName }: Prop }); const options = useMemo(() => { - return ( + const fromApi = definitionsData?.results.map((slo) => ({ label: slo.name, value: slo.id, - })) ?? [] - ); - }, [definitionsData]); + })) ?? []; + if ( + initialSelected && + !fromApi.some((o) => o.value === initialSelected.id) + ) { + return [ + { label: initialSelected.name, value: initialSelected.id }, + ...fromApi, + ]; + } + return fromApi; + }, [definitionsData, initialSelected]); const onChange = (opts: Array>) => { setSelectedOptions(opts); diff --git a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/overview/slo_instance_selector.tsx b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/overview/slo_instance_selector.tsx index 41f85c54c591b..282fdb5c0f4fe 100644 --- a/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/overview/slo_instance_selector.tsx +++ b/x-pack/solutions/observability/plugins/slo/public/embeddable/slo/overview/slo_instance_selector.tsx @@ -18,6 +18,7 @@ interface Props { remoteName?: string; onSelected: (instanceId: string | undefined) => void; hasError?: boolean; + initialSelected?: string; } const ALL_OPTION: EuiComboBoxOptionOption = { @@ -27,9 +28,23 @@ const ALL_OPTION: EuiComboBoxOptionOption = { value: ALL_VALUE, }; -export function SloInstanceSelector({ sloId, remoteName, onSelected, hasError }: Props) { - const [selectedOptions, setSelectedOptions] = useState>>( - [] +export function SloInstanceSelector({ + sloId, + remoteName, + onSelected, + hasError, + initialSelected, +}: Props) { + const [selectedOptions, setSelectedOptions] = useState< + Array> + >(() => + initialSelected !== undefined + ? [ + initialSelected === ALL_VALUE + ? ALL_OPTION + : { label: initialSelected, value: initialSelected }, + ] + : [] ); const [search, setSearch] = useState(); const [debouncedSearch, setDebouncedSearch] = useState(search); @@ -48,6 +63,11 @@ export function SloInstanceSelector({ sloId, remoteName, onSelected, hasError }: label: instance.instanceId, value: instance.instanceId, })) ?? []), + ...(initialSelected && + initialSelected !== ALL_VALUE && + !instancesData?.results?.some((r) => r.instanceId === initialSelected) + ? [{ label: initialSelected, value: initialSelected }] + : []), ]; const onChange = (opts: Array>) => { diff --git a/x-pack/solutions/observability/plugins/slo/server/lib/embeddables/alerts_schema.ts b/x-pack/solutions/observability/plugins/slo/server/lib/embeddables/alerts_schema.ts index f49fd188f8013..07367612ce18a 100644 --- a/x-pack/solutions/observability/plugins/slo/server/lib/embeddables/alerts_schema.ts +++ b/x-pack/solutions/observability/plugins/slo/server/lib/embeddables/alerts_schema.ts @@ -28,10 +28,6 @@ const AlertsCustomSchema = schema.object({ defaultValue: [], meta: { description: 'List of SLOs to display alerts for' }, }), - show_all_group_by_instances: schema.boolean({ - defaultValue: false, - meta: { description: 'Whether to show all group-by instances' }, - }), }); export const getAlertsEmbeddableSchema = (getDrilldownsSchema: GetDrilldownsSchemaFnType) => {