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
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 React from 'react';
import { Link } from 'react-router-dom'; 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 { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { CustomScrollbar, Icon, Input, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { CustomScrollbar, Icon, Input, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
import { fetchDashboards } from './api'; import { fetchSuggestedDashboards } from './api';
import { SuggestedDashboard } from './types';
export interface ScopesDashboardsSceneState extends SceneObjectState { export interface ScopesDashboardsSceneState extends SceneObjectState {
dashboards: ScopeDashboardBinding[]; dashboards: SuggestedDashboard[];
filteredDashboards: ScopeDashboardBinding[]; filteredDashboards: SuggestedDashboard[];
isLoading: boolean; isLoading: boolean;
searchQuery: string; searchQuery: string;
} }
@@ -36,7 +37,7 @@ export class ScopesDashboardsScene extends SceneObjectBase<ScopesDashboardsScene
this.setState({ isLoading: true }); this.setState({ isLoading: true });
const dashboards = await fetchDashboards(scopes); const dashboards = await fetchSuggestedDashboards(scopes);
this.setState({ this.setState({
dashboards, 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(); const lowerCasedSearchQuery = searchQuery.toLowerCase();
return dashboards.filter(({ spec: { dashboardTitle } }) => return dashboards.filter(({ dashboardTitle }) => dashboardTitle.toLowerCase().includes(lowerCasedSearchQuery));
dashboardTitle.toLowerCase().includes(lowerCasedSearchQuery)
);
} }
} }
@@ -89,7 +88,7 @@ export function ScopesDashboardsSceneRenderer({ model }: SceneComponentProps<Sco
/> />
) : ( ) : (
<CustomScrollbar> <CustomScrollbar>
{filteredDashboards.map(({ spec: { dashboard, dashboardTitle } }) => ( {filteredDashboards.map(({ dashboard, dashboardTitle }) => (
<Link <Link
key={dashboard} key={dashboard}
to={urlUtil.renderUrl(`/d/${dashboard}/`, queryParams)} to={urlUtil.renderUrl(`/d/${dashboard}/`, queryParams)}

View File

@@ -9,12 +9,14 @@ import { ScopesFiltersScene } from './ScopesFiltersScene';
import { ScopesScene } from './ScopesScene'; import { ScopesScene } from './ScopesScene';
import { import {
buildTestScene, buildTestScene,
fetchDashboardsSpy, fetchSuggestedDashboardsSpy,
fetchNodesSpy, fetchNodesSpy,
fetchScopeSpy, fetchScopeSpy,
fetchSelectedScopesSpy, fetchSelectedScopesSpy,
getApplicationsClustersExpand, getApplicationsClustersExpand,
getApplicationsClustersSelect, getApplicationsClustersSelect,
getApplicationsClustersSlothClusterNorthSelect,
getApplicationsClustersSlothClusterSouthSelect,
getApplicationsExpand, getApplicationsExpand,
getApplicationsSearch, getApplicationsSearch,
getApplicationsSlothPictureFactorySelect, getApplicationsSlothPictureFactorySelect,
@@ -34,6 +36,7 @@ import {
mocksNodes, mocksNodes,
mocksScopeDashboardBindings, mocksScopeDashboardBindings,
mocksScopes, mocksScopes,
queryAllDashboard,
queryFiltersApply, queryFiltersApply,
queryApplicationsClustersSlothClusterNorthTitle, queryApplicationsClustersSlothClusterNorthTitle,
queryApplicationsClustersTitle, queryApplicationsClustersTitle,
@@ -105,7 +108,7 @@ describe('ScopesScene', () => {
fetchNodesSpy.mockClear(); fetchNodesSpy.mockClear();
fetchScopeSpy.mockClear(); fetchScopeSpy.mockClear();
fetchSelectedScopesSpy.mockClear(); fetchSelectedScopesSpy.mockClear();
fetchDashboardsSpy.mockClear(); fetchSuggestedDashboardsSpy.mockClear();
dashboardScene = buildTestScene(); dashboardScene = buildTestScene();
scopesScene = dashboardScene.state.scopes!; scopesScene = dashboardScene.state.scopes!;
@@ -241,7 +244,7 @@ describe('ScopesScene', () => {
await userEvents.click(getApplicationsExpand()); await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsSlothPictureFactorySelect()); await userEvents.click(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply()); 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 () => { it('Fetches dashboards list when the list is expanded', async () => {
@@ -250,7 +253,7 @@ describe('ScopesScene', () => {
await userEvents.click(getApplicationsExpand()); await userEvents.click(getApplicationsExpand());
await userEvents.click(getApplicationsSlothPictureFactorySelect()); await userEvents.click(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply()); 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 () => { 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(getApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply()); await userEvents.click(getFiltersApply());
await userEvents.click(getDashboardsExpand()); await userEvents.click(getDashboardsExpand());
await waitFor(() => expect(fetchDashboardsSpy).toHaveBeenCalled()); await waitFor(() => expect(fetchSuggestedDashboardsSpy).toHaveBeenCalled());
}); });
it('Shows dashboards for multiple scopes', async () => { it('Shows dashboards for multiple scopes', async () => {
@@ -299,6 +302,20 @@ describe('ScopesScene', () => {
await userEvents.type(getDashboardsSearch(), '1'); await userEvents.type(getDashboardsSearch(), '1');
expect(queryDashboard('2')).not.toBeInTheDocument(); 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', () => { describe('View mode', () => {

View File

@@ -2,7 +2,7 @@ import { Scope, ScopeSpec, ScopeNode, ScopeDashboardBinding } from '@grafana/dat
import { config, getBackendSrv } from '@grafana/runtime'; import { config, getBackendSrv } from '@grafana/runtime';
import { ScopedResourceClient } from 'app/features/apiserver/client'; 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 group = 'scope.grafana.app';
const version = 'v0alpha1'; const version = 'v0alpha1';
@@ -115,3 +115,23 @@ export async function fetchDashboards(scopes: Scope[]): Promise<ScopeDashboardBi
return []; 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' }, metadata: { name: 'binding4' },
spec: { dashboard: '4', dashboardTitle: 'My Dashboard 4', scope: 'slothVoteTracker' }, 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; ] as const;
export const mocksNodes: Array<ScopeNode & { parent: string }> = [ 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 fetchNodesSpy = jest.spyOn(api, 'fetchNodes');
export const fetchScopeSpy = jest.spyOn(api, 'fetchScope'); export const fetchScopeSpy = jest.spyOn(api, 'fetchScope');
export const fetchSelectedScopesSpy = jest.spyOn(api, 'fetchSelectedScopes'); export const fetchSelectedScopesSpy = jest.spyOn(api, 'fetchSelectedScopes');
export const fetchDashboardsSpy = jest.spyOn(api, 'fetchDashboards'); export const fetchSuggestedDashboardsSpy = jest.spyOn(api, 'fetchSuggestedDashboards');
const selectors = { const selectors = {
tree: { tree: {
@@ -261,6 +285,7 @@ export const getDashboardsExpand = () => screen.getByTestId(selectors.dashboards
export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container); export const queryDashboardsContainer = () => screen.queryByTestId(selectors.dashboards.container);
export const getDashboardsContainer = () => screen.getByTestId(selectors.dashboards.container); export const getDashboardsContainer = () => screen.getByTestId(selectors.dashboards.container);
export const getDashboardsSearch = () => screen.getByTestId(selectors.dashboards.search); 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 queryDashboard = (uid: string) => screen.queryByTestId(selectors.dashboards.dashboard(uid));
export const getDashboard = (uid: string) => screen.getByTestId(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 getApplicationsClustersExpand = () => screen.getByTestId(selectors.tree.expand('applications.clusters'));
export const queryApplicationsClustersSlothClusterNorthTitle = () => export const queryApplicationsClustersSlothClusterNorthTitle = () =>
screen.queryByTestId(selectors.tree.title('applications.clusters-slothClusterNorth')); 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 getClustersSelect = () => screen.getByTestId(selectors.tree.select('clusters'));
export const getClustersExpand = () => screen.getByTestId(selectors.tree.expand('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 { export interface Node extends ScopeNodeSpec {
name: string; name: string;
@@ -20,3 +20,9 @@ export interface TreeScope {
scopeName: string; scopeName: string;
path: string[]; path: string[];
} }
export interface SuggestedDashboard {
dashboard: string;
dashboardTitle: string;
items: ScopeDashboardBinding[];
}