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
This commit is contained in:
Ashley Harrison 2023-05-16 13:54:44 +01:00 committed by GitHub
parent 0db397b8be
commit 82114cb316
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 114 additions and 44 deletions

View File

@ -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)) {

View File

@ -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 {

View File

@ -540,6 +540,11 @@ export const mockFolder = (partial?: Partial<FolderDTO>): FolderDTO => {
canDelete: true,
canEdit: true,
canSave: true,
created: '',
createdBy: '',
hasAcl: false,
updated: '',
updatedBy: '',
...partial,
};
};

View File

@ -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);

View File

@ -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,

View File

@ -417,7 +417,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
}
function updateStatePageNavFromProps(props: Props, state: State): State {
const { dashboard } = props;
const { dashboard, navIndex } = props;
if (!dashboard) {
return state;
@ -439,13 +439,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,
};
}

View File

@ -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);

View File

@ -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<void> {
export function getFolderByUid(uid: string): ThunkResult<Promise<FolderDTO>> {
return async (dispatch) => {
const folder = await backendSrv.getFolderByUid(uid);
dispatch(loadFolder(folder));
dispatch(updateNavIndex(buildNavModel(folder)));
return folder;
};
}

View File

@ -3,28 +3,40 @@ 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,
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`,
});
@ -33,7 +45,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`,
});
@ -43,7 +55,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`,
});
@ -53,7 +65,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`,
});
@ -64,6 +76,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',

View File

@ -22,6 +22,11 @@ function getTestFolder(): FolderDTO {
canAdmin: true,
canDelete: true,
version: 0,
created: '',
createdBy: '',
hasAcl: false,
updated: '',
updatedBy: '',
};
}

View File

@ -137,6 +137,11 @@ describe('SearchView', () => {
canEdit: true,
canAdmin: true,
canDelete: true,
created: '',
createdBy: '',
hasAcl: false,
updated: '',
updatedBy: '',
},
},
undefined

View File

@ -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 {