TopNav: Support for deeper nesting in section nav (#52562)

This commit is contained in:
Torkel Ödegaard 2022-07-22 10:42:41 +02:00 committed by GitHub
parent 8d938175f5
commit 5b275ca3f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 111 additions and 27 deletions

View File

@ -52,7 +52,6 @@ const getTabStyles = (theme: GrafanaTheme2) => {
margin-right: ${theme.spacing(2)};
position: relative;
display: block;
margin-bottom: 4px;
`,
link: css`
padding: 6px 12px;
@ -83,8 +82,8 @@ const getTabStyles = (theme: GrafanaTheme2) => {
position: absolute;
left: 0;
width: 4px;
bottom: 0;
top: 0;
bottom: 2px;
top: 2px;
border-radius: 2px;
background-image: linear-gradient(0deg, #f05a28 30%, #fbca0a 99%);
}

View File

@ -234,7 +234,21 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
if err != nil {
return nil, err
}
navTree = append(navTree, appLinks...)
// When topnav is enabled we can test new information architecture where plugins live in Apps category
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
navTree = append(navTree, &dtos.NavLink{
Text: "Apps",
Icon: "apps",
Description: "App plugins",
Id: "apps",
Children: appLinks,
Section: dtos.NavSectionCore,
Url: hs.Cfg.AppSubURL + "/apps",
})
} else {
navTree = append(navTree, appLinks...)
}
configNodes := []*dtos.NavLink{}
@ -335,8 +349,11 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
})
}
var configNode *dtos.NavLink
var serverAdminNode *dtos.NavLink
if len(configNodes) > 0 {
configNode := &dtos.NavLink{
configNode = &dtos.NavLink{
Id: dtos.NavIDCfg,
Text: "Configuration",
SubTitle: "Organization: " + c.OrgName,
@ -352,10 +369,20 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
adminNavLinks := hs.buildAdminNavLinks(c)
if len(adminNavLinks) > 0 {
serverAdminNode := navlinks.GetServerAdminNode(adminNavLinks)
serverAdminNode = navlinks.GetServerAdminNode(adminNavLinks)
navTree = append(navTree, serverAdminNode)
}
if hs.Features.IsEnabled(featuremgmt.FlagTopnav) {
// Move server admin into Configuration and rename to administration
if configNode != nil && serverAdminNode != nil {
configNode.Text = "Administration"
configNode.Children = append(configNode.Children, serverAdminNode)
adminNodeIndex := len(navTree) - 1
navTree = navTree[:adminNodeIndex]
}
}
navTree = hs.addHelpLinks(navTree, c)
return navTree, nil

View File

@ -8,7 +8,7 @@ func GetServerAdminNode(children []*dtos.NavLink) *dtos.NavLink {
url = children[0].Url
}
return &dtos.NavLink{
Text: "Server Admin",
Text: "Server admin",
SubTitle: "Manage all users and orgs",
HideFromTabs: true,
Id: "admin",

View File

@ -88,9 +88,8 @@ const getStyles = (theme: GrafanaTheme2) => {
height: 48,
}),
subSection: css({
padding: theme.spacing(3, 0, 1, 1),
padding: theme.spacing(3, 0, 0.5, 1),
fontWeight: 500,
fontSize: '16px',
}),
};
};

View File

@ -12,13 +12,15 @@ export function buildInitialState(): NavIndex {
function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem?: NavModelItem) {
for (const node of children) {
navIndex[node.id!] = {
const newNode = {
...node,
parentItem: parentItem,
};
navIndex[node.id!] = newNode;
if (node.children) {
buildNavIndex(navIndex, node.children, node);
buildNavIndex(navIndex, node.children, newNode);
}
}

View File

@ -0,0 +1,52 @@
import { config } from '@grafana/runtime';
import { buildInitialState } from '../reducers/navModel';
import { getNavModel } from './navModel';
describe('getNavModel', () => {
config.bootData.navTree = [
{
text: 'Apps',
id: 'apps',
url: 'apps',
children: [
{ text: '', id: 'apps/child1', url: 'apps/child1' },
{ text: '', id: 'apps/child2', url: 'apps/child2' },
{
text: '',
id: 'apps/subapp',
url: 'section/subapp',
children: [
{ text: '', id: 'apps/subapp/child1', url: 'apps/subapp/child1' },
{ text: '', id: 'apps/subapp/child2', url: 'apps/subapp/child2' },
],
},
],
},
];
const navIndex = buildInitialState();
test('returns the correct nav model for root node', () => {
const navModel = getNavModel(navIndex, 'apps');
expect(navModel.main.id).toBe('apps');
expect(navModel.node.id).toBe('apps');
});
test('returns the correct nav model a 1st-level child', () => {
const navModel = getNavModel(navIndex, 'apps/child1');
expect(navModel.main.id).toBe('apps');
expect(navModel.node.id).toBe('apps/child1');
expect(navModel.main.children![0].active).toBe(true);
expect(navModel.node.parentItem?.id).toBe(navModel.main.id);
});
test('returns the correct nav model for a 2nd-level child', () => {
const navModel = getNavModel(navIndex, 'apps/subapp/child1');
expect(navModel.main.id).toBe('apps');
expect(navModel.node.id).toBe('apps/subapp/child1');
expect(navModel.main.children![2].active).toBe(true);
expect(navModel.main.children![2].children![0].active).toBe(true);
});
});

View File

@ -18,22 +18,7 @@ const getNotFoundModel = (): NavModel => {
export const getNavModel = (navIndex: NavIndex, id: string, fallback?: NavModel, onlyChild = false): NavModel => {
if (navIndex[id]) {
const node = navIndex[id];
let main: NavModelItem;
if (!onlyChild && node.parentItem) {
main = { ...node.parentItem };
main.children =
main.children &&
main.children.map((item) => {
return {
...item,
active: item.url === node.url,
};
});
} else {
main = node;
}
const main = onlyChild ? node : getSectionRoot(node);
return {
node,
@ -48,6 +33,26 @@ export const getNavModel = (navIndex: NavIndex, id: string, fallback?: NavModel,
return getNotFoundModel();
};
function getSectionRoot(node: NavModelItem): NavModelItem {
if (!node.parentItem) {
return node;
}
const root = (node.parentItem = { ...node.parentItem });
if (root.children) {
root.children = root.children.map((item) => {
if (item.id === node.id) {
return { ...node, active: true };
}
return item;
});
}
return getSectionRoot(root);
}
export const getTitleFromNavModel = (navModel: NavModel) => {
return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : ''}`;
};