diff --git a/packages/grafana-ui/src/components/Tabs/VerticalTab.tsx b/packages/grafana-ui/src/components/Tabs/VerticalTab.tsx index c1fdeccff2b..6ae690c19a0 100644 --- a/packages/grafana-ui/src/components/Tabs/VerticalTab.tsx +++ b/packages/grafana-ui/src/components/Tabs/VerticalTab.tsx @@ -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%); } diff --git a/pkg/api/index.go b/pkg/api/index.go index dacaaabb94f..9291efcecca 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -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 diff --git a/pkg/api/navlinks/navlinks.go b/pkg/api/navlinks/navlinks.go index 1f39f0d3db7..38f0668ac1c 100644 --- a/pkg/api/navlinks/navlinks.go +++ b/pkg/api/navlinks/navlinks.go @@ -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", diff --git a/public/app/core/components/PageNew/SectionNav.tsx b/public/app/core/components/PageNew/SectionNav.tsx index 0172722562d..2bed640b5e6 100644 --- a/public/app/core/components/PageNew/SectionNav.tsx +++ b/public/app/core/components/PageNew/SectionNav.tsx @@ -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', }), }; }; diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index 6be4673248d..0df0d26ace9 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -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); } } diff --git a/public/app/core/selectors/navModel.test.ts b/public/app/core/selectors/navModel.test.ts new file mode 100644 index 00000000000..951bc5fa3e0 --- /dev/null +++ b/public/app/core/selectors/navModel.test.ts @@ -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); + }); +}); diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index 9a520c9b900..61ef0a7d9b1 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -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 : ''}`; };