Skip to content

Commit b797833

Browse files
committed
feat(panel references): when fetching dashboards referenced panel are also fetched
1 parent d767a66 commit b797833

File tree

5 files changed

+139
-22
lines changed

5 files changed

+139
-22
lines changed

src/platform/plugins/shared/dashboard/common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ export {
2121
} from './reference_utils';
2222

2323
export { isDashboardSection } from './is_dashboard_section';
24+
export { isDashboardPanel } from './is_dashboard_panel';
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import type { DashboardAttributes, DashboardPanel } from '../server/content_management';
11+
12+
export const isDashboardPanel = (
13+
widget: DashboardAttributes['panels'][number]
14+
): widget is DashboardPanel => {
15+
return 'panelConfig' in widget;
16+
};

x-pack/solutions/observability/plugins/observability/server/routes/alerts/route.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const alertsDynamicDashboardSuggestions = createObservabilityServerRoute({
3434
const { alertId } = params.query;
3535
const { ruleRegistry, dashboard } = dependencies;
3636
const { getContentClient } = dashboard;
37+
const { savedObjects } = await context.core;
38+
3739
const dashboardClient = getContentClient()!.getForRequest<
3840
SavedObjectsFindResult<DashboardAttributes>
3941
>({
@@ -50,7 +52,8 @@ const alertsDynamicDashboardSuggestions = createObservabilityServerRoute({
5052
logger,
5153
dashboardClient,
5254
investigateAlertsClient,
53-
alertId
55+
alertId,
56+
savedObjects.client
5457
);
5558
try {
5659
const { suggestedDashboards, linkedDashboards } =

x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.test.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@
66
*/
77
import Boom from '@hapi/boom';
88
import { RelatedDashboardsClient } from './related_dashboards_client';
9-
import { Logger } from '@kbn/core/server';
9+
import { Logger, SavedObjectsClientContract } from '@kbn/core/server';
1010
import { IContentClient } from '@kbn/content-management-plugin/server/types';
1111
import { InvestigateAlertsClient } from './investigate_alerts_client';
1212
import { AlertData } from './alert_data';
1313
import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils';
14+
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
1415

1516
describe('RelatedDashboardsClient', () => {
1617
let logger: jest.Mocked<Logger>;
1718
let dashboardClient: jest.Mocked<IContentClient<any>>;
1819
let alertsClient: jest.Mocked<InvestigateAlertsClient>;
1920
let alertId: string;
2021
let client: RelatedDashboardsClient;
22+
let soClientMock: jest.Mocked<SavedObjectsClientContract>;
2123
const baseMockAlert = {
2224
getAllRelevantFields: jest.fn().mockReturnValue(['field1', 'field2']),
2325
getRuleQueryIndex: jest.fn().mockReturnValue('index1'),
@@ -60,7 +62,15 @@ describe('RelatedDashboardsClient', () => {
6062

6163
alertId = 'test-alert-id';
6264

63-
client = new RelatedDashboardsClient(logger, dashboardClient, alertsClient, alertId);
65+
soClientMock = savedObjectsClientMock.create();
66+
67+
client = new RelatedDashboardsClient(
68+
logger,
69+
dashboardClient,
70+
alertsClient,
71+
alertId,
72+
soClientMock
73+
);
6474

6575
jest.clearAllMocks();
6676
});
@@ -417,6 +427,46 @@ describe('RelatedDashboardsClient', () => {
417427
expect(dashboardClient.search).toHaveBeenCalledWith({ limit: 2, cursor: '1' });
418428
expect(client.dashboardsById.size).toBe(2);
419429
});
430+
431+
it('should fetch referenced panels when fetching dashboards', async () => {
432+
const PANEL_SO_ID = 'panelSOId';
433+
const PANEL_TYPE = 'lens';
434+
const PANEL_INDEX = 'panelIndex';
435+
const PANEL_SO_ATTRIBUTES = { title: 'Panel 1' };
436+
dashboardClient.search.mockResolvedValue({
437+
contentTypeId: 'dashboard',
438+
result: {
439+
hits: [
440+
{
441+
id: 'dashboard1',
442+
attributes: {
443+
title: 'Dashboard 1',
444+
panels: [{ panelConfig: {}, panelIndex: PANEL_INDEX, type: PANEL_TYPE }],
445+
},
446+
references: [{ name: PANEL_INDEX, type: PANEL_TYPE, id: PANEL_SO_ID }],
447+
},
448+
],
449+
pagination: { total: 1 },
450+
},
451+
});
452+
453+
soClientMock.get.mockResolvedValueOnce({
454+
attributes: PANEL_SO_ATTRIBUTES,
455+
type: PANEL_TYPE,
456+
id: PANEL_SO_ID,
457+
references: [],
458+
});
459+
460+
// @ts-ignore next-line
461+
await client.fetchDashboards({ page: 1 });
462+
463+
expect(soClientMock.get).toHaveBeenCalledWith(PANEL_TYPE, PANEL_SO_ID);
464+
expect(client.dashboardsById.get('dashboard1')?.attributes.panels[0]).toStrictEqual({
465+
panelConfig: { attributes: PANEL_SO_ATTRIBUTES },
466+
panelIndex: PANEL_INDEX,
467+
type: PANEL_TYPE,
468+
});
469+
});
420470
});
421471

422472
describe('getDashboardsByIndex', () => {

x-pack/solutions/observability/plugins/observability/server/services/related_dashboards_client.ts

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
* 2.0.
66
*/
77
import { v4 as uuidv4 } from 'uuid';
8-
import { omit } from 'lodash';
8+
import { isEmpty, omit } from 'lodash';
99
import { IContentClient } from '@kbn/content-management-plugin/server/types';
10-
import type { Logger, SavedObjectsFindResult } from '@kbn/core/server';
11-
import { isDashboardSection } from '@kbn/dashboard-plugin/common';
10+
import type { Logger, SavedObjectsClientContract, SavedObjectsFindResult } from '@kbn/core/server';
11+
import { isDashboardPanel } from '@kbn/dashboard-plugin/common';
1212
import type { DashboardAttributes, DashboardPanel } from '@kbn/dashboard-plugin/server';
1313
import type {
1414
FieldBasedIndexPatternColumn,
@@ -33,7 +33,8 @@ export class RelatedDashboardsClient {
3333
private logger: Logger,
3434
private dashboardClient: IContentClient<Dashboard>,
3535
private alertsClient: InvestigateAlertsClient,
36-
private alertId: string
36+
private alertId: string,
37+
private soClient: SavedObjectsClientContract
3738
) {}
3839

3940
public async fetchRelatedDashboards(): Promise<{
@@ -110,6 +111,43 @@ export class RelatedDashboardsClient {
110111
return sortedDashboards;
111112
}
112113

114+
private async fetchReferencedPanel({
115+
dashboard,
116+
panel,
117+
}: {
118+
dashboard: Dashboard;
119+
panel: DashboardPanel;
120+
}): Promise<DashboardPanel | null> {
121+
const panelReference = dashboard.references.find(
122+
(r) => panel.panelIndex && r.name.includes(panel.panelIndex) && r.type === panel.type
123+
);
124+
125+
// A reference of the panel was not found
126+
if (!panelReference) {
127+
this.logger.error(
128+
`Reference for panel of type ${panel.type} and panelIndex ${panel.panelIndex} was not found in dashboard with id ${dashboard.id}`
129+
);
130+
return null;
131+
}
132+
133+
try {
134+
const so = await this.soClient.get(panel.type, panelReference.id);
135+
return {
136+
...panel,
137+
panelConfig: {
138+
...panel.panelConfig,
139+
attributes: so.attributes,
140+
},
141+
};
142+
} catch (error) {
143+
// There was an error fetching the referenced saved object
144+
this.logger.error(
145+
`Error fetching panel with type ${panel.type} and id ${panelReference.id}: ${error.message}`
146+
);
147+
return null;
148+
}
149+
}
150+
113151
private async fetchDashboards({
114152
page,
115153
perPage = 20,
@@ -123,9 +161,24 @@ export class RelatedDashboardsClient {
123161
const {
124162
result: { hits },
125163
} = dashboards;
126-
hits.forEach((dashboard: Dashboard) => {
127-
this.dashboardsById.set(dashboard.id, dashboard);
128-
});
164+
for (const dashboard of hits) {
165+
const panels: DashboardAttributes['panels'] = await Promise.all(
166+
dashboard.attributes.panels.map(async (panel) => {
167+
// Only fetch the panel if it's a panel (not a section) with an empty panelConfig
168+
if (!isDashboardPanel(panel) || !isEmpty(panel.panelConfig)) return panel;
169+
const referencedPanel = await this.fetchReferencedPanel({
170+
dashboard,
171+
panel,
172+
});
173+
return referencedPanel || panel;
174+
})
175+
);
176+
this.dashboardsById.set(dashboard.id, {
177+
...dashboard,
178+
attributes: { ...dashboard.attributes, panels },
179+
});
180+
}
181+
129182
const fetchedUntil = (page - 1) * perPage + dashboards.result.hits.length;
130183

131184
if (dashboards.result.pagination.total <= fetchedUntil) {
@@ -146,7 +199,7 @@ export class RelatedDashboardsClient {
146199
} {
147200
const relevantDashboards: SuggestedDashboard[] = [];
148201
this.dashboardsById.forEach((d) => {
149-
const panels = d.attributes.panels;
202+
const panels = d.attributes.panels.filter(isDashboardPanel);
150203
const matchingPanels = this.getPanelsByIndex(index, panels);
151204
if (matchingPanels.length > 0) {
152205
this.logger.debug(
@@ -190,7 +243,7 @@ export class RelatedDashboardsClient {
190243
} {
191244
const relevantDashboards: SuggestedDashboard[] = [];
192245
this.dashboardsById.forEach((d) => {
193-
const panels = d.attributes.panels;
246+
const panels = d.attributes.panels.filter(isDashboardPanel);
194247
const matchingPanels = this.getPanelsByField(fields, panels);
195248
const allMatchingFields = new Set(
196249
matchingPanels.map((p) => Array.from(p.matchingFields)).flat()
@@ -224,23 +277,17 @@ export class RelatedDashboardsClient {
224277
return { dashboards: relevantDashboards };
225278
}
226279

227-
private getPanelsByIndex(index: string, panels: DashboardAttributes['panels']): DashboardPanel[] {
228-
const panelsByIndex = panels.filter((p) => {
229-
if (isDashboardSection(p)) return false; // filter out sections
230-
const panelIndices = this.getPanelIndices(p);
231-
return panelIndices.has(index);
232-
}) as DashboardPanel[]; // filtering with type guard doesn't actually limit type, so need to cast
280+
private getPanelsByIndex(index: string, panels: DashboardPanel[]): DashboardPanel[] {
281+
const panelsByIndex = panels.filter((p) => this.getPanelIndices(p).has(index));
233282
return panelsByIndex;
234283
}
235284

236285
private getPanelsByField(
237286
fields: string[],
238-
panels: DashboardAttributes['panels']
287+
panels: DashboardPanel[]
239288
): Array<{ matchingFields: Set<string>; panel: DashboardPanel }> {
240289
const panelsByField = panels.reduce((acc, p) => {
241-
if (isDashboardSection(p)) return acc; // filter out sections
242-
const panelFields = this.getPanelFields(p);
243-
const matchingFields = fields.filter((f) => panelFields.has(f));
290+
const matchingFields = fields.filter((f) => this.getPanelFields(p).has(f));
244291
if (matchingFields.length) {
245292
acc.push({ matchingFields: new Set(matchingFields), panel: p });
246293
}

0 commit comments

Comments
 (0)