Scopes: Deduplicate the list of suggested dashboards (#89169)

This commit is contained in:
Bogdan Matei 2024-06-17 13:44:22 +03:00 committed by GitHub
parent 212c1477c2
commit 94e6bcd329
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 89 additions and 18 deletions

View File

@ -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<ScopesDashboardsScene
this.setState({ isLoading: true });
const dashboards = await fetchDashboards(scopes);
const dashboards = await fetchSuggestedDashboards(scopes);
this.setState({
dashboards,
@ -54,12 +55,10 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
});
}
private filterDashboards(dashboards: ScopeDashboardBinding[], searchQuery: string) {
private filterDashboards(dashboards: SuggestedDashboard[], searchQuery: string): SuggestedDashboard[] {
const lowerCasedSearchQuery = searchQuery.toLowerCase();
return dashboards.filter(({ spec: { dashboardTitle } }) =>
dashboardTitle.toLowerCase().includes(lowerCasedSearchQuery)
);
return dashboards.filter(({ dashboardTitle }) => dashboardTitle.toLowerCase().includes(lowerCasedSearchQuery));
}
}
@ -89,7 +88,7 @@ export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<Sco
/>
) : (
<CustomScrollbar>
{filteredDashboards.map(({ spec: { dashboard, dashboardTitle } }) => (
{filteredDashboards.map(({ dashboard, dashboardTitle }) => (
<Link
key={dashboard}
to={urlUtil.renderUrl(`/d/${dashboard}/`, queryParams)}

View File

@ -9,12 +9,14 @@ import { ScopesFiltersScene } from './ScopesFiltersScene';
import { ScopesScene } from './ScopesScene';
import {
buildTestScene,
fetchDashboardsSpy,
fetchSuggestedDashboardsSpy,
fetchNodesSpy,
fetchScopeSpy,
fetchSelectedScopesSpy,
getApplicationsClustersExpand,
getApplicationsClustersSelect,
getApplicationsClustersSlothClusterNorthSelect,
getApplicationsClustersSlothClusterSouthSelect,
getApplicationsExpand,
getApplicationsSearch,
getApplicationsSlothPictureFactorySelect,
@ -34,6 +36,7 @@ import {
mocksNodes,
mocksScopeDashboardBindings,
mocksScopes,
queryAllDashboard,
queryFiltersApply,
queryApplicationsClustersSlothClusterNorthTitle,
queryApplicationsClustersTitle,
@ -105,7 +108,7 @@ describe('ScopesScene', () => {
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', () => {

View File

@ -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<ScopeDashboardBi
return [];
}
}
export async function fetchSuggestedDashboards(scopes: Scope[]): Promise<SuggestedDashboard[]> {
const items = await fetchDashboards(scopes);
return Object.values(
items.reduce<Record<string, SuggestedDashboard>>((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;
}, {})
);
}

View File

@ -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<ScopeNode & { parent: string }> = [
@ -226,7 +250,7 @@ export const mocksNodes: Array<ScopeNode & { parent: string }> = [
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'));

View File

@ -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[];
}