From 0941817c30b6714245cc730e16e6e78d3190ad26 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Thu, 12 Jun 2025 19:32:34 -0400 Subject: [PATCH] [Incident Management] [Related dashboards api] Handle missing lens attributes gracefully (#223619) ## Summary Relates to https://github.com/elastic/kibana/issues/212125 When a Lens panel is saved to a dashboards, it is either saved by value or by reference. If it's saved by reference, the attributes will not be available. This was creating a reference error when attempting to inspect the lens configuration to make suggestions. This PR confirms that the attributes are available. A follow up PR will fetch the values for the references to include them in the evaluation. (cherry picked from commit f6da3f2cbc604c73b9d756306cf0226865b544d8) --- .../related_dashboards_client.test.ts | 56 +++++++++++++++++++ .../services/related_dashboards_client.ts | 47 ++++++++++------ 2 files changed, 87 insertions(+), 16 deletions(-) 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 542f4c955fd2a..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 @@ -123,6 +123,7 @@ describe('RelatedDashboardsClient', () => { }, }, }, + type: 'lens', }, }, }, @@ -149,6 +150,7 @@ describe('RelatedDashboardsClient', () => { }, }, }, + type: 'lens', }, }, }, @@ -182,6 +184,7 @@ describe('RelatedDashboardsClient', () => { formBased: { layers: [{ columns: [{ sourceField: 'field2' }] }] }, }, }, + type: 'lens', }, }, panelIndex: expect.any(String), @@ -212,6 +215,7 @@ describe('RelatedDashboardsClient', () => { formBased: { layers: [{ columns: [{ sourceField: 'field1' }] }] }, }, }, + type: 'lens', }, }, panelIndex: expect.any(String), @@ -233,6 +237,7 @@ describe('RelatedDashboardsClient', () => { formBased: { layers: [{ columns: [{ sourceField: 'field1' }] }] }, }, }, + type: 'lens', }, }, panelIndex: expect.any(String), @@ -277,6 +282,7 @@ describe('RelatedDashboardsClient', () => { formBased: { layers: [{ columns: [{ sourceField: 'field1' }] }] }, }, }, + type: 'lens', }, }, }, @@ -324,6 +330,7 @@ describe('RelatedDashboardsClient', () => { formBased: { layers: [{ columns: [{ sourceField: 'field1' }] }] }, // matches by field which is handled by getDashboardsByField }, }, + type: 'lens', }, }, }, @@ -357,6 +364,7 @@ describe('RelatedDashboardsClient', () => { formBased: { layers: [{ columns: [{ sourceField: 'field1' }] }] }, }, }, + type: 'lens', }, }, panelIndex: '123', @@ -407,6 +415,7 @@ describe('RelatedDashboardsClient', () => { type: 'lens', panelConfig: { attributes: { + type: 'lens', references: [ { name: 'indexpattern', id: 'index2' }, { name: 'irrelevant', id: 'index1' }, @@ -430,6 +439,50 @@ describe('RelatedDashboardsClient', () => { index: ['index2'], }); }); + + it('should return an empty set when lens attributes are not available', () => { + client.dashboardsById.set('dashboard1', { + id: 'dashboard1', + attributes: { + title: 'Dashboard 1', + panels: [ + { + type: 'lens', + panelConfig: { + attributes: null, // Lens attributes are not available + }, + }, + ], + }, + } as any); + + // @ts-ignore next-line + const result = client.getDashboardsByIndex('index1'); + expect(result.dashboards).toEqual([]); + }); + }); + + describe('getPanelsByField', () => { + it('should return an empty set when lens attributes are not available', () => { + client.dashboardsById.set('dashboard1', { + id: 'dashboard1', + attributes: { + title: 'Dashboard 1', + panels: [ + { + type: 'lens', + panelConfig: { + attributes: null, // Lens attributes are not available + }, + }, + ], + }, + } as any); + + // @ts-ignore next-line + const result = client.getDashboardsByField(['field1']); + expect(result.dashboards).toEqual([]); + }); }); describe('dedupePanels', () => { @@ -669,6 +722,7 @@ describe('RelatedDashboardsClient', () => { formBased: { layers: [{ columns: [{ sourceField: 'field1' }] }] }, // matches by field which is handled by getDashboardsByField }, }, + type: 'lens', }, }, }, @@ -691,6 +745,7 @@ describe('RelatedDashboardsClient', () => { formBased: { layers: [{ columns: [{ sourceField: 'field2' }] }] }, }, }, + type: 'lens', }, }, }, @@ -732,6 +787,7 @@ describe('RelatedDashboardsClient', () => { formBased: { layers: [{ columns: [{ sourceField: 'field1' }] }] }, }, }, + type: 'lens', }, }, panelIndex: '123', 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 eb0e4e22b01ff..fcad09ca21022 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 @@ -224,7 +224,7 @@ export class RelatedDashboardsClient { return { dashboards: relevantDashboards }; } - getPanelsByIndex(index: string, panels: DashboardAttributes['panels']): DashboardPanel[] { + private getPanelsByIndex(index: string, panels: DashboardAttributes['panels']): DashboardPanel[] { const panelsByIndex = panels.filter((p) => { if (isDashboardSection(p)) return false; // filter out sections const panelIndices = this.getPanelIndices(p); @@ -233,7 +233,7 @@ export class RelatedDashboardsClient { return panelsByIndex; } - getPanelsByField( + private getPanelsByField( fields: string[], panels: DashboardAttributes['panels'] ): Array<{ matchingFields: Set; panel: DashboardPanel }> { @@ -249,31 +249,46 @@ export class RelatedDashboardsClient { return panelsByField; } - getPanelIndices(panel: DashboardPanel): Set { - const indices = new Set(); + private getPanelIndices(panel: DashboardPanel): Set { + const emptyIndicesSet = new Set(); switch (panel.type) { case 'lens': - const lensAttr = panel.panelConfig.attributes as unknown as LensAttributes; - if (!lensAttr) { - return indices; + const maybeLensAttr = panel.panelConfig.attributes; + if (this.isLensVizAttributes(maybeLensAttr)) { + const lensIndices = this.getLensVizIndices(maybeLensAttr); + return lensIndices; } - const lensIndices = this.getLensVizIndices(lensAttr); - return lensIndices; + return emptyIndicesSet; default: - return indices; + return emptyIndicesSet; } } - getPanelFields(panel: DashboardPanel): Set { - const fields = new Set(); + private getPanelFields(panel: DashboardPanel): Set { + const emptyFieldsSet = new Set(); switch (panel.type) { case 'lens': - const lensAttr = panel.panelConfig.attributes as unknown as LensAttributes; - const lensFields = this.getLensVizFields(lensAttr); - return lensFields; + const maybeLensAttr = panel.panelConfig.attributes; + if (this.isLensVizAttributes(maybeLensAttr)) { + const lensFields = this.getLensVizFields(maybeLensAttr); + return lensFields; + } + return emptyFieldsSet; default: - return fields; + return emptyFieldsSet; + } + } + + private isLensVizAttributes(attributes: unknown): attributes is LensAttributes { + if (!attributes) { + return false; } + return ( + Boolean(attributes) && + typeof attributes === 'object' && + 'type' in attributes && + attributes.type === 'lens' + ); } private getRuleQueryIndex(): string | null {