mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Navigation: show breadcrumbs correctly when on the home page (#55759)
* show breadcrumbs correctly when on the home page * adjust breadcrumb unit tests * update betterer * fix backend tests * update getSectionRoot to look at the home nav id * remove redundant setting of home dashboard * construct a home navmodelitem in the backend * fix cases when the feature toggle is off * fix unit test * fix more unit tests * refactor how buildBreadcrumbs works * use HOME_NAV_ID * move homeNav useSelector into NavToolbar * remove unnecesary cloneDeep * don't need locationUtil here * restore using getUrlForPartial in DashboardPage * special case for the editview query param * remove commented out code * add comment to clarify splice behaviour * slightly cleaner syntax
This commit is contained in:
parent
0d348dc0b1
commit
8984507291
@ -538,7 +538,6 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response {
|
||||
}()
|
||||
|
||||
dash := dtos.DashboardFullWithMeta{}
|
||||
dash.Meta.IsHome = true
|
||||
dash.Meta.CanEdit = c.SignedInUser.HasRole(org.RoleEditor)
|
||||
dash.Meta.FolderTitle = "General"
|
||||
dash.Dashboard = simplejson.New()
|
||||
|
@ -78,7 +78,6 @@ func TestGetHomeDashboard(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dash := dtos.DashboardFullWithMeta{}
|
||||
dash.Meta.IsHome = true
|
||||
dash.Meta.FolderTitle = "General"
|
||||
|
||||
homeDashJSON, err := os.ReadFile(tc.expectedDashboardPath)
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
|
||||
type DashboardMeta struct {
|
||||
IsStarred bool `json:"isStarred,omitempty"`
|
||||
IsHome bool `json:"isHome,omitempty"`
|
||||
IsSnapshot bool `json:"isSnapshot,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
CanSave bool `json:"canSave"`
|
||||
|
@ -11,7 +11,8 @@ const (
|
||||
// are negative to ensure that the default items are placed above
|
||||
// any items with default weight.
|
||||
|
||||
WeightSavedItems = (iota - 20) * 100
|
||||
WeightHome = (iota - 20) * 100
|
||||
WeightSavedItems
|
||||
WeightCreate
|
||||
WeightDashboard
|
||||
WeightExplore
|
||||
|
@ -69,8 +69,12 @@ func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
|
||||
hasAccess := ac.HasAccess(s.accessControl, c)
|
||||
treeRoot := &navtree.NavTreeRoot{}
|
||||
|
||||
if s.features.IsEnabled(featuremgmt.FlagTopnav) {
|
||||
treeRoot.AddSection(s.getHomeNode(c, prefs))
|
||||
}
|
||||
|
||||
if hasAccess(ac.ReqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsRead)) {
|
||||
starredItemsLinks, err := s.buildStarredItemsNavLinks(c, prefs)
|
||||
starredItemsLinks, err := s.buildStarredItemsNavLinks(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -186,6 +190,32 @@ func (s *ServiceImpl) GetNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
|
||||
return treeRoot, nil
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) getHomeNode(c *models.ReqContext, prefs *pref.Preference) *navtree.NavLink {
|
||||
homeUrl := s.cfg.AppSubURL + "/"
|
||||
homePage := s.cfg.HomePage
|
||||
|
||||
if prefs.HomeDashboardID == 0 && len(homePage) > 0 {
|
||||
homeUrl = homePage
|
||||
}
|
||||
|
||||
if prefs.HomeDashboardID != 0 {
|
||||
slugQuery := models.GetDashboardRefByIdQuery{Id: prefs.HomeDashboardID}
|
||||
err := s.dashboardService.GetDashboardUIDById(c.Req.Context(), &slugQuery)
|
||||
if err == nil {
|
||||
homeUrl = models.GetDashboardUrl(slugQuery.Result.Uid, slugQuery.Result.Slug)
|
||||
}
|
||||
}
|
||||
|
||||
return &navtree.NavLink{
|
||||
Text: "Home",
|
||||
Id: "home",
|
||||
Url: homeUrl,
|
||||
Icon: "home-alt",
|
||||
Section: navtree.NavSectionCore,
|
||||
SortWeight: navtree.WeightHome,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) addHelpLinks(treeRoot *navtree.NavTreeRoot, c *models.ReqContext) {
|
||||
if setting.HelpEnabled {
|
||||
helpVersion := fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit)
|
||||
@ -256,7 +286,7 @@ func (s *ServiceImpl) getProfileNode(c *models.ReqContext) *navtree.NavLink {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext, prefs *pref.Preference) ([]*navtree.NavLink, error) {
|
||||
func (s *ServiceImpl) buildStarredItemsNavLinks(c *models.ReqContext) ([]*navtree.NavLink, error) {
|
||||
starredItemsChildNavs := []*navtree.NavLink{}
|
||||
|
||||
query := star.GetUserStarsQuery{
|
||||
|
@ -3,6 +3,8 @@ import React from 'react';
|
||||
|
||||
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||
import { Icon, IconButton, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
import { HOME_NAV_ID } from 'app/core/reducers/navModel';
|
||||
import { useSelector } from 'app/types';
|
||||
|
||||
import { Breadcrumbs } from '../Breadcrumbs/Breadcrumbs';
|
||||
import { buildBreadcrumbs } from '../Breadcrumbs/utils';
|
||||
@ -29,8 +31,9 @@ export function NavToolbar({
|
||||
onToggleSearchBar,
|
||||
onToggleKioskMode,
|
||||
}: Props) {
|
||||
const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID];
|
||||
const styles = useStyles2(getStyles);
|
||||
const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav);
|
||||
const breadcrumbs = buildBreadcrumbs(homeNav, sectionNav, pageNav);
|
||||
|
||||
return (
|
||||
<div className={styles.pageToolbar}>
|
||||
|
@ -2,26 +2,20 @@ import { NavModelItem } from '@grafana/data';
|
||||
|
||||
import { buildBreadcrumbs } from './utils';
|
||||
|
||||
const mockHomeNav: NavModelItem = {
|
||||
text: 'Home',
|
||||
url: '/home',
|
||||
id: 'home',
|
||||
};
|
||||
|
||||
describe('breadcrumb utils', () => {
|
||||
describe('buildBreadcrumbs', () => {
|
||||
it('includes the home breadcrumb at the root', () => {
|
||||
const sectionNav: NavModelItem = {
|
||||
text: 'My section',
|
||||
url: '/my-section',
|
||||
};
|
||||
const result = buildBreadcrumbs(sectionNav);
|
||||
expect(result[0]).toEqual({ href: '/', text: 'Home' });
|
||||
});
|
||||
|
||||
it('includes breadcrumbs for the section nav', () => {
|
||||
const sectionNav: NavModelItem = {
|
||||
text: 'My section',
|
||||
url: '/my-section',
|
||||
};
|
||||
expect(buildBreadcrumbs(sectionNav)).toEqual([
|
||||
{ href: '/', text: 'Home' },
|
||||
{ text: 'My section', href: '/my-section' },
|
||||
]);
|
||||
expect(buildBreadcrumbs(mockHomeNav, sectionNav)).toEqual([{ text: 'My section', href: '/my-section' }]);
|
||||
});
|
||||
|
||||
it('includes breadcrumbs for the page nav', () => {
|
||||
@ -34,8 +28,7 @@ describe('breadcrumb utils', () => {
|
||||
text: 'My page',
|
||||
url: '/my-page',
|
||||
};
|
||||
expect(buildBreadcrumbs(sectionNav, pageNav)).toEqual([
|
||||
{ href: '/', text: 'Home' },
|
||||
expect(buildBreadcrumbs(mockHomeNav, sectionNav, pageNav)).toEqual([
|
||||
{ text: 'My section', href: '/my-section' },
|
||||
{ text: 'My page', href: '/my-page' },
|
||||
]);
|
||||
@ -50,8 +43,7 @@ describe('breadcrumb utils', () => {
|
||||
url: '/my-parent-section',
|
||||
},
|
||||
};
|
||||
expect(buildBreadcrumbs(sectionNav)).toEqual([
|
||||
{ href: '/', text: 'Home' },
|
||||
expect(buildBreadcrumbs(mockHomeNav, sectionNav)).toEqual([
|
||||
{ text: 'My parent section', href: '/my-parent-section' },
|
||||
{ text: 'My section', href: '/my-section' },
|
||||
]);
|
||||
@ -74,13 +66,83 @@ describe('breadcrumb utils', () => {
|
||||
url: '/my-parent-section',
|
||||
},
|
||||
};
|
||||
expect(buildBreadcrumbs(sectionNav, pageNav)).toEqual([
|
||||
{ href: '/', text: 'Home' },
|
||||
expect(buildBreadcrumbs(mockHomeNav, sectionNav, pageNav)).toEqual([
|
||||
{ text: 'My parent section', href: '/my-parent-section' },
|
||||
{ text: 'My section', href: '/my-section' },
|
||||
{ text: 'My parent page', href: '/my-parent-page' },
|
||||
{ text: 'My page', href: '/my-page' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('shortcircuits if the home nav is found early', () => {
|
||||
const pageNav: NavModelItem = {
|
||||
text: 'My page',
|
||||
url: '/my-page',
|
||||
parentItem: {
|
||||
text: 'My parent page',
|
||||
url: '/home',
|
||||
},
|
||||
};
|
||||
const sectionNav: NavModelItem = {
|
||||
text: 'My section',
|
||||
url: '/my-section',
|
||||
parentItem: {
|
||||
text: 'My parent section',
|
||||
url: '/my-parent-section',
|
||||
},
|
||||
};
|
||||
expect(buildBreadcrumbs(mockHomeNav, sectionNav, pageNav)).toEqual([
|
||||
{ text: 'Home', href: '/home' },
|
||||
{ text: 'My page', href: '/my-page' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('matches the home nav ignoring query parameters', () => {
|
||||
const pageNav: NavModelItem = {
|
||||
text: 'My page',
|
||||
url: '/my-page',
|
||||
parentItem: {
|
||||
text: 'My parent page',
|
||||
url: '/home?orgId=1',
|
||||
},
|
||||
};
|
||||
const sectionNav: NavModelItem = {
|
||||
text: 'My section',
|
||||
url: '/my-section',
|
||||
parentItem: {
|
||||
text: 'My parent section',
|
||||
url: '/my-parent-section',
|
||||
},
|
||||
};
|
||||
expect(buildBreadcrumbs(mockHomeNav, sectionNav, pageNav)).toEqual([
|
||||
{ text: 'Home', href: '/home?orgId=1' },
|
||||
{ text: 'My page', href: '/my-page' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not match the home nav if the editview param is different', () => {
|
||||
const pageNav: NavModelItem = {
|
||||
text: 'My page',
|
||||
url: '/my-page',
|
||||
parentItem: {
|
||||
text: 'My parent page',
|
||||
url: '/home?orgId=1&editview=settings',
|
||||
},
|
||||
};
|
||||
const sectionNav: NavModelItem = {
|
||||
text: 'My section',
|
||||
url: '/my-section',
|
||||
parentItem: {
|
||||
text: 'My parent section',
|
||||
url: '/my-parent-section',
|
||||
},
|
||||
};
|
||||
expect(buildBreadcrumbs(mockHomeNav, sectionNav, pageNav)).toEqual([
|
||||
{ text: 'My parent section', href: '/my-parent-section' },
|
||||
{ text: 'My section', href: '/my-section' },
|
||||
{ text: 'My parent page', href: '/home?orgId=1&editview=settings' },
|
||||
{ text: 'My page', href: '/my-page' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -2,24 +2,37 @@ import { NavModelItem } from '@grafana/data';
|
||||
|
||||
import { Breadcrumb } from './types';
|
||||
|
||||
export function buildBreadcrumbs(sectionNav: NavModelItem, pageNav?: NavModelItem) {
|
||||
const crumbs: Breadcrumb[] = [{ href: '/', text: 'Home' }];
|
||||
export function buildBreadcrumbs(homeNav: NavModelItem, sectionNav: NavModelItem, pageNav?: NavModelItem) {
|
||||
const crumbs: Breadcrumb[] = [];
|
||||
let foundHome = false;
|
||||
|
||||
function addCrumbs(node: NavModelItem) {
|
||||
// construct the URL to match
|
||||
// we want to ignore query params except for the editview query param
|
||||
const urlSearchParams = new URLSearchParams(node.url?.split('?')[1]);
|
||||
let urlToMatch = `${node.url?.split('?')[0]}`;
|
||||
if (urlSearchParams.has('editview')) {
|
||||
urlToMatch += `?editview=${urlSearchParams.get('editview')}`;
|
||||
}
|
||||
if (!foundHome && !node.hideFromBreadcrumbs) {
|
||||
if (urlToMatch === homeNav.url) {
|
||||
crumbs.unshift({ text: homeNav.text, href: node.url ?? '' });
|
||||
foundHome = true;
|
||||
} else {
|
||||
crumbs.unshift({ text: node.text, href: node.url ?? '' });
|
||||
}
|
||||
}
|
||||
|
||||
if (node.parentItem) {
|
||||
addCrumbs(node.parentItem);
|
||||
}
|
||||
|
||||
if (!node.hideFromBreadcrumbs) {
|
||||
crumbs.push({ text: node.text, href: node.url ?? '' });
|
||||
}
|
||||
}
|
||||
|
||||
addCrumbs(sectionNav);
|
||||
|
||||
if (pageNav) {
|
||||
addCrumbs(pageNav);
|
||||
}
|
||||
|
||||
addCrumbs(sectionNav);
|
||||
|
||||
return crumbs;
|
||||
}
|
||||
|
@ -56,7 +56,6 @@ describe('MegaMenu', () => {
|
||||
setup();
|
||||
|
||||
expect(await screen.findByTestId('navbarmenu')).toBeInTheDocument();
|
||||
expect(await screen.findByRole('link', { name: 'Home' })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('link', { name: 'Section name' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
@ -3,8 +3,7 @@ import { cloneDeep } from 'lodash';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { GrafanaTheme2, NavModelItem, NavSection } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { GrafanaTheme2, NavSection } from '@grafana/data';
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
import { useSelector } from 'app/types';
|
||||
|
||||
@ -23,16 +22,6 @@ export const MegaMenu = React.memo<Props>(({ onClose, searchBarHidden }) => {
|
||||
const styles = getStyles(theme);
|
||||
const location = useLocation();
|
||||
|
||||
const homeItem: NavModelItem = enrichWithInteractionTracking(
|
||||
{
|
||||
id: 'home',
|
||||
text: 'Home',
|
||||
url: config.appSubUrl || '/',
|
||||
icon: 'home-alt',
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
const navTree = cloneDeep(navBarTree);
|
||||
|
||||
const coreItems = navTree
|
||||
@ -46,7 +35,7 @@ export const MegaMenu = React.memo<Props>(({ onClose, searchBarHidden }) => {
|
||||
location
|
||||
).map((item) => enrichWithInteractionTracking(item, true));
|
||||
|
||||
const navItems = [homeItem, ...coreItems, ...pluginItems, ...configItems];
|
||||
const navItems = [...coreItems, ...pluginItems, ...configItems];
|
||||
|
||||
const activeItem = getActiveItem(navItems, location.pathname);
|
||||
|
||||
|
@ -4,10 +4,19 @@ import { cloneDeep } from 'lodash';
|
||||
import { NavIndex, NavModel, NavModelItem } from '@grafana/data';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export const HOME_NAV_ID = 'home';
|
||||
|
||||
export function buildInitialState(): NavIndex {
|
||||
const navIndex: NavIndex = {};
|
||||
const rootNodes = cloneDeep(config.bootData.navTree as NavModelItem[]);
|
||||
buildNavIndex(navIndex, rootNodes);
|
||||
const homeNav = rootNodes.find((node) => node.id === HOME_NAV_ID);
|
||||
|
||||
// set home as parent for the rootNodes
|
||||
buildNavIndex(navIndex, rootNodes, homeNav);
|
||||
// remove circular parent reference on the home node
|
||||
if (navIndex[HOME_NAV_ID]) {
|
||||
delete navIndex[HOME_NAV_ID].parentItem;
|
||||
}
|
||||
return navIndex;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { NavModel, NavModelItem, NavIndex } from '@grafana/data';
|
||||
|
||||
import { HOME_NAV_ID } from '../reducers/navModel';
|
||||
|
||||
const getNotFoundModel = (): NavModel => {
|
||||
const node: NavModelItem = {
|
||||
id: 'not-found',
|
||||
@ -35,7 +37,7 @@ export const getNavModel = (navIndex: NavIndex, id: string, fallback?: NavModel,
|
||||
};
|
||||
|
||||
function getSectionRoot(node: NavModelItem): NavModelItem {
|
||||
return node.parentItem ? getSectionRoot(node.parentItem) : node;
|
||||
return node.parentItem && node.parentItem.id !== HOME_NAV_ID ? getSectionRoot(node.parentItem) : node;
|
||||
}
|
||||
|
||||
function enrichNodeWithActiveState(node: NavModelItem, activeId: string): NavModelItem {
|
||||
|
@ -2,7 +2,7 @@ import { cx } from '@emotion/css';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { locationUtil, NavModel, NavModelItem, TimeRange, PageLayoutType } from '@grafana/data';
|
||||
import { NavModel, NavModelItem, TimeRange, PageLayoutType, locationUtil } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { Themeable2, withTheme2 } from '@grafana/ui';
|
||||
@ -459,6 +459,7 @@ function updateStatePageNavFromProps(props: Props, state: State): State {
|
||||
...pageNav,
|
||||
text: `${state.editPanel ? 'Edit' : 'View'} panel`,
|
||||
parentItem: pageNav,
|
||||
url: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ jest.mock('@grafana/runtime', () => ({
|
||||
get: jest.fn().mockResolvedValue([{ userId: 1, login: 'Test' }]),
|
||||
}),
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
bootData: { navTree: [], user: {} },
|
||||
},
|
||||
}));
|
||||
|
Loading…
Reference in New Issue
Block a user