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 newPages: NavIndex = {};
|
||||||
const payload = action.payload;
|
const payload = action.payload;
|
||||||
|
|
||||||
for (const node of payload.children!) {
|
function addNewPages(node: NavModelItem) {
|
||||||
newPages[node.id!] = {
|
if (node.children) {
|
||||||
...node,
|
for (const child of node.children) {
|
||||||
parentItem: payload,
|
newPages[child.id!] = {
|
||||||
};
|
...child,
|
||||||
|
parentItem: node,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (node.parentItem) {
|
||||||
|
addNewPages(node.parentItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
addNewPages(payload);
|
||||||
|
|
||||||
return { ...state, ...newPages };
|
return { ...state, ...newPages };
|
||||||
} else if (updateConfigurationSubtitle.match(action)) {
|
} else if (updateConfigurationSubtitle.match(action)) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { NavModel, NavModelItem, NavIndex } from '@grafana/data';
|
import { NavModel, NavModelItem, NavIndex } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
import { HOME_NAV_ID } from '../reducers/navModel';
|
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 {
|
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 {
|
function enrichNodeWithActiveState(node: NavModelItem, activeId: string): NavModelItem {
|
||||||
|
@ -540,6 +540,11 @@ export const mockFolder = (partial?: Partial<FolderDTO>): FolderDTO => {
|
|||||||
canDelete: true,
|
canDelete: true,
|
||||||
canEdit: true,
|
canEdit: true,
|
||||||
canSave: true,
|
canSave: true,
|
||||||
|
created: '',
|
||||||
|
createdBy: '',
|
||||||
|
hasAcl: false,
|
||||||
|
updated: '',
|
||||||
|
updatedBy: '',
|
||||||
...partial,
|
...partial,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -8,7 +8,7 @@ import { Page } from 'app/core/components/Page/Page';
|
|||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
import { useDispatch } from 'app/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 { useSearchStateManager } from '../search/state/SearchStateManager';
|
||||||
import { getSearchPlaceholder } from '../search/tempI18nPhrases';
|
import { getSearchPlaceholder } from '../search/tempI18nPhrases';
|
||||||
|
|
||||||
@ -58,7 +58,21 @@ const BrowseDashboardsPage = memo(({ match }: Props) => {
|
|||||||
}, [isSearching, searchState.result, stateManager]);
|
}, [isSearching, searchState.result, stateManager]);
|
||||||
|
|
||||||
const { data: folderDTO } = useGetFolderQuery(folderUID ?? skipToken);
|
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 hasSelection = useHasSelection();
|
||||||
|
|
||||||
const { canEditInFolder, canCreateDashboards, canCreateFolder } = getFolderPermissions(folderDTO);
|
const { canEditInFolder, canCreateDashboards, canCreateFolder } = getFolderPermissions(folderDTO);
|
||||||
|
@ -170,6 +170,13 @@ function getSettingsPages(dashboard: DashboardModel) {
|
|||||||
return pages;
|
return pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applySectionAsParent(node: NavModelItem, parent: NavModelItem): NavModelItem {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
parentItem: node.parentItem ? applySectionAsParent(node.parentItem, parent) : parent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getSectionNav(
|
function getSectionNav(
|
||||||
pageNav: NavModelItem,
|
pageNav: NavModelItem,
|
||||||
sectionNav: NavModel,
|
sectionNav: NavModel,
|
||||||
@ -194,22 +201,9 @@ function getSectionNav(
|
|||||||
subTitle: page.subTitle,
|
subTitle: page.subTitle,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (pageNav.parentItem) {
|
const pageNavWithSectionParent = applySectionAsParent(pageNav, sectionNav.node);
|
||||||
pageNav = {
|
|
||||||
...pageNav,
|
|
||||||
parentItem: {
|
|
||||||
...pageNav.parentItem,
|
|
||||||
parentItem: sectionNav.node,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
pageNav = {
|
|
||||||
...pageNav,
|
|
||||||
parentItem: sectionNav.node,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
main.parentItem = pageNav;
|
main.parentItem = pageNavWithSectionParent;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
main,
|
main,
|
||||||
|
@ -417,7 +417,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateStatePageNavFromProps(props: Props, state: State): State {
|
function updateStatePageNavFromProps(props: Props, state: State): State {
|
||||||
const { dashboard } = props;
|
const { dashboard, navIndex } = props;
|
||||||
|
|
||||||
if (!dashboard) {
|
if (!dashboard) {
|
||||||
return state;
|
return state;
|
||||||
@ -439,13 +439,11 @@ function updateStatePageNavFromProps(props: Props, state: State): State {
|
|||||||
|
|
||||||
// Check if folder changed
|
// Check if folder changed
|
||||||
const { folderTitle, folderUid } = dashboard.meta;
|
const { folderTitle, folderUid } = dashboard.meta;
|
||||||
|
const folderNavModel = folderUid ? getNavModel(navIndex, `folder-dashboards-${folderUid}`).main : undefined;
|
||||||
if (folderTitle && folderUid && pageNav && pageNav.parentItem?.text !== folderTitle) {
|
if (folderTitle && folderUid && pageNav && pageNav.parentItem?.text !== folderTitle) {
|
||||||
pageNav = {
|
pageNav = {
|
||||||
...pageNav,
|
...pageNav,
|
||||||
parentItem: {
|
parentItem: folderNavModel,
|
||||||
text: folderTitle,
|
|
||||||
url: `/dashboards/f/${dashboard.meta.folderUid}`,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import store from 'app/core/store';
|
|||||||
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||||
import { DashboardSrv, getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
import { DashboardSrv, getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
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 { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||||
import { toStateKey } from 'app/features/variables/utils';
|
import { toStateKey } from 'app/features/variables/utils';
|
||||||
@ -80,6 +81,12 @@ async function fetchDashboard(
|
|||||||
case DashboardRoutes.Normal: {
|
case DashboardRoutes.Normal: {
|
||||||
const dashDTO: DashboardDTO = await dashboardLoaderSrv.loadDashboard(args.urlType, args.urlSlug, args.urlUid);
|
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) {
|
if (args.fixUrl && dashDTO.meta.url && !playlistSrv.isPlaying) {
|
||||||
// check if the current url is correct (might be old slug)
|
// check if the current url is correct (might be old slug)
|
||||||
const dashboardUrl = locationUtil.stripBaseFromUrl(dashDTO.meta.url);
|
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 { createSuccessNotification, createWarningNotification } from 'app/core/copy/appNotification';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
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 { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel } from 'app/types/acl';
|
||||||
|
|
||||||
import { buildNavModel } from './navModel';
|
import { buildNavModel } from './navModel';
|
||||||
import { loadFolder, loadFolderPermissions, setCanViewFolderPermissions } from './reducers';
|
import { loadFolder, loadFolderPermissions, setCanViewFolderPermissions } from './reducers';
|
||||||
|
|
||||||
export function getFolderByUid(uid: string): ThunkResult<void> {
|
export function getFolderByUid(uid: string): ThunkResult<Promise<FolderDTO>> {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
const folder = await backendSrv.getFolderByUid(uid);
|
const folder = await backendSrv.getFolderByUid(uid);
|
||||||
dispatch(loadFolder(folder));
|
dispatch(loadFolder(folder));
|
||||||
dispatch(updateNavIndex(buildNavModel(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 { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { AccessControlAction, FolderDTO } from 'app/types';
|
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 = {
|
const model: NavModelItem = {
|
||||||
icon: 'folder',
|
icon: 'folder',
|
||||||
id: 'manage-folder',
|
id: 'manage-folder',
|
||||||
subTitle: 'Manage folder dashboards and permissions',
|
subTitle: 'Manage folder dashboards and permissions',
|
||||||
url: '',
|
url: folder.url,
|
||||||
text: folder.title,
|
text: folder.title,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
active: false,
|
active: false,
|
||||||
icon: 'apps',
|
icon: 'apps',
|
||||||
id: `folder-dashboards-${folder.uid}`,
|
id: getDashboardsTabID(folder.uid),
|
||||||
text: 'Dashboards',
|
text: 'Dashboards',
|
||||||
url: folder.url,
|
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({
|
model.children!.push({
|
||||||
active: false,
|
active: false,
|
||||||
icon: 'library-panel',
|
icon: 'library-panel',
|
||||||
id: `folder-library-panels-${folder.uid}`,
|
id: getLibraryPanelsTabID(folder.uid),
|
||||||
text: 'Panels',
|
text: 'Panels',
|
||||||
url: `${folder.url}/library-panels`,
|
url: `${folder.url}/library-panels`,
|
||||||
});
|
});
|
||||||
@ -33,7 +45,7 @@ export function buildNavModel(folder: FolderDTO): NavModelItem {
|
|||||||
model.children!.push({
|
model.children!.push({
|
||||||
active: false,
|
active: false,
|
||||||
icon: 'bell',
|
icon: 'bell',
|
||||||
id: `folder-alerting-${folder.uid}`,
|
id: getAlertingTabID(folder.uid),
|
||||||
text: 'Alert rules',
|
text: 'Alert rules',
|
||||||
url: `${folder.url}/alerting`,
|
url: `${folder.url}/alerting`,
|
||||||
});
|
});
|
||||||
@ -43,7 +55,7 @@ export function buildNavModel(folder: FolderDTO): NavModelItem {
|
|||||||
model.children!.push({
|
model.children!.push({
|
||||||
active: false,
|
active: false,
|
||||||
icon: 'lock',
|
icon: 'lock',
|
||||||
id: `folder-permissions-${folder.uid}`,
|
id: getPermissionsTabID(folder.uid),
|
||||||
text: 'Permissions',
|
text: 'Permissions',
|
||||||
url: `${folder.url}/permissions`,
|
url: `${folder.url}/permissions`,
|
||||||
});
|
});
|
||||||
@ -53,7 +65,7 @@ export function buildNavModel(folder: FolderDTO): NavModelItem {
|
|||||||
model.children!.push({
|
model.children!.push({
|
||||||
active: false,
|
active: false,
|
||||||
icon: 'cog',
|
icon: 'cog',
|
||||||
id: `folder-settings-${folder.uid}`,
|
id: getSettingsTabID(folder.uid),
|
||||||
text: 'Settings',
|
text: 'Settings',
|
||||||
url: `${folder.url}/settings`,
|
url: `${folder.url}/settings`,
|
||||||
});
|
});
|
||||||
@ -64,6 +76,11 @@ export function buildNavModel(folder: FolderDTO): NavModelItem {
|
|||||||
|
|
||||||
export function getLoadingNav(tabIndex: number): NavModel {
|
export function getLoadingNav(tabIndex: number): NavModel {
|
||||||
const main = buildNavModel({
|
const main = buildNavModel({
|
||||||
|
created: '',
|
||||||
|
createdBy: '',
|
||||||
|
hasAcl: false,
|
||||||
|
updated: '',
|
||||||
|
updatedBy: '',
|
||||||
id: 1,
|
id: 1,
|
||||||
uid: 'loading',
|
uid: 'loading',
|
||||||
title: 'Loading',
|
title: 'Loading',
|
||||||
|
@ -22,6 +22,11 @@ function getTestFolder(): FolderDTO {
|
|||||||
canAdmin: true,
|
canAdmin: true,
|
||||||
canDelete: true,
|
canDelete: true,
|
||||||
version: 0,
|
version: 0,
|
||||||
|
created: '',
|
||||||
|
createdBy: '',
|
||||||
|
hasAcl: false,
|
||||||
|
updated: '',
|
||||||
|
updatedBy: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +137,11 @@ describe('SearchView', () => {
|
|||||||
canEdit: true,
|
canEdit: true,
|
||||||
canAdmin: true,
|
canAdmin: true,
|
||||||
canDelete: true,
|
canDelete: true,
|
||||||
|
created: '',
|
||||||
|
createdBy: '',
|
||||||
|
hasAcl: false,
|
||||||
|
updated: '',
|
||||||
|
updatedBy: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
undefined
|
undefined
|
||||||
|
@ -3,15 +3,22 @@ import { WithAccessControlMetadata } from '@grafana/data';
|
|||||||
import { DashboardAcl } from './acl';
|
import { DashboardAcl } from './acl';
|
||||||
|
|
||||||
export interface FolderDTO extends WithAccessControlMetadata {
|
export interface FolderDTO extends WithAccessControlMetadata {
|
||||||
id: number;
|
|
||||||
uid: string;
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
version: number;
|
|
||||||
canSave: boolean;
|
|
||||||
canEdit: boolean;
|
|
||||||
canAdmin: boolean;
|
canAdmin: boolean;
|
||||||
canDelete: 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 {
|
export interface FolderState {
|
||||||
|
Loading…
Reference in New Issue
Block a user