From 5940138ddc6cdf8af08ff3e75a34d4c63319ab87 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 16 May 2023 14:55:47 +0100 Subject: [PATCH] [v10.0.x] NestedFolders: Show Dashboard and Folder full breadcrumb hierarchy (#68546) NestedFolders: Show Dashboard and Folder full breadcrumb hierarchy (#68308) * update FolderDTO to match backend struct * hacky way to get folder page breadcrumbs working * hacky way to get dashboard nested breadcrumbs working * undo route changes, get url from folder * fix breadcrumbs in dashboard settings * add parent pages to navIndex * adjust getRootSectionForNode to just return the parent of a leaf node * undo changes to generated files * undo changes to toggles_gen.go * feature toggle dashboardInit code * remove unnecessary code in home dashboard * build navModel directly, don't use getNavModel * don't need fallback here * remove getLoadingNav since it's not used anymore * don't need to hide tabs from breadcrumbs anymore * use id to find dashboards tab (cherry picked from commit 82114cb316f1325cfeb41f8615953d5cd395e276) Co-authored-by: Ashley Harrison --- public/app/core/reducers/navModel.ts | 18 ++++++++--- public/app/core/selectors/navModel.ts | 11 ++++++- public/app/features/alerting/unified/mocks.ts | 5 +++ .../BrowseDashboardsPage.tsx | 18 +++++++++-- .../DashboardSettings/DashboardSettings.tsx | 24 ++++++-------- .../dashboard/containers/DashboardPage.tsx | 8 ++--- .../features/dashboard/state/initDashboard.ts | 7 +++++ public/app/features/folders/state/actions.ts | 5 +-- public/app/features/folders/state/navModel.ts | 31 ++++++++++++++----- .../features/folders/state/reducers.test.ts | 5 +++ .../page/components/SearchView.test.tsx | 5 +++ public/app/types/folders.ts | 21 ++++++++----- 12 files changed, 114 insertions(+), 44 deletions(-) diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index e289681699c..c559a1980e5 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -92,12 +92,20 @@ export const navIndexReducer = (state: NavIndex = initialState, action: AnyActio const newPages: NavIndex = {}; const payload = action.payload; - for (const node of payload.children!) { - newPages[node.id!] = { - ...node, - parentItem: payload, - }; + function addNewPages(node: NavModelItem) { + if (node.children) { + for (const child of node.children) { + newPages[child.id!] = { + ...child, + parentItem: node, + }; + } + } + if (node.parentItem) { + addNewPages(node.parentItem); + } } + addNewPages(payload); return { ...state, ...newPages }; } else if (updateConfigurationSubtitle.match(action)) { diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index 95bdca3aadc..a3ce03f60ea 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -1,4 +1,5 @@ import { NavModel, NavModelItem, NavIndex } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { HOME_NAV_ID } from '../reducers/navModel'; @@ -37,7 +38,15 @@ export const getNavModel = (navIndex: NavIndex, id: string, fallback?: NavModel, }; export function getRootSectionForNode(node: NavModelItem): NavModelItem { - return node.parentItem && node.parentItem.id !== HOME_NAV_ID ? getRootSectionForNode(node.parentItem) : node; + // Don't recurse fully up the tree when nested folders is enabled + // This is to handle folder tabs that still use getNavModel + // Once we've transitioned those pages to build the nav model directly (as in BrowseDashboardsPage) we won't need this + // I _think_ this is correct/safe, but put the change behind the feature toggle just in case + if (config.featureToggles.nestedFolders) { + return node.parentItem && node.parentItem.id !== HOME_NAV_ID ? node.parentItem : node; + } else { + return node.parentItem && node.parentItem.id !== HOME_NAV_ID ? getRootSectionForNode(node.parentItem) : node; + } } function enrichNodeWithActiveState(node: NavModelItem, activeId: string): NavModelItem { diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 34007712684..5d48cb6a61e 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -540,6 +540,11 @@ export const mockFolder = (partial?: Partial): FolderDTO => { canDelete: true, canEdit: true, canSave: true, + created: '', + createdBy: '', + hasAcl: false, + updated: '', + updatedBy: '', ...partial, }; }; diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index 53ed0989428..5c30b1c6cda 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -8,7 +8,7 @@ import { Page } from 'app/core/components/Page/Page'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { useDispatch } from 'app/types'; -import { buildNavModel } from '../folders/state/navModel'; +import { buildNavModel, getDashboardsTabID } from '../folders/state/navModel'; import { useSearchStateManager } from '../search/state/SearchStateManager'; import { getSearchPlaceholder } from '../search/tempI18nPhrases'; @@ -58,7 +58,21 @@ const BrowseDashboardsPage = memo(({ match }: Props) => { }, [isSearching, searchState.result, stateManager]); const { data: folderDTO } = useGetFolderQuery(folderUID ?? skipToken); - const navModel = useMemo(() => (folderDTO ? buildNavModel(folderDTO) : undefined), [folderDTO]); + const navModel = useMemo(() => { + if (!folderDTO) { + return undefined; + } + const model = buildNavModel(folderDTO); + + // Set the "Dashboards" tab to active + const dashboardsTabID = getDashboardsTabID(folderDTO.uid); + const dashboardsTab = model.children?.find((child) => child.id === dashboardsTabID); + if (dashboardsTab) { + dashboardsTab.active = true; + } + return model; + }, [folderDTO]); + const hasSelection = useHasSelection(); const { canEditInFolder, canCreateDashboards, canCreateFolder } = getFolderPermissions(folderDTO); diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx index 624230aefd8..8f8c3860de7 100644 --- a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx @@ -170,6 +170,13 @@ function getSettingsPages(dashboard: DashboardModel) { return pages; } +function applySectionAsParent(node: NavModelItem, parent: NavModelItem): NavModelItem { + return { + ...node, + parentItem: node.parentItem ? applySectionAsParent(node.parentItem, parent) : parent, + }; +} + function getSectionNav( pageNav: NavModelItem, sectionNav: NavModel, @@ -194,22 +201,9 @@ function getSectionNav( subTitle: page.subTitle, })); - if (pageNav.parentItem) { - pageNav = { - ...pageNav, - parentItem: { - ...pageNav.parentItem, - parentItem: sectionNav.node, - }, - }; - } else { - pageNav = { - ...pageNav, - parentItem: sectionNav.node, - }; - } + const pageNavWithSectionParent = applySectionAsParent(pageNav, sectionNav.node); - main.parentItem = pageNav; + main.parentItem = pageNavWithSectionParent; return { main, diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index c1b26833d88..0fa779d8b07 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -523,7 +523,7 @@ export class UnthemedDashboardPage extends PureComponent { } function updateStatePageNavFromProps(props: Props, state: State): State { - const { dashboard } = props; + const { dashboard, navIndex } = props; if (!dashboard) { return state; @@ -545,13 +545,11 @@ function updateStatePageNavFromProps(props: Props, state: State): State { // Check if folder changed const { folderTitle, folderUid } = dashboard.meta; + const folderNavModel = folderUid ? getNavModel(navIndex, `folder-dashboards-${folderUid}`).main : undefined; if (folderTitle && folderUid && pageNav && pageNav.parentItem?.text !== folderTitle) { pageNav = { ...pageNav, - parentItem: { - text: folderTitle, - url: `/dashboards/f/${dashboard.meta.folderUid}`, - }, + parentItem: folderNavModel, }; } diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index e3baea1250d..c4786a2529d 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -9,6 +9,7 @@ import store from 'app/core/store'; import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { DashboardSrv, getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { getFolderByUid } from 'app/features/folders/state/actions'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { toStateKey } from 'app/features/variables/utils'; @@ -80,6 +81,12 @@ async function fetchDashboard( case DashboardRoutes.Normal: { const dashDTO: DashboardDTO = await dashboardLoaderSrv.loadDashboard(args.urlType, args.urlSlug, args.urlUid); + // only the folder API has information about ancestors + // get parent folder (if it exists) and put it in the store + // this will be used to populate the full breadcrumb trail + if (config.featureToggles.nestedFolders && dashDTO.meta.folderUid) { + await dispatch(getFolderByUid(dashDTO.meta.folderUid)); + } if (args.fixUrl && dashDTO.meta.url && !playlistSrv.isPlaying) { // check if the current url is correct (might be old slug) const dashboardUrl = locationUtil.stripBaseFromUrl(dashDTO.meta.url); diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index ed9d5b8ca89..0f08ca1cf98 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -6,17 +6,18 @@ import { notifyApp, updateNavIndex } from 'app/core/actions'; import { createSuccessNotification, createWarningNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/core'; import { backendSrv } from 'app/core/services/backend_srv'; -import { FolderState, ThunkResult } from 'app/types'; +import { FolderDTO, FolderState, ThunkResult } from 'app/types'; import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel } from 'app/types/acl'; import { buildNavModel } from './navModel'; import { loadFolder, loadFolderPermissions, setCanViewFolderPermissions } from './reducers'; -export function getFolderByUid(uid: string): ThunkResult { +export function getFolderByUid(uid: string): ThunkResult> { return async (dispatch) => { const folder = await backendSrv.getFolderByUid(uid); dispatch(loadFolder(folder)); dispatch(updateNavIndex(buildNavModel(folder))); + return folder; }; } diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts index 341e07ac880..468535807c0 100644 --- a/public/app/features/folders/state/navModel.ts +++ b/public/app/features/folders/state/navModel.ts @@ -3,29 +3,41 @@ import { config } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction, FolderDTO } from 'app/types'; -export function buildNavModel(folder: FolderDTO): NavModelItem { +export const getDashboardsTabID = (folderUID: string) => `folder-dashboards-${folderUID}`; +export const getLibraryPanelsTabID = (folderUID: string) => `folder-library-panels-${folderUID}`; +export const getAlertingTabID = (folderUID: string) => `folder-alerting-${folderUID}`; +export const getPermissionsTabID = (folderUID: string) => `folder-permissions-${folderUID}`; +export const getSettingsTabID = (folderUID: string) => `folder-settings-${folderUID}`; + +export function buildNavModel(folder: FolderDTO, parents = folder.parents): NavModelItem { const model: NavModelItem = { icon: 'folder', id: 'manage-folder', subTitle: 'Manage folder dashboards and permissions', - url: '', + url: folder.url, text: folder.title, breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], children: [ { active: false, icon: 'apps', - id: `folder-dashboards-${folder.uid}`, + id: getDashboardsTabID(folder.uid), text: 'Dashboards', url: folder.url, }, ], }; + if (parents && parents.length > 0) { + const parent = parents[parents.length - 1]; + const remainingParents = parents.slice(0, parents.length - 1); + model.parentItem = buildNavModel(parent, remainingParents); + } + model.children!.push({ active: false, icon: 'library-panel', - id: `folder-library-panels-${folder.uid}`, + id: getLibraryPanelsTabID(folder.uid), text: 'Panels', url: `${folder.url}/library-panels`, }); @@ -34,7 +46,7 @@ export function buildNavModel(folder: FolderDTO): NavModelItem { model.children!.push({ active: false, icon: 'bell', - id: `folder-alerting-${folder.uid}`, + id: getAlertingTabID(folder.uid), text: 'Alert rules', url: `${folder.url}/alerting`, }); @@ -44,7 +56,7 @@ export function buildNavModel(folder: FolderDTO): NavModelItem { model.children!.push({ active: false, icon: 'lock', - id: `folder-permissions-${folder.uid}`, + id: getPermissionsTabID(folder.uid), text: 'Permissions', url: `${folder.url}/permissions`, }); @@ -54,7 +66,7 @@ export function buildNavModel(folder: FolderDTO): NavModelItem { model.children!.push({ active: false, icon: 'cog', - id: `folder-settings-${folder.uid}`, + id: getSettingsTabID(folder.uid), text: 'Settings', url: `${folder.url}/settings`, }); @@ -65,6 +77,11 @@ export function buildNavModel(folder: FolderDTO): NavModelItem { export function getLoadingNav(tabIndex: number): NavModel { const main = buildNavModel({ + created: '', + createdBy: '', + hasAcl: false, + updated: '', + updatedBy: '', id: 1, uid: 'loading', title: 'Loading', diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts index f68a707a591..d6ea57ce82f 100644 --- a/public/app/features/folders/state/reducers.test.ts +++ b/public/app/features/folders/state/reducers.test.ts @@ -22,6 +22,11 @@ function getTestFolder(): FolderDTO { canAdmin: true, canDelete: true, version: 0, + created: '', + createdBy: '', + hasAcl: false, + updated: '', + updatedBy: '', }; } diff --git a/public/app/features/search/page/components/SearchView.test.tsx b/public/app/features/search/page/components/SearchView.test.tsx index 9f0954f18f2..e2808e1a62d 100644 --- a/public/app/features/search/page/components/SearchView.test.tsx +++ b/public/app/features/search/page/components/SearchView.test.tsx @@ -137,6 +137,11 @@ describe('SearchView', () => { canEdit: true, canAdmin: true, canDelete: true, + created: '', + createdBy: '', + hasAcl: false, + updated: '', + updatedBy: '', }, }, undefined diff --git a/public/app/types/folders.ts b/public/app/types/folders.ts index 96b372f8ff4..f4d7df84bb5 100644 --- a/public/app/types/folders.ts +++ b/public/app/types/folders.ts @@ -3,15 +3,22 @@ import { WithAccessControlMetadata } from '@grafana/data'; import { DashboardAcl } from './acl'; export interface FolderDTO extends WithAccessControlMetadata { - id: number; - uid: string; - title: string; - url: string; - version: number; - canSave: boolean; - canEdit: boolean; canAdmin: boolean; canDelete: boolean; + canEdit: boolean; + canSave: boolean; + created: string; + createdBy: string; + hasAcl: boolean; + id: number; + parentUid?: string; + parents?: FolderDTO[]; + title: string; + uid: string; + updated: string; + updatedBy: string; + url: string; + version?: number; } export interface FolderState {