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:
co-authored by
Andrej Ocenas
parent
483c6ac70d
commit
c321afdeb7
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user