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:
@@ -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)}
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user