diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 663bc425b9f..83a4d159d5e 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -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 */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 2853154e3fa..c851e583daa 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 5b4c8114e2c..635af7a1e7b 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index f873d3d6e47..11f7841caf2 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 4939f4b6ba0..172e7a15a61 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -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", diff --git a/public/app/features/scopes/ScopesApiClient.ts b/public/app/features/scopes/ScopesApiClient.ts index 358d1b97e4d..5020df6b82a 100644 --- a/public/app/features/scopes/ScopesApiClient.ts +++ b/public/app/features/scopes/ScopesApiClient.ts @@ -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 => { + try { + const response = await getBackendSrv().get<{ items: ScopeNavigation[] }>(apiUrl + `/find/scope_navigations`, { + scope: scopeNames, + }); + + return response?.items ?? []; + } catch (err) { + return []; + } + }; } diff --git a/public/app/features/scopes/dashboards/ScopesDashboards.tsx b/public/app/features/scopes/dashboards/ScopesDashboards.tsx index 5d931e980c6..d0f28643766 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboards.tsx +++ b/public/app/features/scopes/dashboards/ScopesDashboards.tsx @@ -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() { No scopes selected ); - } else if (dashboards.length === 0) { + } else if (dashboards.length === 0 && scopeNavigations.length === 0) { return (
- No dashboards found for the selected scopes + + No dashboards or links found for the selected scopes +
); } diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsService.ts b/public/app/features/scopes/dashboards/ScopesDashboardsService.ts index 5b6b261bdbe..5044867eab6 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsService.ts +++ b/public/app/features/scopes/dashboards/ScopesDashboardsService.ts @@ -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; // 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 { 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 0 }); + this.updateState({ + scopeNavigations: res, + filteredFolders, + folders, + loading: false, + drawerOpened: res.length > 0, + }); } }; - public groupDashboards = (dashboards: ScopeDashboardBinding[]): SuggestedDashboardsFoldersMap => { - return dashboards.reduce( - (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 + ): 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((acc, [folderId, folder]) => { + return Object.entries(folders).reduce((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 - 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), }; } diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsTree.tsx b/public/app/features/scopes/dashboards/ScopesDashboardsTree.tsx index c90ce0fd729..5e276837d2e 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsTree.tsx +++ b/public/app/features/scopes/dashboards/ScopesDashboardsTree.tsx @@ -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) => ( - + {Object.values(folder.suggestedNavigations).map((navigation) => ( + ))} ); diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsTreeDashboardItem.tsx b/public/app/features/scopes/dashboards/ScopesDashboardsTreeDashboardItem.tsx deleted file mode 100644 index 5566deebb66..00000000000 --- a/public/app/features/scopes/dashboards/ScopesDashboardsTreeDashboardItem.tsx +++ /dev/null @@ -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 ( - - {dashboard.dashboardTitle} - - ); -} - -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), - }), - }; -}; diff --git a/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx b/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx index 333a2d3f6bc..33ae1e23400 100644 --- a/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx +++ b/public/app/features/scopes/dashboards/ScopesDashboardsTreeFolderItem.tsx @@ -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; } diff --git a/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.tsx b/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.tsx new file mode 100644 index 00000000000..03c25d4f8c7 --- /dev/null +++ b/public/app/features/scopes/dashboards/ScopesNavigationTreeLink.tsx @@ -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 ( + + {title} + + ); +} + +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([ + ['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), + }), + }; +}; diff --git a/public/app/features/scopes/dashboards/types.ts b/public/app/features/scopes/dashboards/types.ts index acf478e346a..78242f28c59 100644 --- a/public/app/features/scopes/dashboards/types.ts +++ b/public/app/features/scopes/dashboards/types.ts @@ -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; -export type SuggestedDashboardsFoldersMap = Record; +export interface SuggestedNavigationsFolder { + title: string; + expanded: boolean; + folders: SuggestedNavigationsFoldersMap; + suggestedNavigations: SuggestedNavigationsMap; +} +export type SuggestedNavigationsFoldersMap = Record; +export type SuggestedNavigationsMap = Record; export type OnFolderUpdate = (path: string[], expanded: boolean) => void; diff --git a/public/app/features/scopes/selector/ScopesSelectorService.ts b/public/app/features/scopes/selector/ScopesSelectorService.ts index 54b3aa6e5fb..966d017d4a9 100644 --- a/public/app/features/scopes/selector/ScopesSelectorService.ts +++ b/public/app/features/scopes/selector/ScopesSelectorService.ts @@ -184,6 +184,7 @@ export class ScopesSelectorService extends ScopesServiceBase scope.metadata.name)); selectedScopes = await this.apiClient.fetchMultipleScopes(treeScopes); diff --git a/public/app/features/scopes/tests/dashboardsList.test.ts b/public/app/features/scopes/tests/dashboardsList.test.ts index eec2829b144..30a33ad200c 100644 --- a/public/app/features/scopes/tests/dashboardsList.test.ts +++ b/public/app/features/scopes/tests/dashboardsList.test.ts @@ -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', }, }, }, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 48bf288226d..f7c125251d1 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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": {