Scopes: Fetch ScopeNavigation and display them (#102385)

* Initial API client for ScopesNavigation

* Display scopes navigation items and group them

* Remove refactored component

* Fix link highlighting

* Underline on focus

* Set icons based un URL path

* Remove explicit icon names

* Add links to copy

* Map dashboardbinding to scopeNavigation in folder generation

* Consolidate dashboardbindings and scopenavigations to a single way of handling

* Let fetchDashboards handle all fetching

* Move endpoint types to core

* Remove comments

* Fix import

* Rename name to id

* Update translation

* Add feature toggle

* Fix tests

* Fix go.mod for some reason

---------

Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>
This commit is contained in:
Tobias Skarhed
2025-03-31 17:20:00 +02:00
committed by GitHub
co-authored by Andrej Ocenas
parent 483c6ac70d
commit c321afdeb7
16 changed files with 358 additions and 230 deletions
@@ -1001,6 +1001,10 @@ export interface FeatureToggles {
*/
alertingJiraIntegration?: boolean;
/**
* Use the scopes navigation endpoint instead of the dashboardbindings endpoint
*/
useScopesNavigationEndpoint?: boolean;
/**
* Enables the alert rule version history restore feature
* @default true
*/
+8
View File
@@ -1708,6 +1708,14 @@ var (
FrontendOnly: true,
HideFromDocs: true,
},
{
Name: "useScopesNavigationEndpoint",
Description: "Use the scopes navigation endpoint instead of the dashboardbindings endpoint",
Stage: FeatureStageExperimental,
Owner: grafanaFrontendPlatformSquad,
FrontendOnly: true,
HideFromDocs: true,
},
{
Name: "alertingRuleVersionHistoryRestore",
Description: "Enables the alert rule version history restore feature",
+1
View File
@@ -226,6 +226,7 @@ newLogsPanel,experimental,@grafana/observability-logs,false,false,true
grafanaconThemes,experimental,@grafana/grafana-frontend-platform,false,true,false
pluginsCDNSyncLoader,experimental,@grafana/plugins-platform-backend,false,false,false
alertingJiraIntegration,experimental,@grafana/alerting-squad,false,false,true
useScopesNavigationEndpoint,experimental,@grafana/grafana-frontend-platform,false,false,true
alertingRuleVersionHistoryRestore,GA,@grafana/alerting-squad,false,false,true
newShareReportDrawer,experimental,@grafana/sharing-squad,false,false,false
rendererDisableAppPluginsPreload,experimental,@grafana/sharing-squad,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
226 grafanaconThemes experimental @grafana/grafana-frontend-platform false true false
227 pluginsCDNSyncLoader experimental @grafana/plugins-platform-backend false false false
228 alertingJiraIntegration experimental @grafana/alerting-squad false false true
229 useScopesNavigationEndpoint experimental @grafana/grafana-frontend-platform false false true
230 alertingRuleVersionHistoryRestore GA @grafana/alerting-squad false false true
231 newShareReportDrawer experimental @grafana/sharing-squad false false false
232 rendererDisableAppPluginsPreload experimental @grafana/sharing-squad false false true
+4
View File
@@ -915,6 +915,10 @@ const (
// Enables the new Jira integration for contact points in cloud alert managers.
FlagAlertingJiraIntegration = "alertingJiraIntegration"
// FlagUseScopesNavigationEndpoint
// Use the scopes navigation endpoint instead of the dashboardbindings endpoint
FlagUseScopesNavigationEndpoint = "useScopesNavigationEndpoint"
// FlagAlertingRuleVersionHistoryRestore
// Enables the alert rule version history restore feature
FlagAlertingRuleVersionHistoryRestore = "alertingRuleVersionHistoryRestore"
+14
View File
@@ -4390,6 +4390,20 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "useScopesNavigationEndpoint",
"resourceVersion": "1743087365203",
"creationTimestamp": "2025-03-27T14:56:05Z"
},
"spec": {
"description": "Use the scopes navigation endpoint instead of the dashboardbindings endpoint",
"stage": "experimental",
"codeowner": "@grafana/grafana-frontend-platform",
"frontend": true,
"hideFromDocs": true
}
},
{
"metadata": {
"name": "useSeessionStorageForRedirection",
@@ -3,6 +3,7 @@ import { getBackendSrv } from '@grafana/runtime';
import { getAPINamespace } from '../../api/utils';
import { ScopeNavigation } from './dashboards/types';
import { NodeReason, NodesMap, SelectedScope, TreeScope } from './selector/types';
import { getEmptyScopeObject } from './utils';
@@ -104,4 +105,16 @@ export class ScopesApiClient {
return [];
}
};
public fetchScopeNavigations = async (scopeNames: string[]): Promise<ScopeNavigation[]> => {
try {
const response = await getBackendSrv().get<{ items: ScopeNavigation[] }>(apiUrl + `/find/scope_navigations`, {
scope: scopeNames,
});
return response?.items ?? [];
} catch (err) {
return [];
}
};
}
@@ -27,7 +27,8 @@ export function ScopesDashboards() {
}
const { scopesDashboardsService } = scopeServices;
const { loading, forScopeNames, dashboards, searchQuery, filteredFolders } = scopesDashboardsService.state;
const { loading, forScopeNames, dashboards, scopeNavigations, searchQuery, filteredFolders } =
scopesDashboardsService.state;
const { changeSearchQuery, updateFolder, clearSearchQuery } = scopesDashboardsService;
if (!loading) {
@@ -40,13 +41,15 @@ export function ScopesDashboards() {
<Trans i18nKey="scopes.dashboards.noResultsNoScopes">No scopes selected</Trans>
</div>
);
} else if (dashboards.length === 0) {
} else if (dashboards.length === 0 && scopeNavigations.length === 0) {
return (
<div
className={cx(styles.container, styles.noResultsContainer)}
data-testid="scopes-dashboards-notFoundForScope"
>
<Trans i18nKey="scopes.dashboards.noResultsForScopes">No dashboards found for the selected scopes</Trans>
<Trans i18nKey="scopes.dashboards.noResultsForScopes">
No dashboards or links found for the selected scopes
</Trans>
</div>
);
}
@@ -1,21 +1,23 @@
import { isEqual } from 'lodash';
import { ScopeDashboardBinding } from '@grafana/data';
import { config } from '@grafana/runtime';
import { ScopesApiClient } from '../ScopesApiClient';
import { ScopesServiceBase } from '../ScopesServiceBase';
import { SuggestedDashboardsFoldersMap } from './types';
import { ScopeNavigation, SuggestedNavigationsFoldersMap } from './types';
interface ScopesDashboardsServiceState {
// State of the drawer showing related dashboards
drawerOpened: boolean;
// by keeping a track of the raw response, it's much easier to check if we got any dashboards for the currently selected scopes
dashboards: ScopeDashboardBinding[];
scopeNavigations: Array<ScopeDashboardBinding | ScopeNavigation>;
// a filtered version of the `folders` property. this prevents a lot of unnecessary parsings in React renders
filteredFolders: SuggestedDashboardsFoldersMap;
filteredFolders: SuggestedNavigationsFoldersMap;
// this is a grouping in folders of the `dashboards` property. it is used for filtering the dashboards and folders when the search query changes
folders: SuggestedDashboardsFoldersMap;
folders: SuggestedNavigationsFoldersMap;
forScopeNames: string[];
loading: boolean;
searchQuery: string;
@@ -26,6 +28,7 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
super({
drawerOpened: false,
dashboards: [],
scopeNavigations: [],
filteredFolders: {},
folders: {},
forScopeNames: [],
@@ -37,8 +40,8 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
public updateFolder = (path: string[], expanded: boolean) => {
let folders = { ...this.state.folders };
let filteredFolders = { ...this.state.filteredFolders };
let currentLevelFolders: SuggestedDashboardsFoldersMap = folders;
let currentLevelFilteredFolders: SuggestedDashboardsFoldersMap = filteredFolders;
let currentLevelFolders: SuggestedNavigationsFoldersMap = folders;
let currentLevelFilteredFolders: SuggestedNavigationsFoldersMap = filteredFolders;
for (let idx = 0; idx < path.length - 1; idx++) {
currentLevelFolders = currentLevelFolders[path[idx]].folders;
@@ -87,66 +90,90 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
this.updateState({ forScopeNames, loading: true });
const dashboards = await this.apiClient.fetchDashboards(forScopeNames);
const fetchNavigations = config.featureToggles.useScopesNavigationEndpoint
? this.apiClient.fetchScopeNavigations
: this.apiClient.fetchDashboards;
const res = await fetchNavigations(forScopeNames);
if (isEqual(this.state.forScopeNames, forScopeNames)) {
const folders = this.groupDashboards(dashboards);
const folders = this.groupSuggestedItems(res);
const filteredFolders = this.filterFolders(folders, this.state.searchQuery);
this.updateState({ dashboards, filteredFolders, folders, loading: false, drawerOpened: dashboards.length > 0 });
this.updateState({
scopeNavigations: res,
filteredFolders,
folders,
loading: false,
drawerOpened: res.length > 0,
});
}
};
public groupDashboards = (dashboards: ScopeDashboardBinding[]): SuggestedDashboardsFoldersMap => {
return dashboards.reduce<SuggestedDashboardsFoldersMap>(
(acc, dashboard) => {
const rootNode = acc[''];
const groups = dashboard.status.groups ?? [];
groups.forEach((group) => {
if (group && !rootNode.folders[group]) {
rootNode.folders[group] = {
title: group,
expanded: false,
folders: {},
dashboards: {},
};
}
});
const targets =
groups.length > 0
? groups.map((group) => (group === '' ? rootNode.dashboards : rootNode.folders[group].dashboards))
: [rootNode.dashboards];
targets.forEach((target) => {
if (!target[dashboard.spec.dashboard]) {
target[dashboard.spec.dashboard] = {
dashboard: dashboard.spec.dashboard,
dashboardTitle: dashboard.status.dashboardTitle,
items: [],
};
}
target[dashboard.spec.dashboard].items.push(dashboard);
});
return acc;
public groupSuggestedItems = (
navigationItems: Array<ScopeDashboardBinding | ScopeNavigation>
): SuggestedNavigationsFoldersMap => {
const folders: SuggestedNavigationsFoldersMap = {
'': {
title: '',
expanded: true,
folders: {},
suggestedNavigations: {},
},
{
'': {
title: '',
expanded: true,
folders: {},
dashboards: {},
},
}
);
};
// Process navigations
navigationItems.forEach((navigation) => {
const rootNode = folders[''];
const groups = navigation.status.groups ?? [];
groups.forEach((group) => {
if (group && !rootNode.folders[group]) {
rootNode.folders[group] = {
title: group,
expanded: false,
folders: {},
suggestedNavigations: {},
};
}
});
const targets =
groups.length > 0
? groups.map((group) =>
group === '' ? rootNode.suggestedNavigations : rootNode.folders[group].suggestedNavigations
)
: [rootNode.suggestedNavigations];
targets.forEach((target) => {
// Dashboard
if (
'dashboard' in navigation.spec &&
'dashboardTitle' in navigation.status &&
!target[navigation.spec.dashboard]
) {
target[navigation.spec.dashboard] = {
url: '/d/' + navigation.spec.dashboard,
title: navigation.status.dashboardTitle,
id: navigation.spec.dashboard,
};
} else if ('url' in navigation.spec && 'title' in navigation.status && !target[navigation.spec.url]) {
target[navigation.spec.url] = {
title: navigation.status.title || navigation.metadata.name,
url: navigation.spec.url,
id: navigation.metadata.name,
};
}
});
});
return folders;
};
public filterFolders = (folders: SuggestedDashboardsFoldersMap, query: string): SuggestedDashboardsFoldersMap => {
public filterFolders = (folders: SuggestedNavigationsFoldersMap, query: string): SuggestedNavigationsFoldersMap => {
query = (query ?? '').toLowerCase();
return Object.entries(folders).reduce<SuggestedDashboardsFoldersMap>((acc, [folderId, folder]) => {
return Object.entries(folders).reduce<SuggestedNavigationsFoldersMap>((acc, [folderId, folder]) => {
// If folder matches the query, we show everything inside
if (folder.title.toLowerCase().includes(query)) {
acc[folderId] = {
@@ -158,16 +185,17 @@ export class ScopesDashboardsService extends ScopesServiceBase<ScopesDashboardsS
}
const filteredFolders = this.filterFolders(folder.folders, query);
const filteredDashboards = Object.entries(folder.dashboards).filter(([_, dashboard]) =>
dashboard.dashboardTitle.toLowerCase().includes(query)
const filteredNavigations = Object.entries(folder.suggestedNavigations).filter(([_, navigation]) =>
navigation.title.toLowerCase().includes(query)
);
if (Object.keys(filteredFolders).length > 0 || filteredDashboards.length > 0) {
if (Object.keys(filteredFolders).length > 0 || filteredNavigations.length > 0) {
acc[folderId] = {
...folder,
expanded: true,
folders: filteredFolders,
dashboards: Object.fromEntries(filteredDashboards),
suggestedNavigations: Object.fromEntries(filteredNavigations),
};
}
@@ -1,14 +1,19 @@
import { ScopesDashboardsTreeDashboardItem } from './ScopesDashboardsTreeDashboardItem';
import { urlUtil } from '@grafana/data';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { ScopesDashboardsTreeFolderItem } from './ScopesDashboardsTreeFolderItem';
import { OnFolderUpdate, SuggestedDashboardsFoldersMap } from './types';
import { ScopesNavigationTreeLink } from './ScopesNavigationTreeLink';
import { OnFolderUpdate, SuggestedNavigationsFoldersMap } from './types';
export interface ScopesDashboardsTreeProps {
folders: SuggestedDashboardsFoldersMap;
folders: SuggestedNavigationsFoldersMap;
folderPath: string[];
onFolderUpdate: OnFolderUpdate;
}
export function ScopesDashboardsTree({ folders, folderPath, onFolderUpdate }: ScopesDashboardsTreeProps) {
const [queryParams] = useQueryParams();
const folderId = folderPath[folderPath.length - 1];
const folder = folders[folderId];
@@ -23,9 +28,13 @@ export function ScopesDashboardsTree({ folders, folderPath, onFolderUpdate }: Sc
onFolderUpdate={onFolderUpdate}
/>
))}
{Object.values(folder.dashboards).map((dashboard) => (
<ScopesDashboardsTreeDashboardItem key={dashboard.dashboard} dashboard={dashboard} />
{Object.values(folder.suggestedNavigations).map((navigation) => (
<ScopesNavigationTreeLink
key={navigation.id}
to={urlUtil.renderUrl(navigation.url, queryParams)}
title={navigation.title}
id={navigation.id}
/>
))}
</div>
);
@@ -1,50 +0,0 @@
import { css } from '@emotion/css';
import { Link } from 'react-router-dom-v5-compat';
import { GrafanaTheme2, urlUtil } from '@grafana/data';
import { Icon, useStyles2 } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { SuggestedDashboard } from './types';
export interface ScopesDashboardsTreeDashboardItemProps {
dashboard: SuggestedDashboard;
}
export function ScopesDashboardsTreeDashboardItem({ dashboard }: ScopesDashboardsTreeDashboardItemProps) {
const styles = useStyles2(getStyles);
const [queryParams] = useQueryParams();
return (
<Link
key={dashboard.dashboard}
to={urlUtil.renderUrl(`/d/${dashboard.dashboard}/`, queryParams)}
className={styles.container}
data-testid={`scopes-dashboards-${dashboard.dashboard}`}
role="treeitem"
>
<Icon name="apps" className={styles.icon} /> {dashboard.dashboardTitle}
</Link>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
display: 'flex',
alignItems: 'flex-start',
gap: theme.spacing(1),
padding: theme.spacing(0.5, 0),
textAlign: 'left',
wordBreak: 'break-word',
'&:last-child': css({
paddingBottom: 0,
}),
}),
icon: css({
marginTop: theme.spacing(0.25),
}),
};
};
@@ -5,12 +5,12 @@ import { Icon, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { ScopesDashboardsTree } from './ScopesDashboardsTree';
import { OnFolderUpdate, SuggestedDashboardsFolder, SuggestedDashboardsFoldersMap } from './types';
import { OnFolderUpdate, SuggestedNavigationsFolder, SuggestedNavigationsFoldersMap } from './types';
export interface ScopesDashboardsTreeFolderItemProps {
folder: SuggestedDashboardsFolder;
folder: SuggestedNavigationsFolder;
folderPath: string[];
folders: SuggestedDashboardsFoldersMap;
folders: SuggestedNavigationsFoldersMap;
onFolderUpdate: OnFolderUpdate;
}
@@ -0,0 +1,68 @@
import { css } from '@emotion/css';
import { useMemo } from 'react';
import { Link } from 'react-router-dom-v5-compat';
import { GrafanaTheme2, IconName, locationUtil } from '@grafana/data';
import { Icon, useStyles2 } from '@grafana/ui';
export interface ScopesNavigationTreeLinkProps {
to: string;
title: string;
id: string;
}
export function ScopesNavigationTreeLink({ to, title, id }: ScopesNavigationTreeLinkProps) {
const styles = useStyles2(getStyles);
const linkIcon = useMemo(() => getLinkIcon(to), [to]);
return (
<Link to={to} className={styles.container} data-testid={`scopes-dashboards-${id}`} role="treeitem">
<Icon name={linkIcon} className={styles.icon} /> {title}
</Link>
);
}
function getLinkIcon(to: string) {
// Strip base URL and normalize path
const normalizedPath = locationUtil.stripBaseFromUrl(to);
for (const [key, value] of linkMap.entries()) {
if (normalizedPath.startsWith(key)) {
return value;
}
}
return 'link';
}
const linkMap = new Map<string, IconName>([
['http', 'external-link-alt'],
['/d', 'apps'],
['/explore/metrics', 'drilldown'],
['/a/grafana-metricsdrilldown-app/', 'drilldown'],
['/a/grafana-lokiexplore-app/', 'drilldown'],
['/a/grafana-exploretraces-app/', 'drilldown'],
['/a/grafana-pyroscope-app/', 'drilldown'],
]);
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
display: 'flex',
alignItems: 'flex-start',
gap: theme.spacing(1),
padding: theme.spacing(0.5, 0),
textAlign: 'left',
wordBreak: 'break-word',
'&:last-child': css({
paddingBottom: 0,
}),
'&:hover, &:focus': css({
textDecoration: 'underline',
}),
}),
icon: css({
marginTop: theme.spacing(0.25),
}),
};
};
+31 -6
View File
@@ -1,19 +1,44 @@
import { ScopeDashboardBinding } from '@grafana/data';
// TODO: replace with generate API client types
export interface ScopeNavigationSpec {
url: string;
scope: string;
}
export interface ScopeNavigationStatus {
title: string;
groups?: string[];
}
export interface ScopeNavigation {
metadata: {
name: string;
};
spec: ScopeNavigationSpec;
status: ScopeNavigationStatus;
}
export interface SuggestedDashboard {
dashboard: string;
dashboardTitle: string;
items: ScopeDashboardBinding[];
}
export interface SuggestedDashboardsFolder {
export interface SuggestedNavigation {
title: string;
expanded: boolean;
folders: SuggestedDashboardsFoldersMap;
dashboards: SuggestedDashboardsMap;
url: string;
// Used for testid and keys
id: string;
}
export type SuggestedDashboardsMap = Record<string, SuggestedDashboard>;
export type SuggestedDashboardsFoldersMap = Record<string, SuggestedDashboardsFolder>;
export interface SuggestedNavigationsFolder {
title: string;
expanded: boolean;
folders: SuggestedNavigationsFoldersMap;
suggestedNavigations: SuggestedNavigationsMap;
}
export type SuggestedNavigationsFoldersMap = Record<string, SuggestedNavigationsFolder>;
export type SuggestedNavigationsMap = Record<string, SuggestedNavigation>;
export type OnFolderUpdate = (path: string[], expanded: boolean) => void;
@@ -184,6 +184,7 @@ export class ScopesSelectorService extends ScopesServiceBase<ScopesSelectorServi
// Apply the scopes right away even though we don't have the metadata yet.
this.updateState({ selectedScopes, treeScopes, loading: true });
// Fetches both dashboards and scope navigations
this.dashboardsService.fetchDashboards(selectedScopes.map(({ scope }) => scope.metadata.name));
selectedScopes = await this.apiClient.fetchMultipleScopes(treeScopes);
@@ -284,16 +284,16 @@ describe('Dashboards list', () => {
describe('groupDashboards', () => {
it('Assigns dashboards without groups to root folder', () => {
expect(scopesDashboardsService.groupDashboards([dashboardWithoutFolder])).toEqual({
expect(scopesDashboardsService.groupSuggestedItems([dashboardWithoutFolder])).toEqual({
'': {
title: '',
expanded: true,
folders: {},
dashboards: {
suggestedNavigations: {
[dashboardWithoutFolder.spec.dashboard]: {
dashboard: dashboardWithoutFolder.spec.dashboard,
dashboardTitle: dashboardWithoutFolder.status.dashboardTitle,
items: [dashboardWithoutFolder],
url: '/d/' + dashboardWithoutFolder.spec.dashboard,
title: dashboardWithoutFolder.status.dashboardTitle,
id: dashboardWithoutFolder.spec.dashboard,
},
},
},
@@ -301,16 +301,16 @@ describe('Dashboards list', () => {
});
it('Assigns dashboards with root group to root folder', () => {
expect(scopesDashboardsService.groupDashboards([dashboardWithRootFolder])).toEqual({
expect(scopesDashboardsService.groupSuggestedItems([dashboardWithRootFolder])).toEqual({
'': {
title: '',
expanded: true,
folders: {},
dashboards: {
suggestedNavigations: {
[dashboardWithRootFolder.spec.dashboard]: {
dashboard: dashboardWithRootFolder.spec.dashboard,
dashboardTitle: dashboardWithRootFolder.status.dashboardTitle,
items: [dashboardWithRootFolder],
url: '/d/' + dashboardWithRootFolder.spec.dashboard,
title: dashboardWithRootFolder.status.dashboardTitle,
id: dashboardWithRootFolder.spec.dashboard,
},
},
},
@@ -318,7 +318,7 @@ describe('Dashboards list', () => {
});
it('Merges folders from multiple dashboards', () => {
expect(scopesDashboardsService.groupDashboards([dashboardWithOneFolder, dashboardWithTwoFolders])).toEqual({
expect(scopesDashboardsService.groupSuggestedItems([dashboardWithOneFolder, dashboardWithTwoFolders])).toEqual({
'': {
title: '',
expanded: true,
@@ -327,16 +327,16 @@ describe('Dashboards list', () => {
title: 'Folder 1',
expanded: false,
folders: {},
dashboards: {
suggestedNavigations: {
[dashboardWithOneFolder.spec.dashboard]: {
dashboard: dashboardWithOneFolder.spec.dashboard,
dashboardTitle: dashboardWithOneFolder.status.dashboardTitle,
items: [dashboardWithOneFolder],
url: '/d/' + dashboardWithOneFolder.spec.dashboard,
title: dashboardWithOneFolder.status.dashboardTitle,
id: dashboardWithOneFolder.spec.dashboard,
},
[dashboardWithTwoFolders.spec.dashboard]: {
dashboard: dashboardWithTwoFolders.spec.dashboard,
dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
items: [dashboardWithTwoFolders],
url: '/d/' + dashboardWithTwoFolders.spec.dashboard,
title: dashboardWithTwoFolders.status.dashboardTitle,
id: dashboardWithTwoFolders.spec.dashboard,
},
},
},
@@ -344,23 +344,23 @@ describe('Dashboards list', () => {
title: 'Folder 2',
expanded: false,
folders: {},
dashboards: {
suggestedNavigations: {
[dashboardWithTwoFolders.spec.dashboard]: {
dashboard: dashboardWithTwoFolders.spec.dashboard,
dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
items: [dashboardWithTwoFolders],
url: '/d/' + dashboardWithTwoFolders.spec.dashboard,
title: dashboardWithTwoFolders.status.dashboardTitle,
id: dashboardWithTwoFolders.spec.dashboard,
},
},
},
},
dashboards: {},
suggestedNavigations: {},
},
});
});
it('Merges scopes from multiple dashboards', () => {
expect(
scopesDashboardsService.groupDashboards([dashboardWithTwoFolders, alternativeDashboardWithTwoFolders])
scopesDashboardsService.groupSuggestedItems([dashboardWithTwoFolders, alternativeDashboardWithTwoFolders])
).toEqual({
'': {
title: '',
@@ -370,11 +370,11 @@ describe('Dashboards list', () => {
title: 'Folder 1',
expanded: false,
folders: {},
dashboards: {
suggestedNavigations: {
[dashboardWithTwoFolders.spec.dashboard]: {
dashboard: dashboardWithTwoFolders.spec.dashboard,
dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
url: '/d/' + dashboardWithTwoFolders.spec.dashboard,
title: dashboardWithTwoFolders.status.dashboardTitle,
id: dashboardWithTwoFolders.spec.dashboard,
},
},
},
@@ -382,23 +382,23 @@ describe('Dashboards list', () => {
title: 'Folder 2',
expanded: false,
folders: {},
dashboards: {
suggestedNavigations: {
[dashboardWithTwoFolders.spec.dashboard]: {
dashboard: dashboardWithTwoFolders.spec.dashboard,
dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
url: '/d/' + dashboardWithTwoFolders.spec.dashboard,
title: dashboardWithTwoFolders.status.dashboardTitle,
id: dashboardWithTwoFolders.spec.dashboard,
},
},
},
},
dashboards: {},
suggestedNavigations: {},
},
});
});
it('Matches snapshot', () => {
expect(
scopesDashboardsService.groupDashboards([
scopesDashboardsService.groupSuggestedItems([
dashboardWithoutFolder,
dashboardWithOneFolder,
dashboardWithTwoFolders,
@@ -409,35 +409,35 @@ describe('Dashboards list', () => {
])
).toEqual({
'': {
dashboards: {
suggestedNavigations: {
[dashboardWithRootFolderAndOtherFolder.spec.dashboard]: {
dashboard: dashboardWithRootFolderAndOtherFolder.spec.dashboard,
dashboardTitle: dashboardWithRootFolderAndOtherFolder.status.dashboardTitle,
items: [dashboardWithRootFolderAndOtherFolder],
url: '/d/' + dashboardWithRootFolderAndOtherFolder.spec.dashboard,
title: dashboardWithRootFolderAndOtherFolder.status.dashboardTitle,
id: dashboardWithRootFolderAndOtherFolder.spec.dashboard,
},
[dashboardWithRootFolder.spec.dashboard]: {
dashboard: dashboardWithRootFolder.spec.dashboard,
dashboardTitle: dashboardWithRootFolder.status.dashboardTitle,
items: [dashboardWithRootFolder, alternativeDashboardWithRootFolder],
url: '/d/' + dashboardWithRootFolder.spec.dashboard,
title: dashboardWithRootFolder.status.dashboardTitle,
id: dashboardWithRootFolder.spec.dashboard,
},
[dashboardWithoutFolder.spec.dashboard]: {
dashboard: dashboardWithoutFolder.spec.dashboard,
dashboardTitle: dashboardWithoutFolder.status.dashboardTitle,
items: [dashboardWithoutFolder],
url: '/d/' + dashboardWithoutFolder.spec.dashboard,
title: dashboardWithoutFolder.status.dashboardTitle,
id: dashboardWithoutFolder.spec.dashboard,
},
},
folders: {
'Folder 1': {
dashboards: {
suggestedNavigations: {
[dashboardWithOneFolder.spec.dashboard]: {
dashboard: dashboardWithOneFolder.spec.dashboard,
dashboardTitle: dashboardWithOneFolder.status.dashboardTitle,
items: [dashboardWithOneFolder],
url: '/d/' + dashboardWithOneFolder.spec.dashboard,
title: dashboardWithOneFolder.status.dashboardTitle,
id: dashboardWithOneFolder.spec.dashboard,
},
[dashboardWithTwoFolders.spec.dashboard]: {
dashboard: dashboardWithTwoFolders.spec.dashboard,
dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
url: '/d/' + dashboardWithTwoFolders.spec.dashboard,
title: dashboardWithTwoFolders.status.dashboardTitle,
id: dashboardWithTwoFolders.spec.dashboard,
},
},
folders: {},
@@ -445,11 +445,11 @@ describe('Dashboards list', () => {
title: 'Folder 1',
},
'Folder 2': {
dashboards: {
suggestedNavigations: {
[dashboardWithTwoFolders.spec.dashboard]: {
dashboard: dashboardWithTwoFolders.spec.dashboard,
dashboardTitle: dashboardWithTwoFolders.status.dashboardTitle,
items: [dashboardWithTwoFolders, alternativeDashboardWithTwoFolders],
url: '/d/' + dashboardWithTwoFolders.spec.dashboard,
title: dashboardWithTwoFolders.status.dashboardTitle,
id: dashboardWithTwoFolders.spec.dashboard,
},
},
folders: {},
@@ -457,11 +457,11 @@ describe('Dashboards list', () => {
title: 'Folder 2',
},
'Folder 3': {
dashboards: {
suggestedNavigations: {
[dashboardWithRootFolderAndOtherFolder.spec.dashboard]: {
dashboard: dashboardWithRootFolderAndOtherFolder.spec.dashboard,
dashboardTitle: dashboardWithRootFolderAndOtherFolder.status.dashboardTitle,
items: [dashboardWithRootFolderAndOtherFolder],
url: '/d/' + dashboardWithRootFolderAndOtherFolder.spec.dashboard,
title: dashboardWithRootFolderAndOtherFolder.status.dashboardTitle,
id: dashboardWithRootFolderAndOtherFolder.spec.dashboard,
},
},
folders: {},
@@ -489,11 +489,11 @@ describe('Dashboards list', () => {
title: 'Folder 1',
expanded: false,
folders: {},
dashboards: {
suggestedNavigations: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
url: '/d/Dashboard ID',
title: 'Dashboard Title',
id: 'Dashboard ID',
},
},
},
@@ -501,20 +501,20 @@ describe('Dashboards list', () => {
title: 'Folder 2',
expanded: true,
folders: {},
dashboards: {
suggestedNavigations: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
url: '/d/Dashboard ID',
title: 'Dashboard Title',
id: 'Dashboard ID',
},
},
},
},
dashboards: {
suggestedNavigations: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
url: '/d/Dashboard ID',
title: 'Dashboard Title',
id: 'Dashboard ID',
},
},
},
@@ -530,11 +530,11 @@ describe('Dashboards list', () => {
title: 'Folder 1',
expanded: true,
folders: {},
dashboards: {
suggestedNavigations: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
url: '/d/Dashboard ID',
title: 'Dashboard Title',
id: 'Dashboard ID',
},
},
},
@@ -542,16 +542,16 @@ describe('Dashboards list', () => {
title: 'Folder 2',
expanded: true,
folders: {},
dashboards: {
suggestedNavigations: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
url: '/d/Dashboard ID',
title: 'Dashboard Title',
id: 'Dashboard ID',
},
},
},
},
dashboards: {},
suggestedNavigations: {},
},
});
});
@@ -568,11 +568,11 @@ describe('Dashboards list', () => {
title: 'Folder 1',
expanded: false,
folders: {},
dashboards: {
suggestedNavigations: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
url: '/d/Dashboard ID',
title: 'Dashboard Title',
id: 'Dashboard ID',
},
},
},
@@ -580,25 +580,25 @@ describe('Dashboards list', () => {
title: 'Folder 2',
expanded: true,
folders: {},
dashboards: {
suggestedNavigations: {
'Random ID': {
dashboard: 'Random ID',
dashboardTitle: 'Random Title',
items: [],
url: '/d/Random ID',
title: 'Random Title',
id: 'Random ID',
},
},
},
},
dashboards: {
suggestedNavigations: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
url: '/d/Dashboard ID',
title: 'Dashboard Title',
id: 'Dashboard ID',
},
'Random ID': {
dashboard: 'Random ID',
dashboardTitle: 'Random Title',
items: [],
url: '/d/Random ID',
title: 'Random Title',
id: 'Random ID',
},
},
},
@@ -614,20 +614,20 @@ describe('Dashboards list', () => {
title: 'Folder 1',
expanded: true,
folders: {},
dashboards: {
suggestedNavigations: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
url: '/d/Dashboard ID',
title: 'Dashboard Title',
id: 'Dashboard ID',
},
},
},
},
dashboards: {
suggestedNavigations: {
'Dashboard ID': {
dashboard: 'Dashboard ID',
dashboardTitle: 'Dashboard Title',
items: [],
url: '/d/Dashboard ID',
title: 'Dashboard Title',
id: 'Dashboard ID',
},
},
},
+1 -1
View File
@@ -4786,7 +4786,7 @@
"loading": "Loading dashboards",
"noResultsForFilter": "No results found for your query",
"noResultsForFilterClear": "Clear search",
"noResultsForScopes": "No dashboards found for the selected scopes",
"noResultsForScopes": "No dashboards or links found for the selected scopes",
"noResultsNoScopes": "No scopes selected",
"search": "Search",
"toggle": {