[Incident Management] [Suggested dashboards] Deduplicated linked dashboards from list of linked dashboards (#221972)

## Summary

Resolves https://github.com/elastic/kibana/issues/212801

Removes already linked dashboards from the list of suggested dashboards

Also has the side effect of returning the linked dashboards from the
related dashboards api, which can be used to render the linked
dashboards list along with the suggested dashboards, rather than calling
a separate API from the client.

---------

Co-authored-by: Justin Kambic <jk@elastic.co>
This commit is contained in:
Dominique Clarke 2025-06-11 21:18:22 -04:00 committed by GitHub
parent 2c5d5a49d7
commit 33825663e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 409 additions and 68 deletions

View file

@ -6,7 +6,12 @@
*/
export { type RelevantPanel, relevantPanelSchema } from './schema/relevant_panel/v1';
export { type RelatedDashboard, relatedDashboardSchema } from './schema/related_dashboard/v1';
export {
type RelatedDashboard,
type SuggestedDashboard,
relatedDashboardSchema,
suggestedDashboardSchema,
} from './schema/related_dashboard/v1';
export {
type GetRelatedDashboardsResponse,
getRelatedDashboardsResponseSchema,

View file

@ -9,6 +9,18 @@ import { z } from '@kbn/zod';
import { relevantPanelSchema } from '../relevant_panel/latest';
export const relatedDashboardSchema = z.object({
id: z.string(),
title: z.string(),
matchedBy: z.object({
fields: z.array(z.string()).optional(),
index: z.array(z.string()).optional(),
linked: z.boolean().optional(),
}),
relevantPanelCount: z.number().optional(),
relevantPanels: z.array(relevantPanelSchema).optional(),
});
export const suggestedDashboardSchema = z.object({
id: z.string(),
title: z.string(),
matchedBy: z.object({
@ -21,3 +33,4 @@ export const relatedDashboardSchema = z.object({
});
export type RelatedDashboard = z.output<typeof relatedDashboardSchema>;
export type SuggestedDashboard = z.output<typeof suggestedDashboardSchema>;

View file

@ -42,7 +42,8 @@ const alertsDynamicDashboardSuggestions = createObservabilityServerRoute({
});
const alertsClient = await ruleRegistry.getRacClientWithRequest(request);
const investigateAlertsClient = new InvestigateAlertsClient(alertsClient);
const rulesClient = await ruleRegistry.alerting.getRulesClientWithRequest(request);
const investigateAlertsClient = new InvestigateAlertsClient(alertsClient, rulesClient);
const dashboardParser = new RelatedDashboardsClient(
logger,
@ -51,10 +52,11 @@ const alertsDynamicDashboardSuggestions = createObservabilityServerRoute({
alertId
);
try {
const { suggestedDashboards } = await dashboardParser.fetchSuggestedDashboards();
const { suggestedDashboards, linkedDashboards } =
await dashboardParser.fetchRelatedDashboards();
return {
suggestedDashboards,
linkedDashboards: [],
linkedDashboards,
};
} catch (e) {
if (e instanceof AlertNotFoundError) {

View file

@ -7,21 +7,27 @@
import { omit } from 'lodash';
import { CustomThresholdParams } from '@kbn/response-ops-rule-params/custom_threshold';
import type { AlertsClient } from '@kbn/rule-registry-plugin/server';
import { DataViewSpec } from '@kbn/response-ops-rule-params/common';
import {
ALERT_RULE_PARAMETERS,
ALERT_RULE_TYPE_ID,
ALERT_RULE_UUID,
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
fields as TECHNICAL_ALERT_FIELDS,
} from '@kbn/rule-data-utils';
export class AlertData {
constructor(private alert: any) {}
constructor(private alert: Awaited<ReturnType<AlertsClient['get']>>) {}
getRuleParameters() {
return this.alert[ALERT_RULE_PARAMETERS];
}
getRuleId() {
return this.alert[ALERT_RULE_UUID];
}
getRelevantRuleFields(): Set<string> {
const ruleParameters = this.getRuleParameters();
const relevantFields = new Set<string>();
@ -81,7 +87,7 @@ export class AlertData {
}
}
getRuleTypeId() {
getRuleTypeId(): string | undefined {
return this.alert[ALERT_RULE_TYPE_ID];
}
}

View file

@ -5,12 +5,13 @@
* 2.0.
*/
import { OBSERVABILITY_RULE_TYPE_IDS } from '@kbn/rule-data-utils';
import { AlertsClient } from '@kbn/rule-registry-plugin/server';
import type { RulesClientApi } from '@kbn/alerting-plugin/server/types';
import type { AlertsClient } from '@kbn/rule-registry-plugin/server';
import { AlertNotFoundError } from '../common/errors/alert_not_found_error';
import { AlertData } from './alert_data';
export class InvestigateAlertsClient {
constructor(private alertsClient: AlertsClient) {}
constructor(private alertsClient: AlertsClient, private rulesClient: RulesClientApi) {}
async getAlertById(alertId: string): Promise<AlertData> {
const indices = (await this.getAlertsIndices()) || [];
@ -31,6 +32,10 @@ export class InvestigateAlertsClient {
}
}
async getRuleById(ruleId: string) {
return await this.rulesClient.get({ id: ruleId });
}
async getAlertsIndices() {
return await this.alertsClient.getAuthorizedAlertsIndices(OBSERVABILITY_RULE_TYPE_IDS);
}

View file

@ -17,6 +17,11 @@ describe('RelatedDashboardsClient', () => {
let alertsClient: jest.Mocked<InvestigateAlertsClient>;
let alertId: string;
let client: RelatedDashboardsClient;
const baseMockAlert = {
getAllRelevantFields: jest.fn().mockReturnValue(['field1', 'field2']),
getRuleQueryIndex: jest.fn().mockReturnValue('index1'),
getRuleId: jest.fn().mockReturnValue('rule-id'),
} as unknown as AlertData;
beforeEach(() => {
logger = {
@ -39,10 +44,16 @@ describe('RelatedDashboardsClient', () => {
pagination: { total: 2 },
},
}),
get: jest.fn(),
} as unknown as jest.Mocked<IContentClient<any>>;
alertsClient = {
getAlertById: jest.fn(),
getRuleById: jest.fn().mockResolvedValue({
artifacts: {
dashboards: [],
},
}),
} as unknown as jest.Mocked<InvestigateAlertsClient>;
alertId = 'test-alert-id';
@ -55,18 +66,13 @@ describe('RelatedDashboardsClient', () => {
// @ts-ignore next-line
alertsClient.getAlertById.mockResolvedValue(null);
await expect(client.fetchSuggestedDashboards()).rejects.toThrow(
await expect(client.fetchRelatedDashboards()).rejects.toThrow(
`Alert with id ${alertId} not found. Could not fetch related dashboards.`
);
});
it('should fetch dashboards and return suggested dashboards', async () => {
const mockAlert = {
getAllRelevantFields: jest.fn().mockReturnValue(['field1', 'field2']),
getRuleQueryIndex: jest.fn().mockReturnValue('index1'),
} as unknown as AlertData;
alertsClient.getAlertById.mockResolvedValue(mockAlert);
alertsClient.getAlertById.mockResolvedValue(baseMockAlert);
dashboardClient.search.mockResolvedValue({
contentTypeId: 'dashboard',
result: {
@ -75,7 +81,7 @@ describe('RelatedDashboardsClient', () => {
},
});
const result = await client.fetchSuggestedDashboards();
const result = await client.fetchRelatedDashboards();
expect(result.suggestedDashboards).toEqual([]);
expect(alertsClient.getAlertById).toHaveBeenCalledWith(alertId);
@ -83,12 +89,16 @@ describe('RelatedDashboardsClient', () => {
it('should sort dashboards by score', async () => {
const mockAlert = {
...baseMockAlert,
getAllRelevantFields: jest.fn().mockReturnValue(['field1']),
getRuleQueryIndex: jest.fn().mockReturnValue('index1'),
} as unknown as AlertData;
alertsClient.getAlertById.mockResolvedValue(mockAlert);
// @ts-ignore next-line
client.setAlert(mockAlert);
dashboardClient.search.mockResolvedValue({
contentTypeId: 'dashboard',
result: {
@ -150,7 +160,7 @@ describe('RelatedDashboardsClient', () => {
},
});
const result = await client.fetchSuggestedDashboards();
const result = await client.fetchRelatedDashboards();
expect(result.suggestedDashboards).toEqual([
{
@ -239,6 +249,7 @@ describe('RelatedDashboardsClient', () => {
it('should return only the top 10 results', async () => {
const mockAlert = {
...baseMockAlert,
getAllRelevantFields: jest.fn().mockReturnValue(['field1']),
getRuleQueryIndex: jest.fn().mockReturnValue('index1'),
} as unknown as AlertData;
@ -276,19 +287,23 @@ describe('RelatedDashboardsClient', () => {
},
});
const result = await client.fetchSuggestedDashboards();
const { suggestedDashboards } = await client.fetchRelatedDashboards();
expect(result.suggestedDashboards).toHaveLength(10);
expect(suggestedDashboards).toHaveLength(10);
});
it('should deduplicate dashboards found by field and index', async () => {
const mockAlert = {
...baseMockAlert,
getAllRelevantFields: jest.fn().mockReturnValue(['field1']),
getRuleQueryIndex: jest.fn().mockReturnValue('index1'),
} as unknown as AlertData;
alertsClient.getAlertById.mockResolvedValue(mockAlert);
// @ts-ignore next-line
client.setAlert(mockAlert);
dashboardClient.search.mockResolvedValue({
contentTypeId: 'dashboard',
result: {
@ -320,12 +335,12 @@ describe('RelatedDashboardsClient', () => {
},
});
const result = await client.fetchSuggestedDashboards();
const { suggestedDashboards } = await client.fetchRelatedDashboards();
expect(result.suggestedDashboards).toHaveLength(1);
expect(suggestedDashboards).toHaveLength(1);
// should return only one dashboard even though it was found by both internal methods
// should mark the relevant panel as matching by index and field
expect(result.suggestedDashboards).toEqual([
expect(suggestedDashboards).toEqual([
{
id: 'dashboard1',
matchedBy: { fields: ['field1'], index: ['index1'] },
@ -370,6 +385,7 @@ describe('RelatedDashboardsClient', () => {
},
});
// @ts-ignore next-line
await client.fetchDashboards({ page: 1, perPage: 2 });
expect(dashboardClient.search).toHaveBeenCalledWith(
@ -402,9 +418,11 @@ describe('RelatedDashboardsClient', () => {
},
} as any);
// @ts-ignore next-line
const resultWithoutMatch = client.getDashboardsByIndex('index1');
expect(resultWithoutMatch.dashboards).toEqual([]);
// @ts-ignore next-line
const resultWithMatch = client.getDashboardsByIndex('index2');
expect(resultWithMatch.dashboards).toHaveLength(1);
expect(resultWithMatch.dashboards[0].id).toBe('dashboard1');
@ -421,6 +439,7 @@ describe('RelatedDashboardsClient', () => {
{ panel: { panelIndex: '1' }, matchedBy: { fields: ['field1'] } },
];
// @ts-ignore next-line
const result = client.dedupePanels(panels as any);
expect(result).toHaveLength(1);
@ -431,16 +450,19 @@ describe('RelatedDashboardsClient', () => {
describe('getScore', () => {
it('should calculate the relevance score for a dashboard', () => {
const mockAlert = {
...baseMockAlert,
getAllRelevantFields: jest.fn().mockReturnValue(['field1', 'field2']),
getRuleQueryIndex: jest.fn().mockReturnValue('index1'),
} as unknown as AlertData;
// @ts-ignore next-line
client.setAlert(mockAlert);
const dashboard = {
matchedBy: { fields: ['field1'], index: ['index1'] },
} as any;
// @ts-ignore next-line
const score = client.getScore(dashboard);
expect(score).toBeCloseTo(2 / 3);
@ -449,9 +471,245 @@ describe('RelatedDashboardsClient', () => {
matchedBy: { fields: ['field1', 'field2'], index: ['index1'] },
} as any;
// @ts-ignore next-line
const score2 = client.getScore(dashboard2);
expect(score2).toBe(1);
});
});
describe('Linked Dashboards', () => {
describe('getLinkedDashboards', () => {
it('should throw an error if no alert is set', async () => {
// @ts-ignore next-line
client.setAlert(null);
// @ts-ignore next-line
await expect(client.getLinkedDashboards()).rejects.toThrow(
`Alert with id ${alertId} not found. Could not fetch related dashboards.`
);
});
it('should return an empty array if no rule ID is found', async () => {
const mockAlert = {
...baseMockAlert,
getRuleId: jest.fn().mockReturnValue(null),
} as unknown as AlertData;
// @ts-ignore next-line
client.setAlert(mockAlert);
// @ts-ignore next-line
await expect(client.getLinkedDashboards()).rejects.toThrow(
`Alert with id ${alertId} does not have a rule ID. Could not fetch linked dashboards.`
);
});
it('should return an empty array if no rule is found', 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(null);
// @ts-ignore next-line
await expect(client.getLinkedDashboards()).rejects.toThrow(
`Rule with id rule-id not found. Could not fetch linked dashboards for alert with id ${alertId}.`
);
});
it('should return linked dashboards based on rule artifacts', 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' } } },
})
.mockResolvedValueOnce({
result: { item: { id: 'dashboard2', attributes: { title: 'Dashboard 2' } } },
});
// @ts-ignore next-line
const result = await client.getLinkedDashboards();
expect(result).toEqual([
{ id: 'dashboard1', title: 'Dashboard 1', matchedBy: { linked: true } },
{ id: 'dashboard2', title: 'Dashboard 2', matchedBy: { linked: true } },
]);
});
});
describe('getLinkedDashboardsByIds', () => {
it('should return linked dashboards by IDs', async () => {
dashboardClient.get = jest
.fn()
.mockResolvedValueOnce({
result: { item: { id: 'dashboard1', attributes: { title: 'Dashboard 1' } } },
})
.mockResolvedValueOnce({
result: { item: { id: 'dashboard2', attributes: { title: 'Dashboard 2' } } },
});
// @ts-ignore next-line
const result = await client.getLinkedDashboardsByIds(['dashboard1', 'dashboard2']);
expect(result).toEqual([
{ id: 'dashboard1', title: 'Dashboard 1', matchedBy: { linked: true } },
{ id: 'dashboard2', title: 'Dashboard 2', matchedBy: { linked: true } },
]);
expect(dashboardClient.get).toHaveBeenCalledTimes(2);
expect(dashboardClient.get).toHaveBeenCalledWith('dashboard1');
expect(dashboardClient.get).toHaveBeenCalledWith('dashboard2');
});
it('should handle empty IDs array gracefully', async () => {
dashboardClient.get = jest.fn();
// @ts-ignore next-line
const result = await client.getLinkedDashboardsByIds([]);
expect(result).toEqual([]);
expect(dashboardClient.get).not.toHaveBeenCalled();
});
it('should handle errors when fetching dashboards', async () => {
dashboardClient.get = jest.fn().mockRejectedValue(new Error('Dashboard fetch failed'));
// @ts-ignore next-line
await expect(client.getLinkedDashboardsByIds(['dashboard1'])).rejects.toThrow(
'Dashboard fetch failed'
);
});
});
});
describe('deduplicateDashboards', () => {
it('should deduplicate suggested and linked dashboards', async () => {
const mockAlert = {
...baseMockAlert,
getAllRelevantFields: jest.fn().mockReturnValue(['field1']),
getRuleQueryIndex: jest.fn().mockReturnValue('index1'),
} as unknown as AlertData;
alertsClient.getAlertById.mockResolvedValue(mockAlert);
// @ts-ignore next-line
alertsClient.getRuleById.mockResolvedValue({
artifacts: {
dashboards: [{ id: 'dashboard2' }],
},
});
dashboardClient.get = jest.fn().mockResolvedValueOnce({
result: { item: { id: 'dashboard2', attributes: { title: 'Dashboard 2' } } },
});
dashboardClient.search.mockResolvedValue({
contentTypeId: 'dashboard',
result: {
hits: [
{
id: 'dashboard1',
attributes: {
title: 'Dashboard 1',
panels: [
{
type: 'lens',
panelIndex: '123',
panelConfig: {
attributes: {
references: [{ name: 'indexpattern', id: 'index1' }], // matches by index which is handled by getDashboardsByIndex
state: {
datasourceStates: {
formBased: { layers: [{ columns: [{ sourceField: 'field1' }] }] }, // matches by field which is handled by getDashboardsByField
},
},
},
},
},
],
},
},
{
id: 'dashboard2',
attributes: {
title: 'Dashboard 2',
panels: [
{
type: 'lens',
panelIndex: '123',
panelConfig: {
attributes: {
references: [{ name: 'indexpattern', id: 'index1' }],
state: {
datasourceStates: {
formBased: { layers: [{ columns: [{ sourceField: 'field2' }] }] },
},
},
},
},
},
],
},
},
],
pagination: { total: 1 },
},
});
const { suggestedDashboards, linkedDashboards } = await client.fetchRelatedDashboards();
expect(linkedDashboards).toHaveLength(1);
expect(linkedDashboards).toEqual([
{
id: 'dashboard2',
title: 'Dashboard 2',
matchedBy: { linked: true },
},
]);
expect(suggestedDashboards).toHaveLength(1);
// should return only one dashboard even though it was found by both internal methods
// should mark the relevant panel as matching by index and field
expect(suggestedDashboards).toEqual([
{
id: 'dashboard1',
matchedBy: { fields: ['field1'], index: ['index1'] },
relevantPanelCount: 1,
relevantPanels: [
{
matchedBy: { index: ['index1'], fields: ['field1'] },
panel: {
panelConfig: {
attributes: {
references: [{ id: 'index1', name: 'indexpattern' }],
state: {
datasourceStates: {
formBased: { layers: [{ columns: [{ sourceField: 'field1' }] }] },
},
},
},
},
panelIndex: '123',
title: undefined,
type: 'lens',
},
},
],
score: 0.5,
title: 'Dashboard 1',
},
]);
});
});
});

View file

@ -4,20 +4,24 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { v4 as uuidv4 } from 'uuid';
import { omit } from 'lodash';
import { IContentClient } from '@kbn/content-management-plugin/server/types';
import type { Logger, SavedObjectsFindResult } from '@kbn/core/server';
import { isDashboardSection } from '@kbn/dashboard-plugin/common';
import type { DashboardAttributes, DashboardPanel } from '@kbn/dashboard-plugin/server';
import type { LensAttributes } from '@kbn/lens-embeddable-utils';
import type {
FieldBasedIndexPatternColumn,
GenericIndexPatternColumn,
} from '@kbn/lens-plugin/public';
import type { RelatedDashboard, RelevantPanel } from '@kbn/observability-schema';
import { v4 as uuidv4 } from 'uuid';
import type { AlertData } from './alert_data';
import type { LensAttributes } from '@kbn/lens-embeddable-utils';
import type {
RelevantPanel,
RelatedDashboard,
SuggestedDashboard,
} from '@kbn/observability-schema';
import type { InvestigateAlertsClient } from './investigate_alerts_client';
import type { AlertData } from './alert_data';
type Dashboard = SavedObjectsFindResult<DashboardAttributes>;
export class RelatedDashboardsClient {
@ -31,35 +35,56 @@ export class RelatedDashboardsClient {
private alertId: string
) {}
setAlert(alert: AlertData) {
this.alert = alert;
}
async fetchSuggestedDashboards(): Promise<{ suggestedDashboards: RelatedDashboard[] }> {
const allRelatedDashboards = new Set<RelatedDashboard>();
const relevantDashboardsById = new Map<string, RelatedDashboard>();
const [alert] = await Promise.all([
public async fetchRelatedDashboards(): Promise<{
suggestedDashboards: RelatedDashboard[];
linkedDashboards: RelatedDashboard[];
}> {
const [alertDocument] = await Promise.all([
this.alertsClient.getAlertById(this.alertId),
this.fetchFirst500Dashboards(),
]);
this.setAlert(alert);
if (!this.alert) {
this.setAlert(alertDocument);
const [suggestedDashboards, linkedDashboards] = await Promise.all([
this.fetchSuggestedDashboards(),
this.getLinkedDashboards(),
]);
const filteredSuggestedDashboards = suggestedDashboards.filter(
(suggested) => !linkedDashboards.some((linked) => linked.id === suggested.id)
);
return {
suggestedDashboards: filteredSuggestedDashboards.slice(0, 10), // limit to 10 suggested dashboards
linkedDashboards,
};
}
private setAlert(alert: AlertData) {
this.alert = alert;
}
private checkAlert(): AlertData {
if (!this.alert)
throw new Error(
`Alert with id ${this.alertId} not found. Could not fetch related dashboards.`
);
}
return this.alert;
}
private async fetchSuggestedDashboards(): Promise<SuggestedDashboard[]> {
const alert = this.checkAlert();
const allSuggestedDashboards = new Set<SuggestedDashboard>();
const relevantDashboardsById = new Map<string, SuggestedDashboard>();
const index = await this.getRuleQueryIndex();
const allRelevantFields = this.alert.getAllRelevantFields();
const allRelevantFields = alert.getAllRelevantFields();
if (index) {
const { dashboards } = this.getDashboardsByIndex(index);
dashboards.forEach((dashboard) => allRelatedDashboards.add(dashboard));
dashboards.forEach((dashboard) => allSuggestedDashboards.add(dashboard));
}
if (allRelevantFields.length > 0) {
const { dashboards } = this.getDashboardsByField(allRelevantFields);
dashboards.forEach((dashboard) => allRelatedDashboards.add(dashboard));
dashboards.forEach((dashboard) => allSuggestedDashboards.add(dashboard));
}
allRelatedDashboards.forEach((dashboard) => {
allSuggestedDashboards.forEach((dashboard) => {
const dedupedPanels = this.dedupePanels([
...(relevantDashboardsById.get(dashboard.id)?.relevantPanels || []),
...dashboard.relevantPanels,
@ -79,10 +104,10 @@ export class RelatedDashboardsClient {
const sortedDashboards = Array.from(relevantDashboardsById.values()).sort((a, b) => {
return b.score - a.score;
});
return { suggestedDashboards: sortedDashboards.slice(0, 10) };
return sortedDashboards;
}
async fetchDashboards({
private async fetchDashboards({
page,
perPage = 20,
limit,
@ -112,14 +137,14 @@ export class RelatedDashboardsClient {
await this.fetchDashboards({ page: page + 1, perPage, limit });
}
async fetchFirst500Dashboards() {
private async fetchFirst500Dashboards() {
await this.fetchDashboards({ page: 1, perPage: 500, limit: 500 });
}
getDashboardsByIndex(index: string): {
dashboards: RelatedDashboard[];
private getDashboardsByIndex(index: string): {
dashboards: SuggestedDashboard[];
} {
const relevantDashboards: RelatedDashboard[] = [];
const relevantDashboards: SuggestedDashboard[] = [];
this.dashboardsById.forEach((d) => {
const panels = d.attributes.panels;
const matchingPanels = this.getPanelsByIndex(index, panels);
@ -148,7 +173,7 @@ export class RelatedDashboardsClient {
return { dashboards: relevantDashboards };
}
dedupePanels(panels: RelevantPanel[]): RelevantPanel[] {
private dedupePanels(panels: RelevantPanel[]): RelevantPanel[] {
const uniquePanels = new Map<string, RelevantPanel>();
panels.forEach((p) => {
uniquePanels.set(p.panel.panelIndex, {
@ -159,10 +184,10 @@ export class RelatedDashboardsClient {
return Array.from(uniquePanels.values());
}
getDashboardsByField(fields: string[]): {
dashboards: RelatedDashboard[];
private getDashboardsByField(fields: string[]): {
dashboards: SuggestedDashboard[];
} {
const relevantDashboards: RelatedDashboard[] = [];
const relevantDashboards: SuggestedDashboard[] = [];
this.dashboardsById.forEach((d) => {
const panels = d.attributes.panels;
const matchingPanels = this.getPanelsByField(fields, panels);
@ -249,15 +274,13 @@ export class RelatedDashboardsClient {
}
}
getRuleQueryIndex(): string | null {
if (!this.alert) {
throw new Error('Alert not found. Could not get the rule query index.');
}
const index = this.alert.getRuleQueryIndex();
private getRuleQueryIndex(): string | null {
const alert = this.checkAlert();
const index = alert.getRuleQueryIndex();
return index;
}
getLensVizIndices(lensAttr: LensAttributes): Set<string> {
private getLensVizIndices(lensAttr: LensAttributes): Set<string> {
const indices = new Set(
lensAttr.references
.filter((r) => r.name.match(`indexpattern`))
@ -266,7 +289,7 @@ export class RelatedDashboardsClient {
return indices;
}
getLensVizFields(lensAttr: LensAttributes): Set<string> {
private getLensVizFields(lensAttr: LensAttributes): Set<string> {
const fields = new Set<string>();
const dataSourceLayers = lensAttr.state.datasourceStates.formBased?.layers || {};
Object.values(dataSourceLayers).forEach((ds) => {
@ -284,10 +307,43 @@ export class RelatedDashboardsClient {
return fields;
}
getMatchingFields(dashboard: RelatedDashboard): string[] {
private async getLinkedDashboards(): Promise<RelatedDashboard[]> {
const alert = this.checkAlert();
const ruleId = alert.getRuleId();
if (!ruleId) {
throw new Error(
`Alert with id ${this.alertId} does not have a rule ID. Could not fetch linked dashboards.`
);
}
const rule = await this.alertsClient.getRuleById(ruleId);
if (!rule) {
throw new Error(
`Rule with id ${ruleId} not found. Could not fetch linked dashboards for alert with id ${this.alertId}.`
);
}
const linkedDashboardsArtifacts = rule.artifacts?.dashboards || [];
const linkedDashboards = await this.getLinkedDashboardsByIds(
linkedDashboardsArtifacts.map((d) => d.id)
);
return linkedDashboards;
}
private async getLinkedDashboardsByIds(ids: string[]): Promise<RelatedDashboard[]> {
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 },
}));
}
private getMatchingFields(dashboard: RelatedDashboard): string[] {
const matchingFields = new Set<string>();
// grab all the top level arrays from the matchedBy object via Object.values
Object.values(dashboard.matchedBy).forEach((match) => {
Object.values(omit(dashboard.matchedBy, 'linked')).forEach((match) => {
// add the values of each array to the matchingFields set
match.forEach((value) => {
matchingFields.add(value);
@ -296,13 +352,9 @@ export class RelatedDashboardsClient {
return Array.from(matchingFields);
}
getScore(dashboard: RelatedDashboard): number {
if (!this.alert) {
throw new Error(
`Alert with id ${this.alertId} not found. Could not compute the relevance score for suggested dashboard.`
);
}
const allRelevantFields = this.alert.getAllRelevantFields();
private getScore(dashboard: RelatedDashboard): number {
const alert = this.checkAlert();
const allRelevantFields = alert.getAllRelevantFields();
const index = this.getRuleQueryIndex();
const setA = new Set<string>([...allRelevantFields, ...(index ? [index] : [])]);
const setB = new Set<string>(this.getMatchingFields(dashboard));