From 94e6bcd329ddc0f33e6bfa43a1c30f69f0d5f91b Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Mon, 17 Jun 2024 13:44:22 +0300 Subject: [PATCH] Scopes: Deduplicate the list of suggested dashboards (#89169) --- .../scene/Scopes/ScopesDashboardsScene.tsx | 19 ++++++------ .../scene/Scopes/ScopesScene.test.tsx | 27 +++++++++++++--- .../dashboard-scene/scene/Scopes/api.ts | 22 ++++++++++++- .../scene/Scopes/testUtils.tsx | 31 ++++++++++++++++++- .../dashboard-scene/scene/Scopes/types.ts | 8 ++++- 5 files changed, 89 insertions(+), 18 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx index 35ab7ea9be6..40eb79ade3a 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesDashboardsScene.tsx @@ -2,17 +2,18 @@ import { css } from '@emotion/css'; import React from 'react'; import { Link } from 'react-router-dom'; -import { GrafanaTheme2, Scope, ScopeDashboardBinding, urlUtil } from '@grafana/data'; +import { GrafanaTheme2, Scope, urlUtil } from '@grafana/data'; import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; import { CustomScrollbar, Icon, Input, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { t } from 'app/core/internationalization'; -import { fetchDashboards } from './api'; +import { fetchSuggestedDashboards } from './api'; +import { SuggestedDashboard } from './types'; export interface ScopesDashboardsSceneState extends SceneObjectState { - dashboards: ScopeDashboardBinding[]; - filteredDashboards: ScopeDashboardBinding[]; + dashboards: SuggestedDashboard[]; + filteredDashboards: SuggestedDashboard[]; isLoading: boolean; searchQuery: string; } @@ -36,7 +37,7 @@ export class ScopesDashboardsScene extends SceneObjectBase - dashboardTitle.toLowerCase().includes(lowerCasedSearchQuery) - ); + return dashboards.filter(({ dashboardTitle }) => dashboardTitle.toLowerCase().includes(lowerCasedSearchQuery)); } } @@ -89,7 +88,7 @@ export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps ) : ( - {filteredDashboards.map(({ spec: { dashboard, dashboardTitle } }) => ( + {filteredDashboards.map(({ dashboard, dashboardTitle }) => ( { fetchNodesSpy.mockClear(); fetchScopeSpy.mockClear(); fetchSelectedScopesSpy.mockClear(); - fetchDashboardsSpy.mockClear(); + fetchSuggestedDashboardsSpy.mockClear(); dashboardScene = buildTestScene(); scopesScene = dashboardScene.state.scopes!; @@ -241,7 +244,7 @@ describe('ScopesScene', () => { await userEvents.click(getApplicationsExpand()); await userEvents.click(getApplicationsSlothPictureFactorySelect()); await userEvents.click(getFiltersApply()); - await waitFor(() => expect(fetchDashboardsSpy).not.toHaveBeenCalled()); + await waitFor(() => expect(fetchSuggestedDashboardsSpy).not.toHaveBeenCalled()); }); it('Fetches dashboards list when the list is expanded', async () => { @@ -250,7 +253,7 @@ describe('ScopesScene', () => { await userEvents.click(getApplicationsExpand()); await userEvents.click(getApplicationsSlothPictureFactorySelect()); await userEvents.click(getFiltersApply()); - await waitFor(() => expect(fetchDashboardsSpy).toHaveBeenCalled()); + await waitFor(() => expect(fetchSuggestedDashboardsSpy).toHaveBeenCalled()); }); it('Fetches dashboards list when the list is expanded after scope selection', async () => { @@ -259,7 +262,7 @@ describe('ScopesScene', () => { await userEvents.click(getApplicationsSlothPictureFactorySelect()); await userEvents.click(getFiltersApply()); await userEvents.click(getDashboardsExpand()); - await waitFor(() => expect(fetchDashboardsSpy).toHaveBeenCalled()); + await waitFor(() => expect(fetchSuggestedDashboardsSpy).toHaveBeenCalled()); }); it('Shows dashboards for multiple scopes', async () => { @@ -299,6 +302,20 @@ describe('ScopesScene', () => { await userEvents.type(getDashboardsSearch(), '1'); expect(queryDashboard('2')).not.toBeInTheDocument(); }); + + it('Deduplicates the dashboards list', async () => { + await userEvents.click(getDashboardsExpand()); + await userEvents.click(getFiltersInput()); + await userEvents.click(getApplicationsExpand()); + await userEvents.click(getApplicationsClustersExpand()); + await userEvents.click(getApplicationsClustersSlothClusterNorthSelect()); + await userEvents.click(getApplicationsClustersSlothClusterSouthSelect()); + await userEvents.click(getFiltersApply()); + expect(queryAllDashboard('5')).toHaveLength(1); + expect(queryAllDashboard('6')).toHaveLength(1); + expect(queryAllDashboard('7')).toHaveLength(1); + expect(queryAllDashboard('8')).toHaveLength(1); + }); }); describe('View mode', () => { diff --git a/public/app/features/dashboard-scene/scene/Scopes/api.ts b/public/app/features/dashboard-scene/scene/Scopes/api.ts index d35b19d277f..71ec07cec19 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/api.ts +++ b/public/app/features/dashboard-scene/scene/Scopes/api.ts @@ -2,7 +2,7 @@ import { Scope, ScopeSpec, ScopeNode, ScopeDashboardBinding } from '@grafana/dat import { config, getBackendSrv } from '@grafana/runtime'; import { ScopedResourceClient } from 'app/features/apiserver/client'; -import { NodesMap, SelectedScope, TreeScope } from './types'; +import { NodesMap, SelectedScope, SuggestedDashboard, TreeScope } from './types'; const group = 'scope.grafana.app'; const version = 'v0alpha1'; @@ -115,3 +115,23 @@ export async function fetchDashboards(scopes: Scope[]): Promise { + const items = await fetchDashboards(scopes); + + return Object.values( + items.reduce>((acc, item) => { + if (!acc[item.spec.dashboard]) { + acc[item.spec.dashboard] = { + dashboard: item.spec.dashboard, + dashboardTitle: item.spec.dashboardTitle, + items: [], + }; + } + + acc[item.spec.dashboard].items.push(item); + + return acc; + }, {}) + ); +} diff --git a/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx b/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx index faf5838bde6..7ca1fcaa8de 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx +++ b/public/app/features/dashboard-scene/scene/Scopes/testUtils.tsx @@ -89,6 +89,30 @@ export const mocksScopeDashboardBindings: ScopeDashboardBinding[] = [ metadata: { name: 'binding4' }, spec: { dashboard: '4', dashboardTitle: 'My Dashboard 4', scope: 'slothVoteTracker' }, }, + { + metadata: { name: 'binding5' }, + spec: { dashboard: '5', dashboardTitle: 'My Dashboard 5', scope: 'slothClusterNorth' }, + }, + { + metadata: { name: 'binding6' }, + spec: { dashboard: '6', dashboardTitle: 'My Dashboard 6', scope: 'slothClusterNorth' }, + }, + { + metadata: { name: 'binding7' }, + spec: { dashboard: '7', dashboardTitle: 'My Dashboard 7', scope: 'slothClusterNorth' }, + }, + { + metadata: { name: 'binding8' }, + spec: { dashboard: '5', dashboardTitle: 'My Dashboard 5', scope: 'slothClusterSouth' }, + }, + { + metadata: { name: 'binding9' }, + spec: { dashboard: '6', dashboardTitle: 'My Dashboard 6', scope: 'slothClusterSouth' }, + }, + { + metadata: { name: 'binding10' }, + spec: { dashboard: '8', dashboardTitle: 'My Dashboard 8', scope: 'slothClusterSouth' }, + }, ] as const; export const mocksNodes: Array = [ @@ -226,7 +250,7 @@ export const mocksNodes: Array = [ export const fetchNodesSpy = jest.spyOn(api, 'fetchNodes'); export const fetchScopeSpy = jest.spyOn(api, 'fetchScope'); export const fetchSelectedScopesSpy = jest.spyOn(api, 'fetchSelectedScopes'); -export const fetchDashboardsSpy = jest.spyOn(api, 'fetchDashboards'); +export const fetchSuggestedDashboardsSpy = jest.spyOn(api, 'fetchSuggestedDashboards'); const selectors = { tree: { @@ -261,6 +285,7 @@ export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container); export const getDashboardsContainer = () => screen.getByTestId(selectors.dashboards.container); export const getDashboardsSearch = () => screen.getByTestId(selectors.dashboards.search); +export const queryAllDashboard = (uid: string) => screen.queryAllByTestId(selectors.dashboards.dashboard(uid)); export const queryDashboard = (uid: string) => screen.queryByTestId(selectors.dashboards.dashboard(uid)); export const getDashboard = (uid: string) => screen.getByTestId(selectors.dashboards.dashboard(uid)); @@ -281,6 +306,10 @@ export const getApplicationsClustersSelect = () => screen.getByTestId(selectors. export const getApplicationsClustersExpand = () => screen.getByTestId(selectors.tree.expand('applications.clusters')); export const queryApplicationsClustersSlothClusterNorthTitle = () => screen.queryByTestId(selectors.tree.title('applications.clusters-slothClusterNorth')); +export const getApplicationsClustersSlothClusterNorthSelect = () => + screen.getByTestId(selectors.tree.select('applications.clusters-slothClusterNorth')); +export const getApplicationsClustersSlothClusterSouthSelect = () => + screen.getByTestId(selectors.tree.select('applications.clusters-slothClusterSouth')); export const getClustersSelect = () => screen.getByTestId(selectors.tree.select('clusters')); export const getClustersExpand = () => screen.getByTestId(selectors.tree.expand('clusters')); diff --git a/public/app/features/dashboard-scene/scene/Scopes/types.ts b/public/app/features/dashboard-scene/scene/Scopes/types.ts index 1a556870ecf..5dc7cfc1e5d 100644 --- a/public/app/features/dashboard-scene/scene/Scopes/types.ts +++ b/public/app/features/dashboard-scene/scene/Scopes/types.ts @@ -1,4 +1,4 @@ -import { Scope, ScopeNodeSpec } from '@grafana/data'; +import { Scope, ScopeDashboardBinding, ScopeNodeSpec } from '@grafana/data'; export interface Node extends ScopeNodeSpec { name: string; @@ -20,3 +20,9 @@ export interface TreeScope { scopeName: string; path: string[]; } + +export interface SuggestedDashboard { + dashboard: string; + dashboardTitle: string; + items: ScopeDashboardBinding[]; +}