mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
0db397b8be
commit
82114cb316
@ -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)) {
|
||||
|
@ -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 {
|
||||
|
@ -540,6 +540,11 @@ export const mockFolder = (partial?: Partial<FolderDTO>): FolderDTO => {
|
||||
canDelete: true,
|
||||
canEdit: true,
|
||||
canSave: true,
|
||||
created: '',
|
||||
createdBy: '',
|
||||
hasAcl: false,
|
||||
updated: '',
|
||||
updatedBy: '',
|
||||
...partial,
|
||||
};
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -22,6 +22,11 @@ function getTestFolder(): FolderDTO {
|
||||
canAdmin: true,
|
||||
canDelete: true,
|
||||
version: 0,
|
||||
created: '',
|
||||
createdBy: '',
|
||||
hasAcl: false,
|
||||
updated: '',
|
||||
updatedBy: '',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -137,6 +137,11 @@ describe('SearchView', () => {
|
||||
canEdit: true,
|
||||
canAdmin: true,
|
||||
canDelete: true,
|
||||
created: '',
|
||||
createdBy: '',
|
||||
hasAcl: false,
|
||||
updated: '',
|
||||
updatedBy: '',
|
||||
},
|
||||
},
|
||||
undefined
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user