mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Scopes: Deduplicate the list of suggested dashboards (#89169)
This commit is contained in:
parent
212c1477c2
commit
94e6bcd329
@ -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)}
|
||||
|
@ -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', () => {
|
||||
|
@ -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;
|
||||
}, {})
|
||||
);
|
||||
}
|
||||
|
@ -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'));
|
||||
|
@ -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[];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user