From 28b336ac8081036ba830709c6c4edf25a313180f Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Tue, 6 Feb 2024 13:43:11 +0000 Subject: [PATCH] DockedMegaMenu: Clean up toggle and old code (#81878) * remove toggle * remove code not behind toggle * remove old MegaMenu * rename DockedMegaMenu -> MegaMenu and clean up go code * fix backend test * run yarn i18n:extract * fix some unit tests * fix remaining unit tests * fix remaining e2e/unit tests --- .betterer.results | 6 - .../feature-toggles/index.md | 1 - .../frontend-sandbox-app.spec.ts | 22 +- e2e/various-suite/graph-auto-migrate.spec.ts | 2 +- e2e/various-suite/navigation.spec.ts | 6 +- .../src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 8 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/navtree/navtreeimpl/applinks.go | 41 +--- .../navtree/navtreeimpl/applinks_test.go | 2 +- .../components/AppChrome/AppChrome.test.tsx | 34 +-- .../core/components/AppChrome/AppChrome.tsx | 28 +-- .../components/AppChrome/AppChromeMenu.tsx | 2 +- .../components/AppChrome/AppChromeService.tsx | 18 +- .../DockedMegaMenu/MegaMenu.test.tsx | 82 ------- .../AppChrome/DockedMegaMenu/MegaMenu.tsx | 132 ---------- .../AppChrome/DockedMegaMenu/utils.test.ts | 186 -------------- .../AppChrome/DockedMegaMenu/utils.ts | 142 ----------- .../FeatureHighlight.tsx | 0 .../AppChrome/MegaMenu/MegaMenu.test.tsx | 28 ++- .../AppChrome/MegaMenu/MegaMenu.tsx | 138 ++++++++--- .../MegaMenuItem.tsx | 0 .../MegaMenuItemText.tsx | 0 .../AppChrome/MegaMenu/NavBarItemIcon.tsx | 38 --- .../AppChrome/MegaMenu/NavBarMenu.tsx | 231 ------------------ .../AppChrome/MegaMenu/NavBarMenuItem.tsx | 139 ----------- .../MegaMenu/NavBarMenuItemWrapper.tsx | 105 -------- .../AppChrome/MegaMenu/NavBarMenuSection.tsx | 117 --------- .../MegaMenu/NavFeatureHighlight.tsx | 34 --- .../components/AppChrome/MegaMenu/utils.ts | 18 +- .../AppChrome/SectionNav/SectionNav.tsx | 109 --------- .../SectionNav/SectionNavItem.test.tsx | 25 -- .../AppChrome/SectionNav/SectionNavItem.tsx | 131 ---------- .../AppChrome/SectionNav/SectionNavToggle.tsx | 68 ------ .../app/core/components/Breadcrumbs/utils.ts | 10 +- public/app/core/components/Page/Page.tsx | 41 +--- .../core/navigation/GrafanaRouteLoading.tsx | 13 +- .../app/core/utils/navBarItem-translations.ts | 5 +- .../AccessControlDashboardPermissions.tsx | 3 +- .../DashboardSettings/AnnotationsSettings.tsx | 4 +- .../DashboardSettings/DashboardSettings.tsx | 9 +- .../DashboardSettings/GeneralSettings.tsx | 2 +- .../DashboardSettings/JsonEditorSettings.tsx | 3 +- .../DashboardSettings/LinksSettings.test.tsx | 4 +- .../DashboardSettings/LinksSettings.tsx | 21 +- .../DashboardSettings/VersionsSettings.tsx | 3 +- .../ServiceAccountCreatePage.test.tsx | 1 + public/app/features/teams/TeamPages.test.tsx | 1 + .../editor/VariableEditorContainer.tsx | 15 +- public/locales/en-US/grafana.json | 3 - public/locales/pseudo-LOCALE/grafana.json | 3 - 52 files changed, 218 insertions(+), 1822 deletions(-) delete mode 100644 public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx delete mode 100644 public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx delete mode 100644 public/app/core/components/AppChrome/DockedMegaMenu/utils.test.ts delete mode 100644 public/app/core/components/AppChrome/DockedMegaMenu/utils.ts rename public/app/core/components/AppChrome/{DockedMegaMenu => MegaMenu}/FeatureHighlight.tsx (100%) rename public/app/core/components/AppChrome/{DockedMegaMenu => MegaMenu}/MegaMenuItem.tsx (100%) rename public/app/core/components/AppChrome/{DockedMegaMenu => MegaMenu}/MegaMenuItemText.tsx (100%) delete mode 100644 public/app/core/components/AppChrome/MegaMenu/NavBarItemIcon.tsx delete mode 100644 public/app/core/components/AppChrome/MegaMenu/NavBarMenu.tsx delete mode 100644 public/app/core/components/AppChrome/MegaMenu/NavBarMenuItem.tsx delete mode 100644 public/app/core/components/AppChrome/MegaMenu/NavBarMenuItemWrapper.tsx delete mode 100644 public/app/core/components/AppChrome/MegaMenu/NavBarMenuSection.tsx delete mode 100644 public/app/core/components/AppChrome/MegaMenu/NavFeatureHighlight.tsx delete mode 100644 public/app/core/components/AppChrome/SectionNav/SectionNav.tsx delete mode 100644 public/app/core/components/AppChrome/SectionNav/SectionNavItem.test.tsx delete mode 100644 public/app/core/components/AppChrome/SectionNav/SectionNavItem.tsx delete mode 100644 public/app/core/components/AppChrome/SectionNav/SectionNavToggle.tsx diff --git a/.betterer.results b/.betterer.results index c3afe7fd90f..0d2c5197690 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1175,12 +1175,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Unexpected any. Specify a different type.", "6"] ], - "public/app/core/components/AppChrome/SectionNav/SectionNavItem.tsx:5381": [ - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], - [0, 0, 0, "Styles should be written using objects.", "1"], - [0, 0, 0, "Styles should be written using objects.", "2"], - [0, 0, 0, "Styles should be written using objects.", "3"] - ], "public/app/core/components/AppChrome/TopBar/TopSearchBarCommandPaletteTrigger.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 2079e4b704e..e54f1e1e81c 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -105,7 +105,6 @@ Experimental features might be changed or removed without prior notice. | `scenes` | Experimental framework to build interactive dashboards | | `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | | `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | -| `dockedMegaMenu` | Enable support for a persistent (docked) navigation menu | | `returnToPrevious` | Enables the return to previous context functionality | | `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | | `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | diff --git a/e2e/various-suite/frontend-sandbox-app.spec.ts b/e2e/various-suite/frontend-sandbox-app.spec.ts index 9bcf4d49832..b3e17ece6a9 100644 --- a/e2e/various-suite/frontend-sandbox-app.spec.ts +++ b/e2e/various-suite/frontend-sandbox-app.spec.ts @@ -26,21 +26,14 @@ describe('Datasource sandbox', () => { }); it('Loads the app page without the sandbox div wrapper', () => { - e2e.pages.Home.visit(); - e2e.components.NavBar.Toggle.button().click(); - e2e.components.NavToolbar.container().get('[aria-label="Expand section Apps"]').click(); - e2e.components.NavMenu.item().contains('Sandbox app test plugin').click(); + cy.visit(`/a/${APP_ID}`); cy.wait(200); // wait to prevent false positives because cypress checks too fast cy.get('div[data-plugin-sandbox="sandbox-app-test"]').should('not.exist'); cy.get('div[data-testid="sandbox-app-test-page-one"]').should('exist'); }); it('Loads the app configuration without the sandbox div wrapper', () => { - e2e.pages.Home.visit(); - e2e.components.NavBar.Toggle.button().click(); - e2e.components.NavToolbar.container().get('[aria-label="Expand section Apps"]').click(); - e2e.components.NavMenu.item().contains('Apps').click(); - cy.get('a[aria-label="Tab Sandbox App Page"]').click(); + cy.visit(`/plugins/${APP_ID}`); cy.wait(200); // wait to prevent false positives because cypress checks too fast cy.get('div[data-plugin-sandbox="sandbox-app-test"]').should('not.exist'); cy.get('div[data-testid="sandbox-app-test-config-page"]').should('exist'); @@ -55,20 +48,13 @@ describe('Datasource sandbox', () => { }); it('Loads the app page with the sandbox div wrapper', () => { - e2e.pages.Home.visit(); - e2e.components.NavBar.Toggle.button().click(); - e2e.components.NavToolbar.container().get('[aria-label="Expand section Apps"]').click(); - e2e.components.NavMenu.item().contains('Sandbox app test plugin').click(); + cy.visit(`/a/${APP_ID}`); cy.get('div[data-plugin-sandbox="sandbox-app-test"]').should('exist'); cy.get('div[data-testid="sandbox-app-test-page-one"]').should('exist'); }); it('Loads the app configuration with the sandbox div wrapper', () => { - e2e.pages.Home.visit(); - e2e.components.NavBar.Toggle.button().click(); - e2e.components.NavToolbar.container().get('[aria-label="Expand section Apps"]').click(); - e2e.components.NavMenu.item().contains('Apps').click(); - cy.get('a[aria-label="Tab Sandbox App Page"]').click(); + cy.visit(`/plugins/${APP_ID}`); cy.get('div[data-plugin-sandbox="sandbox-app-test"]').should('exist'); cy.get('div[data-testid="sandbox-app-test-config-page"]').should('exist'); }); diff --git a/e2e/various-suite/graph-auto-migrate.spec.ts b/e2e/various-suite/graph-auto-migrate.spec.ts index 4360477ce61..2889eae6382 100644 --- a/e2e/various-suite/graph-auto-migrate.spec.ts +++ b/e2e/various-suite/graph-auto-migrate.spec.ts @@ -32,7 +32,7 @@ describe('Auto-migrate graph panel', () => { e2e.pages.Dashboard.Annotations.marker().should('exist'); }); - cy.get('body').children().find('.scrollbar-view').first().scrollTo('bottom'); + cy.get('#pageContent .scrollbar-view').first().scrollTo('bottom'); e2e.components.Panels.Panel.title('05:00') .should('exist') diff --git a/e2e/various-suite/navigation.spec.ts b/e2e/various-suite/navigation.spec.ts index b068d56f3ad..099a0c875f4 100644 --- a/e2e/various-suite/navigation.spec.ts +++ b/e2e/various-suite/navigation.spec.ts @@ -6,11 +6,7 @@ describe('Docked Navigation', () => { cy.viewport(1280, 800); e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); - cy.visit(fromBaseUrl('/'), { - onBeforeLoad(window) { - window.localStorage.setItem('grafana.featureToggles', 'dockedMegaMenu=1'); - }, - }); + cy.visit(fromBaseUrl('/')); }); it('should remain docked when reloading the page', () => { diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e200c425377..8f5a8d9517c 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -41,7 +41,6 @@ export interface FeatureToggles { logRequestsInstrumentedAsUnknown?: boolean; dataConnectionsConsole?: boolean; topnav?: boolean; - dockedMegaMenu?: boolean; returnToPrevious?: boolean; grpcServer?: boolean; unifiedStorage?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 8ffaed7b6ac..39fb0b2907f 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -203,14 +203,6 @@ var ( Owner: grafanaFrontendPlatformSquad, Created: time.Date(2022, time.June, 20, 12, 0, 0, 0, time.UTC), }, - { - Name: "dockedMegaMenu", - Description: "Enable support for a persistent (docked) navigation menu", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaFrontendPlatformSquad, - Created: time.Date(2023, time.September, 18, 12, 0, 0, 0, time.UTC), - }, { Name: "returnToPrevious", Description: "Enables the return to previous context functionality", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 7b4ff2e734c..025ca9a8c5f 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -22,7 +22,6 @@ disableSecretsCompatibility,experimental,@grafana/hosted-grafana-team,2022-07-13 logRequestsInstrumentedAsUnknown,experimental,@grafana/hosted-grafana-team,2022-06-10,false,false,false dataConnectionsConsole,GA,@grafana/plugins-platform-backend,2022-06-01,false,false,false topnav,deprecated,@grafana/grafana-frontend-platform,2022-06-20,false,false,false -dockedMegaMenu,experimental,@grafana/grafana-frontend-platform,2023-09-18,false,false,true returnToPrevious,experimental,@grafana/grafana-frontend-platform,2024-01-09,false,false,true grpcServer,preview,@grafana/grafana-app-platform-squad,2022-09-27,false,false,false unifiedStorage,experimental,@grafana/grafana-app-platform-squad,2022-12-01,true,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index f8cfe3a4b92..91b0db67bb7 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -99,10 +99,6 @@ const ( // Enables topnav support in external plugins. The new Grafana navigation cannot be disabled. FlagTopnav = "topnav" - // FlagDockedMegaMenu - // Enable support for a persistent (docked) navigation menu - FlagDockedMegaMenu = "dockedMegaMenu" - // FlagReturnToPrevious // Enables the return to previous context functionality FlagReturnToPrevious = "returnToPrevious" diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index b8e3eb96019..c81a8b7633e 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -289,41 +289,12 @@ func (s *ServiceImpl) hasAccessToInclude(c *contextmodel.ReqContext, pluginID st } func (s *ServiceImpl) readNavigationSettings() { - k8sCfg := NavigationAppConfig{SectionID: navtree.NavIDMonitoring, SortWeight: 1, Text: "Kubernetes"} - appO11yCfg := NavigationAppConfig{SectionID: navtree.NavIDMonitoring, SortWeight: 2, Text: "Application"} - profilesCfg := NavigationAppConfig{SectionID: navtree.NavIDMonitoring, SortWeight: 3, Text: "Profiles"} - frontendCfg := NavigationAppConfig{SectionID: navtree.NavIDMonitoring, SortWeight: 4, Text: "Frontend"} - k6Cfg := NavigationAppConfig{SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightAlertsAndIncidents + 1, Text: "Performance testing", Icon: "k6"} - syntheticsCfg := NavigationAppConfig{SectionID: navtree.NavIDMonitoring, SortWeight: 5, Text: "Synthetics"} - - if s.features.IsEnabledGlobally(featuremgmt.FlagDockedMegaMenu) { - k8sCfg.SectionID = navtree.NavIDInfrastructure - - appO11yCfg.SectionID = navtree.NavIDRoot - appO11yCfg.SortWeight = navtree.WeightApplication - appO11yCfg.Icon = "graph-bar" - - profilesCfg.SectionID = navtree.NavIDExplore - profilesCfg.SortWeight = 1 - - frontendCfg.SectionID = navtree.NavIDRoot - frontendCfg.SortWeight = navtree.WeightFrontend - frontendCfg.Icon = "frontend-observability" - - k6Cfg.SectionID = navtree.NavIDTestingAndSynthetics - k6Cfg.SortWeight = 1 - k6Cfg.Text = "Performance" - - syntheticsCfg.SectionID = navtree.NavIDTestingAndSynthetics - syntheticsCfg.SortWeight = 2 - } - s.navigationAppConfig = map[string]NavigationAppConfig{ - "grafana-k8s-app": k8sCfg, - "grafana-app-observability-app": appO11yCfg, - "grafana-pyroscope-app": profilesCfg, - "grafana-kowalski-app": frontendCfg, - "grafana-synthetic-monitoring-app": syntheticsCfg, + "grafana-k8s-app": {SectionID: navtree.NavIDInfrastructure, SortWeight: 1, Text: "Kubernetes"}, + "grafana-app-observability-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightApplication, Text: "Application", Icon: "graph-bar"}, + "grafana-pyroscope-app": {SectionID: navtree.NavIDExplore, SortWeight: 1, Text: "Profiles"}, + "grafana-kowalski-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightFrontend, Text: "Frontend", Icon: "frontend-observability"}, + "grafana-synthetic-monitoring-app": {SectionID: navtree.NavIDTestingAndSynthetics, SortWeight: 2, Text: "Synthetics"}, "grafana-oncall-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 1, Text: "OnCall"}, "grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2, Text: "Incidents"}, "grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3, Text: "Machine Learning"}, @@ -332,7 +303,7 @@ func (s *ServiceImpl) readNavigationSettings() { "grafana-adaptive-metrics-app": {SectionID: navtree.NavIDCfg, Text: "Adaptive Metrics"}, "grafana-logvolumeexplorer-app": {SectionID: navtree.NavIDCfg, Text: "Log Volume Explorer"}, "grafana-easystart-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightApps + 1, Text: "Connections", Icon: "adjust-circle"}, - "k6-app": k6Cfg, + "k6-app": {SectionID: navtree.NavIDTestingAndSynthetics, SortWeight: 1, Text: "Performance"}, "grafana-asserts-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightAsserts, Icon: "asserts"}, } diff --git a/pkg/services/navtree/navtreeimpl/applinks_test.go b/pkg/services/navtree/navtreeimpl/applinks_test.go index a877638fd6b..a526caef899 100644 --- a/pkg/services/navtree/navtreeimpl/applinks_test.go +++ b/pkg/services/navtree/navtreeimpl/applinks_test.go @@ -367,7 +367,7 @@ func TestReadingNavigationSettings(t *testing.T) { _, _ = service.cfg.Raw.NewSection("navigation.app_sections") service.readNavigationSettings() - require.Equal(t, "monitoring", service.navigationAppConfig["grafana-k8s-app"].SectionID) + require.Equal(t, "infrastructure", service.navigationAppConfig["grafana-k8s-app"].SectionID) }) t.Run("Can add additional overrides via ini system", func(t *testing.T) { diff --git a/public/app/core/components/AppChrome/AppChrome.test.tsx b/public/app/core/components/AppChrome/AppChrome.test.tsx index 0893f09e7af..870030ab187 100644 --- a/public/app/core/components/AppChrome/AppChrome.test.tsx +++ b/public/app/core/components/AppChrome/AppChrome.test.tsx @@ -5,7 +5,7 @@ import React, { ReactNode } from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; -import { DataFrame, DataFrameView, FieldType, NavModelItem } from '@grafana/data'; +import { DataFrame, DataFrameView, FieldType } from '@grafana/data'; import { config } from '@grafana/runtime'; import { HOME_NAV_ID } from 'app/core/reducers/navModel'; import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from 'app/features/search/service'; @@ -19,14 +19,6 @@ jest.mock('@grafana/runtime', () => ({ getPluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), })); -const pageNav: NavModelItem = { - text: 'pageNav title', - children: [ - { text: 'pageNav child1', url: '1', active: true }, - { text: 'pageNav child2', url: '2' }, - ], -}; - const searchData: DataFrame = { fields: [ { name: 'kind', type: FieldType.string, config: {}, values: [] }, @@ -92,30 +84,6 @@ describe('AppChrome', () => { jest.clearAllMocks(); }); - it('should render section nav model based on navId', async () => { - setup(Children); - expect(await screen.findByTestId('page-children')).toBeInTheDocument(); - - expect(screen.getByRole('tab', { name: 'Tab Section name' })).toBeInTheDocument(); - expect(screen.getByRole('tab', { name: 'Tab Child1' })).toBeInTheDocument(); - expect(screen.getByRole('tab', { name: 'Tab Child1' })).toBeInTheDocument(); - expect(screen.getAllByRole('tab').length).toBe(3); - }); - - it('should render section nav model based on navId and item page nav', async () => { - setup( - - Children - - ); - expect(await screen.findByTestId('page-children')).toBeInTheDocument(); - - expect(screen.getByRole('tab', { name: 'Tab Section name' })).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'pageNav title' })).toBeInTheDocument(); - expect(screen.getByRole('tab', { name: 'Tab Child1' })).toBeInTheDocument(); - expect(screen.getByRole('tab', { name: 'Tab pageNav child1' })).toBeInTheDocument(); - }); - it('should create a skip link to skip to main content', async () => { setup(Children); expect(await screen.findByRole('link', { name: 'Skip to main content' })).toBeInTheDocument(); diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index 2846bda2b93..ac5a9b62231 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css'; import classNames from 'classnames'; import React, { PropsWithChildren, useEffect } from 'react'; -import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; +import { GrafanaTheme2 } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { useStyles2, LinkButton, useTheme2 } from '@grafana/ui'; import config from 'app/core/config'; @@ -14,11 +14,9 @@ import { KioskMode } from 'app/types'; import { AppChromeMenu } from './AppChromeMenu'; import { DOCKED_LOCAL_STORAGE_KEY, DOCKED_MENU_OPEN_LOCAL_STORAGE_KEY } from './AppChromeService'; -import { MegaMenu as DockedMegaMenu } from './DockedMegaMenu/MegaMenu'; import { MegaMenu } from './MegaMenu/MegaMenu'; import { NavToolbar } from './NavToolbar/NavToolbar'; import { ReturnToPrevious } from './ReturnToPrevious/ReturnToPrevious'; -import { SectionNav } from './SectionNav/SectionNav'; import { TopSearchBar } from './TopBar/TopSearchBar'; import { TOP_BAR_LEVEL_HEIGHT } from './types'; @@ -36,7 +34,7 @@ export function AppChrome({ children }: Props) { useMediaQueryChange({ breakpoint: dockedMenuBreakpoint, onChange: (e) => { - if (config.featureToggles.dockedMegaMenu && dockedMenuLocalStorageState) { + if (dockedMenuLocalStorageState) { chrome.setMegaMenuDocked(e.matches, false); chrome.setMegaMenuOpen( e.matches ? store.getBool(DOCKED_MENU_OPEN_LOCAL_STORAGE_KEY, state.megaMenuOpen) : false @@ -99,26 +97,15 @@ export function AppChrome({ children }: Props) { )}
- {state.layout === PageLayoutType.Standard && state.sectionNav && !config.featureToggles.dockedMegaMenu && ( - - )} - {config.featureToggles.dockedMegaMenu && !state.chromeless && state.megaMenuDocked && state.megaMenuOpen && ( - chrome.setMegaMenuOpen(false)} /> + {!state.chromeless && state.megaMenuDocked && state.megaMenuOpen && ( + chrome.setMegaMenuOpen(false)} /> )}
{children}
- {!state.chromeless && !state.megaMenuDocked && ( - <> - {config.featureToggles.dockedMegaMenu ? ( - - ) : ( - chrome.setMegaMenuOpen(false)} /> - )} - - )} + {!state.chromeless && !state.megaMenuDocked && } {!state.chromeless && } {shouldShowReturnToPrevious && state.returnToPrevious && ( @@ -128,10 +115,6 @@ export function AppChrome({ children }: Props) { } const getStyles = (theme: GrafanaTheme2) => { - const shadow = theme.isDark - ? `0 0.6px 1.5px rgb(0 0 0), 0 2px 4px rgb(0 0 0 / 40%), 0 5px 10px rgb(0 0 0 / 23%)` - : '0 4px 8px rgb(0 0 0 / 4%)'; - return { content: css({ display: 'flex', @@ -162,7 +145,6 @@ const getStyles = (theme: GrafanaTheme2) => { zIndex: theme.zIndex.navbarFixed, left: 0, right: 0, - boxShadow: config.featureToggles.dockedMegaMenu ? undefined : shadow, background: theme.colors.background.primary, flexDirection: 'column', }), diff --git a/public/app/core/components/AppChrome/AppChromeMenu.tsx b/public/app/core/components/AppChrome/AppChromeMenu.tsx index 5efec4390df..7471a2416d2 100644 --- a/public/app/core/components/AppChrome/AppChromeMenu.tsx +++ b/public/app/core/components/AppChrome/AppChromeMenu.tsx @@ -10,7 +10,7 @@ import { useStyles2, useTheme2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { KioskMode } from 'app/types'; -import { MegaMenu, MENU_WIDTH } from './DockedMegaMenu/MegaMenu'; +import { MegaMenu, MENU_WIDTH } from './MegaMenu/MegaMenu'; import { TOGGLE_BUTTON_ID } from './NavToolbar/NavToolbar'; import { TOP_BAR_LEVEL_HEIGHT } from './types'; diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx index 3567e3efa8a..ad79bee4455 100644 --- a/public/app/core/components/AppChrome/AppChromeService.tsx +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -38,12 +38,8 @@ export class AppChromeService { private routeChangeHandled = true; private megaMenuDocked = Boolean( - config.featureToggles.dockedMegaMenu && - window.innerWidth >= config.theme2.breakpoints.values.xl && - store.getBool( - DOCKED_LOCAL_STORAGE_KEY, - Boolean(config.featureToggles.dockedMegaMenu && window.innerWidth >= config.theme2.breakpoints.values.xxl) - ) + window.innerWidth >= config.theme2.breakpoints.values.xl && + store.getBool(DOCKED_LOCAL_STORAGE_KEY, Boolean(window.innerWidth >= config.theme2.breakpoints.values.xxl)) ); private sessionStorageData = window.sessionStorage.getItem('returnToPrevious'); @@ -130,14 +126,10 @@ export class AppChromeService { public setMegaMenuOpen = (newOpenState: boolean) => { const { megaMenuDocked } = this.state.getValue(); - if (config.featureToggles.dockedMegaMenu) { - if (megaMenuDocked) { - store.set(DOCKED_MENU_OPEN_LOCAL_STORAGE_KEY, newOpenState); - } - reportInteraction('grafana_mega_menu_open', { state: newOpenState }); - } else { - reportInteraction('grafana_toggle_menu_clicked', { action: newOpenState ? 'open' : 'close' }); + if (megaMenuDocked) { + store.set(DOCKED_MENU_OPEN_LOCAL_STORAGE_KEY, newOpenState); } + reportInteraction('grafana_mega_menu_open', { state: newOpenState }); this.update({ megaMenuOpen: newOpenState, }); diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx deleted file mode 100644 index 43f9bc20180..00000000000 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import React from 'react'; -import { Router } from 'react-router-dom'; -import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; - -import { NavModelItem } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { locationService } from '@grafana/runtime'; - -import { TestProvider } from '../../../../../test/helpers/TestProvider'; - -import { MegaMenu } from './MegaMenu'; - -const setup = () => { - const navBarTree: NavModelItem[] = [ - { - text: 'Section name', - id: 'section', - url: 'section', - children: [ - { - text: 'Child1', - id: 'child1', - url: 'section/child1', - children: [{ text: 'Grandchild1', id: 'grandchild1', url: 'section/child1/grandchild1' }], - }, - { text: 'Child2', id: 'child2', url: 'section/child2' }, - ], - }, - { - text: 'Profile', - id: 'profile', - url: 'profile', - }, - ]; - - const grafanaContext = getGrafanaContextMock(); - grafanaContext.chrome.setMegaMenuOpen(true); - - return render( - - - {}} /> - - - ); -}; - -describe('MegaMenu', () => { - afterEach(() => { - window.localStorage.clear(); - }); - it('should render component', async () => { - setup(); - - expect(await screen.findByTestId(selectors.components.NavMenu.Menu)).toBeInTheDocument(); - expect(await screen.findByRole('link', { name: 'Section name' })).toBeInTheDocument(); - }); - - it('should render children', async () => { - setup(); - await userEvent.click(await screen.findByRole('button', { name: 'Expand section Section name' })); - expect(await screen.findByRole('link', { name: 'Child1' })).toBeInTheDocument(); - expect(await screen.findByRole('link', { name: 'Child2' })).toBeInTheDocument(); - }); - - it('should render grandchildren', async () => { - setup(); - await userEvent.click(await screen.findByRole('button', { name: 'Expand section Section name' })); - expect(await screen.findByRole('link', { name: 'Child1' })).toBeInTheDocument(); - await userEvent.click(await screen.findByRole('button', { name: 'Expand section Child1' })); - expect(await screen.findByRole('link', { name: 'Grandchild1' })).toBeInTheDocument(); - expect(await screen.findByRole('link', { name: 'Child2' })).toBeInTheDocument(); - }); - - it('should filter out profile', async () => { - setup(); - - expect(screen.queryByLabelText('Profile')).not.toBeInTheDocument(); - }); -}); diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx deleted file mode 100644 index a7df60a46f2..00000000000 --- a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { css } from '@emotion/css'; -import { DOMAttributes } from '@react-types/shared'; -import React, { forwardRef } from 'react'; -import { useLocation } from 'react-router-dom'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { CustomScrollbar, Icon, IconButton, useStyles2, Stack } from '@grafana/ui'; -import { useGrafana } from 'app/core/context/GrafanaContext'; -import { t } from 'app/core/internationalization'; -import { useSelector } from 'app/types'; - -import { MegaMenuItem } from './MegaMenuItem'; -import { enrichWithInteractionTracking, getActiveItem } from './utils'; - -export const MENU_WIDTH = '300px'; - -export interface Props extends DOMAttributes { - onClose: () => void; -} - -export const MegaMenu = React.memo( - forwardRef(({ onClose, ...restProps }, ref) => { - const navTree = useSelector((state) => state.navBarTree); - const styles = useStyles2(getStyles); - const location = useLocation(); - const { chrome } = useGrafana(); - const state = chrome.useState(); - - // Remove profile + help from tree - const navItems = navTree - .filter((item) => item.id !== 'profile' && item.id !== 'help') - .map((item) => enrichWithInteractionTracking(item, state.megaMenuDocked)); - - const activeItem = getActiveItem(navItems, location.pathname); - - const handleDockedMenu = () => { - chrome.setMegaMenuDocked(!state.megaMenuDocked); - if (state.megaMenuDocked) { - chrome.setMegaMenuOpen(false); - } - - // refocus on undock/menu open button when changing state - setTimeout(() => { - document.getElementById(state.megaMenuDocked ? 'mega-menu-toggle' : 'dock-menu-button')?.focus(); - }); - }; - - return ( -
-
- - -
- -
- ); - }) -); - -MegaMenu.displayName = 'MegaMenu'; - -const getStyles = (theme: GrafanaTheme2) => ({ - content: css({ - display: 'flex', - flexDirection: 'column', - height: '100%', - minHeight: 0, - position: 'relative', - }), - mobileHeader: css({ - display: 'flex', - justifyContent: 'space-between', - padding: theme.spacing(1, 1, 1, 2), - borderBottom: `1px solid ${theme.colors.border.weak}`, - - [theme.breakpoints.up('md')]: { - display: 'none', - }, - }), - itemList: css({ - boxSizing: 'border-box', - display: 'flex', - flexDirection: 'column', - listStyleType: 'none', - padding: theme.spacing(1), - [theme.breakpoints.up('md')]: { - width: MENU_WIDTH, - }, - }), - dockMenuButton: css({ - display: 'none', - - [theme.breakpoints.up('xl')]: { - display: 'inline-flex', - }, - }), -}); diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/utils.test.ts b/public/app/core/components/AppChrome/DockedMegaMenu/utils.test.ts deleted file mode 100644 index eacea3cfdf9..00000000000 --- a/public/app/core/components/AppChrome/DockedMegaMenu/utils.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { GrafanaConfig, locationUtil, NavModelItem } from '@grafana/data'; -import { ContextSrv, setContextSrv } from 'app/core/services/context_srv'; - -import { enrichHelpItem, getActiveItem, isMatchOrChildMatch } from './utils'; - -jest.mock('../../../app_events', () => ({ - publish: jest.fn(), -})); - -describe('enrichConfigItems', () => { - let mockHelpNode: NavModelItem; - - beforeEach(() => { - mockHelpNode = { - id: 'help', - text: 'Help', - }; - }); - - it('enhances the help node with extra child links', () => { - const contextSrv = new ContextSrv(); - setContextSrv(contextSrv); - const helpNode = enrichHelpItem(mockHelpNode); - expect(helpNode!.children).toContainEqual( - expect.objectContaining({ - text: 'Documentation', - }) - ); - expect(helpNode!.children).toContainEqual( - expect.objectContaining({ - text: 'Support', - }) - ); - expect(helpNode!.children).toContainEqual( - expect.objectContaining({ - text: 'Community', - }) - ); - expect(helpNode!.children).toContainEqual( - expect.objectContaining({ - text: 'Keyboard shortcuts', - }) - ); - }); -}); - -describe('isMatchOrChildMatch', () => { - const mockChild: NavModelItem = { - text: 'Child', - url: '/dashboards/child', - }; - const mockItemToCheck: NavModelItem = { - text: 'Dashboards', - url: '/dashboards', - children: [mockChild], - }; - - it('returns true if the itemToCheck is an exact match with the searchItem', () => { - const searchItem = mockItemToCheck; - expect(isMatchOrChildMatch(mockItemToCheck, searchItem)).toBe(true); - }); - - it('returns true if the itemToCheck has a child that matches the searchItem', () => { - const searchItem = mockChild; - expect(isMatchOrChildMatch(mockItemToCheck, searchItem)).toBe(true); - }); - - it('returns false otherwise', () => { - const searchItem: NavModelItem = { - text: 'No match', - url: '/noMatch', - }; - expect(isMatchOrChildMatch(mockItemToCheck, searchItem)).toBe(false); - }); -}); - -describe('getActiveItem', () => { - const mockNavTree: NavModelItem[] = [ - { - text: 'Item', - url: '/item', - }, - { - text: 'Item with query param', - url: '/itemWithQueryParam?foo=bar', - }, - { - text: 'Item after subpath', - url: '/subUrl/itemAfterSubpath', - }, - { - text: 'Item with children', - url: '/itemWithChildren', - children: [ - { - text: 'Child', - url: '/child', - }, - ], - }, - { - text: 'Alerting item', - url: '/alerting/list', - }, - { - text: 'Base', - url: '/', - }, - { - text: 'Starred', - url: '/dashboards?starred', - id: 'starred', - }, - { - text: 'Dashboards', - url: '/dashboards', - }, - { - text: 'More specific dashboard', - url: '/d/moreSpecificDashboard', - }, - ]; - beforeEach(() => { - locationUtil.initialize({ - config: { appSubUrl: '/subUrl' } as GrafanaConfig, - getVariablesUrlParams: () => ({}), - getTimeRangeForUrl: () => ({ from: 'now-7d', to: 'now' }), - }); - }); - - it('returns an exact match at the top level', () => { - const mockPathName = '/item'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Item', - url: '/item', - }); - }); - - it('returns an exact match ignoring root subpath', () => { - const mockPathName = '/itemAfterSubpath'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Item after subpath', - url: '/subUrl/itemAfterSubpath', - }); - }); - - it('returns an exact match ignoring query params', () => { - const mockPathName = '/itemWithQueryParam?bar=baz'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Item with query param', - url: '/itemWithQueryParam?foo=bar', - }); - }); - - it('returns an exact child match', () => { - const mockPathName = '/child'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Child', - url: '/child', - }); - }); - - it('returns the alerting link if the pathname is an alert notification', () => { - const mockPathName = '/alerting/notification/foo'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Alerting item', - url: '/alerting/list', - }); - }); - - it('returns the dashboards route link if the pathname starts with /d/', () => { - const mockPathName = '/d/foo'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Dashboards', - url: '/dashboards', - }); - }); - - it('returns a more specific link if one exists', () => { - const mockPathName = '/d/moreSpecificDashboard'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'More specific dashboard', - url: '/d/moreSpecificDashboard', - }); - }); -}); diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/utils.ts b/public/app/core/components/AppChrome/DockedMegaMenu/utils.ts deleted file mode 100644 index 9486c3ee5ce..00000000000 --- a/public/app/core/components/AppChrome/DockedMegaMenu/utils.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { locationUtil, NavModelItem } from '@grafana/data'; -import { config, reportInteraction } from '@grafana/runtime'; -import { t } from 'app/core/internationalization'; - -import { ShowModalReactEvent } from '../../../../types/events'; -import appEvents from '../../../app_events'; -import { getFooterLinks } from '../../Footer/Footer'; -import { HelpModal } from '../../help/HelpModal'; - -export const enrichHelpItem = (helpItem: NavModelItem) => { - let menuItems = helpItem.children || []; - - if (helpItem.id === 'help') { - const onOpenShortcuts = () => { - appEvents.publish(new ShowModalReactEvent({ component: HelpModal })); - }; - helpItem.children = [ - ...menuItems, - ...getFooterLinks(), - ...getEditionAndUpdateLinks(), - { - id: 'keyboard-shortcuts', - text: t('nav.help/keyboard-shortcuts', 'Keyboard shortcuts'), - icon: 'keyboard', - onClick: onOpenShortcuts, - }, - ]; - } - return helpItem; -}; - -export const enrichWithInteractionTracking = (item: NavModelItem, megaMenuDockedState: boolean) => { - // creating a new object here to not mutate the original item object - const newItem = { ...item }; - const onClick = newItem.onClick; - newItem.onClick = () => { - reportInteraction('grafana_navigation_item_clicked', { - path: newItem.url ?? newItem.id, - menuIsDocked: megaMenuDockedState, - }); - onClick?.(); - }; - if (newItem.children) { - newItem.children = newItem.children.map((item) => enrichWithInteractionTracking(item, megaMenuDockedState)); - } - return newItem; -}; - -export const isMatchOrChildMatch = (itemToCheck: NavModelItem, searchItem?: NavModelItem) => { - return Boolean(itemToCheck === searchItem || hasChildMatch(itemToCheck, searchItem)); -}; - -export const hasChildMatch = (itemToCheck: NavModelItem, searchItem?: NavModelItem): boolean => { - return Boolean( - itemToCheck.children?.some((child) => { - if (child === searchItem) { - return true; - } else { - return hasChildMatch(child, searchItem); - } - }) - ); -}; - -const stripQueryParams = (url?: string) => { - return url?.split('?')[0] ?? ''; -}; - -const isBetterMatch = (newMatch: NavModelItem, currentMatch?: NavModelItem) => { - const currentMatchUrl = stripQueryParams(currentMatch?.url); - const newMatchUrl = stripQueryParams(newMatch.url); - return newMatchUrl && newMatchUrl.length > currentMatchUrl?.length; -}; - -export const getActiveItem = ( - navTree: NavModelItem[], - pathname: string, - currentBestMatch?: NavModelItem -): NavModelItem | undefined => { - const dashboardLinkMatch = '/dashboards'; - - for (const link of navTree) { - const linkWithoutParams = stripQueryParams(link.url); - const linkPathname = locationUtil.stripBaseFromUrl(linkWithoutParams); - if (linkPathname && link.id !== 'starred') { - if (linkPathname === pathname) { - // exact match - currentBestMatch = link; - break; - } else if (linkPathname !== '/' && pathname.startsWith(linkPathname)) { - // partial match - if (isBetterMatch(link, currentBestMatch)) { - currentBestMatch = link; - } - } else if (linkPathname === '/alerting/list' && pathname.startsWith('/alerting/notification/')) { - // alert channel match - // TODO refactor routes such that we don't need this custom logic - currentBestMatch = link; - break; - } else if (linkPathname === dashboardLinkMatch && pathname.startsWith('/d/')) { - // dashboard match - // TODO refactor routes such that we don't need this custom logic - if (isBetterMatch(link, currentBestMatch)) { - currentBestMatch = link; - } - } - } - if (link.children) { - currentBestMatch = getActiveItem(link.children, pathname, currentBestMatch); - } - if (stripQueryParams(currentBestMatch?.url) === pathname) { - return currentBestMatch; - } - } - return currentBestMatch; -}; - -export function getEditionAndUpdateLinks(): NavModelItem[] { - const { buildInfo, licenseInfo } = config; - const stateInfo = licenseInfo.stateInfo ? ` (${licenseInfo.stateInfo})` : ''; - const links: NavModelItem[] = []; - - links.push({ - target: '_blank', - id: 'version', - text: `${buildInfo.edition}${stateInfo}`, - url: licenseInfo.licenseUrl, - icon: 'external-link-alt', - }); - - if (buildInfo.hasUpdate) { - links.push({ - target: '_blank', - id: 'updateVersion', - text: `New version available!`, - icon: 'download-alt', - url: 'https://grafana.com/grafana/download?utm_source=grafana_footer', - }); - } - - return links; -} diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/FeatureHighlight.tsx b/public/app/core/components/AppChrome/MegaMenu/FeatureHighlight.tsx similarity index 100% rename from public/app/core/components/AppChrome/DockedMegaMenu/FeatureHighlight.tsx rename to public/app/core/components/AppChrome/MegaMenu/FeatureHighlight.tsx diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.test.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.test.tsx index 3d220c4b80c..43f9bc20180 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.test.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { Router } from 'react-router-dom'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; @@ -18,7 +19,12 @@ const setup = () => { id: 'section', url: 'section', children: [ - { text: 'Child1', id: 'child1', url: 'section/child1' }, + { + text: 'Child1', + id: 'child1', + url: 'section/child1', + children: [{ text: 'Grandchild1', id: 'grandchild1', url: 'section/child1/grandchild1' }], + }, { text: 'Child2', id: 'child2', url: 'section/child2' }, ], }, @@ -42,6 +48,9 @@ const setup = () => { }; describe('MegaMenu', () => { + afterEach(() => { + window.localStorage.clear(); + }); it('should render component', async () => { setup(); @@ -49,10 +58,25 @@ describe('MegaMenu', () => { expect(await screen.findByRole('link', { name: 'Section name' })).toBeInTheDocument(); }); + it('should render children', async () => { + setup(); + await userEvent.click(await screen.findByRole('button', { name: 'Expand section Section name' })); + expect(await screen.findByRole('link', { name: 'Child1' })).toBeInTheDocument(); + expect(await screen.findByRole('link', { name: 'Child2' })).toBeInTheDocument(); + }); + + it('should render grandchildren', async () => { + setup(); + await userEvent.click(await screen.findByRole('button', { name: 'Expand section Section name' })); + expect(await screen.findByRole('link', { name: 'Child1' })).toBeInTheDocument(); + await userEvent.click(await screen.findByRole('button', { name: 'Expand section Child1' })); + expect(await screen.findByRole('link', { name: 'Grandchild1' })).toBeInTheDocument(); + expect(await screen.findByRole('link', { name: 'Child2' })).toBeInTheDocument(); + }); + it('should filter out profile', async () => { setup(); - expect(await screen.findByTestId(selectors.components.NavMenu.Menu)).toBeInTheDocument(); expect(screen.queryByLabelText('Profile')).not.toBeInTheDocument(); }); }); diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index 72b90257473..a7df60a46f2 100644 --- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx +++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx @@ -1,50 +1,132 @@ import { css } from '@emotion/css'; -import { cloneDeep } from 'lodash'; -import React from 'react'; +import { DOMAttributes } from '@react-types/shared'; +import React, { forwardRef } from 'react'; import { useLocation } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; -import { useTheme2 } from '@grafana/ui'; +import { selectors } from '@grafana/e2e-selectors'; +import { CustomScrollbar, Icon, IconButton, useStyles2, Stack } from '@grafana/ui'; +import { useGrafana } from 'app/core/context/GrafanaContext'; +import { t } from 'app/core/internationalization'; import { useSelector } from 'app/types'; -import { NavBarMenu } from './NavBarMenu'; +import { MegaMenuItem } from './MegaMenuItem'; import { enrichWithInteractionTracking, getActiveItem } from './utils'; -export interface Props { +export const MENU_WIDTH = '300px'; + +export interface Props extends DOMAttributes { onClose: () => void; - searchBarHidden?: boolean; } -export const MegaMenu = React.memo(({ onClose, searchBarHidden }) => { - const navBarTree = useSelector((state) => state.navBarTree); - const theme = useTheme2(); - const styles = getStyles(theme); - const location = useLocation(); +export const MegaMenu = React.memo( + forwardRef(({ onClose, ...restProps }, ref) => { + const navTree = useSelector((state) => state.navBarTree); + const styles = useStyles2(getStyles); + const location = useLocation(); + const { chrome } = useGrafana(); + const state = chrome.useState(); - const navTree = cloneDeep(navBarTree); + // Remove profile + help from tree + const navItems = navTree + .filter((item) => item.id !== 'profile' && item.id !== 'help') + .map((item) => enrichWithInteractionTracking(item, state.megaMenuDocked)); - // Remove profile + help from tree - const navItems = navTree - .filter((item) => item.id !== 'profile' && item.id !== 'help') - .map((item) => enrichWithInteractionTracking(item, true)); + const activeItem = getActiveItem(navItems, location.pathname); - const activeItem = getActiveItem(navItems, location.pathname); + const handleDockedMenu = () => { + chrome.setMegaMenuDocked(!state.megaMenuDocked); + if (state.megaMenuDocked) { + chrome.setMegaMenuOpen(false); + } - return ( -
- -
- ); -}); + // refocus on undock/menu open button when changing state + setTimeout(() => { + document.getElementById(state.megaMenuDocked ? 'mega-menu-toggle' : 'dock-menu-button')?.focus(); + }); + }; + + return ( +
+
+ + +
+ +
+ ); + }) +); MegaMenu.displayName = 'MegaMenu'; const getStyles = (theme: GrafanaTheme2) => ({ - menuWrapper: css({ - position: 'fixed', - display: 'grid', - gridAutoFlow: 'column', + content: css({ + display: 'flex', + flexDirection: 'column', height: '100%', - zIndex: theme.zIndex.sidemenu, + minHeight: 0, + position: 'relative', + }), + mobileHeader: css({ + display: 'flex', + justifyContent: 'space-between', + padding: theme.spacing(1, 1, 1, 2), + borderBottom: `1px solid ${theme.colors.border.weak}`, + + [theme.breakpoints.up('md')]: { + display: 'none', + }, + }), + itemList: css({ + boxSizing: 'border-box', + display: 'flex', + flexDirection: 'column', + listStyleType: 'none', + padding: theme.spacing(1), + [theme.breakpoints.up('md')]: { + width: MENU_WIDTH, + }, + }), + dockMenuButton: css({ + display: 'none', + + [theme.breakpoints.up('xl')]: { + display: 'inline-flex', + }, }), }); diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx similarity index 100% rename from public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItem.tsx rename to public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItemText.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx similarity index 100% rename from public/app/core/components/AppChrome/DockedMegaMenu/MegaMenuItemText.tsx rename to public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx diff --git a/public/app/core/components/AppChrome/MegaMenu/NavBarItemIcon.tsx b/public/app/core/components/AppChrome/MegaMenu/NavBarItemIcon.tsx deleted file mode 100644 index b259eb97660..00000000000 --- a/public/app/core/components/AppChrome/MegaMenu/NavBarItemIcon.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { css, cx } from '@emotion/css'; -import React from 'react'; - -import { GrafanaTheme2, NavModelItem } from '@grafana/data'; -import { Icon, toIconName, useTheme2 } from '@grafana/ui'; - -import { Branding } from '../../Branding/Branding'; - -interface NavBarItemIconProps { - link: NavModelItem; -} - -export function NavBarItemIcon({ link }: NavBarItemIconProps) { - const theme = useTheme2(); - const styles = getStyles(theme); - - if (link.icon === 'grafana') { - return ; - } else if (link.icon) { - const iconName = toIconName(link.icon); - return ; - } else { - // consumer of NavBarItemIcon gives enclosing element an appropriate label - return ; - } -} - -function getStyles(theme: GrafanaTheme2) { - return { - img: css({ - height: theme.spacing(3), - width: theme.spacing(3), - }), - round: css({ - borderRadius: theme.shape.radius.circle, - }), - }; -} diff --git a/public/app/core/components/AppChrome/MegaMenu/NavBarMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/NavBarMenu.tsx deleted file mode 100644 index 6fc40275cef..00000000000 --- a/public/app/core/components/AppChrome/MegaMenu/NavBarMenu.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { css } from '@emotion/css'; -import { useDialog } from '@react-aria/dialog'; -import { FocusScope } from '@react-aria/focus'; -import { OverlayContainer, useOverlay } from '@react-aria/overlays'; -import React, { useEffect, useRef, useState } from 'react'; -import CSSTransition from 'react-transition-group/CSSTransition'; - -import { GrafanaTheme2, NavModelItem } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { CustomScrollbar, Icon, IconButton, useTheme2 } from '@grafana/ui'; -import { useGrafana } from 'app/core/context/GrafanaContext'; - -import { TOP_BAR_LEVEL_HEIGHT } from '../types'; - -import { NavBarMenuItemWrapper } from './NavBarMenuItemWrapper'; - -const MENU_WIDTH = '350px'; - -export interface Props { - activeItem?: NavModelItem; - navItems: NavModelItem[]; - searchBarHidden?: boolean; - onClose: () => void; -} - -export function NavBarMenu({ activeItem, navItems, searchBarHidden, onClose }: Props) { - const theme = useTheme2(); - const styles = getStyles(theme, searchBarHidden); - const animationSpeed = theme.transitions.duration.shortest; - const animStyles = getAnimStyles(theme, animationSpeed); - const { chrome } = useGrafana(); - const state = chrome.useState(); - const ref = useRef(null); - const backdropRef = useRef(null); - const { dialogProps } = useDialog({}, ref); - const [isOpen, setIsOpen] = useState(false); - - const onMenuClose = () => setIsOpen(false); - - const { overlayProps, underlayProps } = useOverlay( - { - isDismissable: true, - isOpen: true, - onClose: onMenuClose, - }, - ref - ); - - useEffect(() => { - if (state.megaMenuOpen) { - setIsOpen(true); - } - }, [state.megaMenuOpen]); - - return ( - - - -
-
- - -
- -
-
-
- -
- - - ); -} - -NavBarMenu.displayName = 'NavBarMenu'; - -const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => { - const topPosition = (searchBarHidden ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2) + 1; - - return { - backdrop: css({ - backdropFilter: 'blur(1px)', - backgroundColor: theme.components.overlay.background, - bottom: 0, - left: 0, - position: 'fixed', - right: 0, - top: searchBarHidden ? 0 : TOP_BAR_LEVEL_HEIGHT, - zIndex: theme.zIndex.modalBackdrop, - - [theme.breakpoints.up('md')]: { - top: topPosition, - }, - }), - container: css({ - display: 'flex', - bottom: 0, - flexDirection: 'column', - left: 0, - marginRight: theme.spacing(1.5), - right: 0, - // Needs to below navbar should we change the navbarFixed? add add a new level? - zIndex: theme.zIndex.modal, - position: 'fixed', - top: searchBarHidden ? 0 : TOP_BAR_LEVEL_HEIGHT, - backgroundColor: theme.colors.background.primary, - boxSizing: 'content-box', - flex: '1 1 0', - - [theme.breakpoints.up('md')]: { - borderRight: `1px solid ${theme.colors.border.weak}`, - right: 'unset', - top: topPosition, - }, - }), - content: css({ - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - minHeight: 0, - }), - mobileHeader: css({ - display: 'flex', - justifyContent: 'space-between', - padding: theme.spacing(1, 1, 1, 2), - borderBottom: `1px solid ${theme.colors.border.weak}`, - - [theme.breakpoints.up('md')]: { - display: 'none', - }, - }), - itemList: css({ - display: 'grid', - gridAutoRows: `minmax(${theme.spacing(6)}, auto)`, - gridTemplateColumns: `minmax(${MENU_WIDTH}, auto)`, - minWidth: MENU_WIDTH, - }), - }; -}; - -const getAnimStyles = (theme: GrafanaTheme2, animationDuration: number) => { - const commonTransition = { - transitionDuration: `${animationDuration}ms`, - transitionTimingFunction: theme.transitions.easing.easeInOut, - [theme.breakpoints.down('md')]: { - overflow: 'hidden', - }, - }; - - const overlayTransition = { - ...commonTransition, - transitionProperty: 'box-shadow, width', - // this is needed to prevent a horizontal scrollbar during the animation on firefox - '.scrollbar-view': { - overflow: 'hidden !important', - }, - }; - - const backdropTransition = { - ...commonTransition, - transitionProperty: 'opacity', - }; - - const overlayOpen = { - width: '100%', - [theme.breakpoints.up('md')]: { - boxShadow: theme.shadows.z3, - width: MENU_WIDTH, - }, - }; - - const overlayClosed = { - boxShadow: 'none', - width: 0, - }; - - const backdropOpen = { - opacity: 1, - }; - - const backdropClosed = { - opacity: 0, - }; - - return { - backdrop: { - enter: css(backdropClosed), - enterActive: css(backdropTransition, backdropOpen), - enterDone: css(backdropOpen), - }, - overlay: { - enter: css(overlayClosed), - enterActive: css(overlayTransition, overlayOpen), - enterDone: css(overlayOpen), - }, - }; -}; diff --git a/public/app/core/components/AppChrome/MegaMenu/NavBarMenuItem.tsx b/public/app/core/components/AppChrome/MegaMenu/NavBarMenuItem.tsx deleted file mode 100644 index 4829d36701a..00000000000 --- a/public/app/core/components/AppChrome/MegaMenu/NavBarMenuItem.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { css, cx } from '@emotion/css'; -import React from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { Icon, IconName, Link, useTheme2 } from '@grafana/ui'; - -export interface Props { - children: React.ReactNode; - icon?: IconName; - isActive?: boolean; - isChild?: boolean; - onClick?: () => void; - target?: HTMLAnchorElement['target']; - url?: string; -} - -export function NavBarMenuItem({ children, icon, isActive, isChild, onClick, target, url }: Props) { - const theme = useTheme2(); - const styles = getStyles(theme, isActive, isChild); - - const linkContent = ( -
- {icon && } - -
{children}
- - {target === '_blank' && ( - - )} -
- ); - - let element = ( - - ); - - if (url) { - element = - !target && url.startsWith('/') ? ( - - {linkContent} - - ) : ( - - {linkContent} - - ); - } - - return
  • {element}
  • ; -} - -NavBarMenuItem.displayName = 'NavBarMenuItem'; - -const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive'], isChild: Props['isActive']) => ({ - button: css({ - backgroundColor: 'unset', - borderStyle: 'unset', - }), - linkContent: css({ - alignItems: 'center', - display: 'flex', - gap: '0.5rem', - height: '100%', - width: '100%', - }), - linkText: css({ - textOverflow: 'ellipsis', - overflow: 'hidden', - whiteSpace: 'nowrap', - }), - externalLinkIcon: css({ - color: theme.colors.text.secondary, - }), - element: css({ - alignItems: 'center', - boxSizing: 'border-box', - position: 'relative', - color: isActive ? theme.colors.text.primary : theme.colors.text.secondary, - padding: theme.spacing(1, 1, 1, isChild ? 5 : 0), - ...(isChild && { - borderRadius: theme.shape.radius.default, - }), - width: '100%', - '&:hover, &:focus-visible': { - ...(isChild && { - background: theme.colors.emphasize(theme.colors.background.primary, 0.03), - }), - textDecoration: 'underline', - color: theme.colors.text.primary, - }, - '&:focus-visible': { - boxShadow: 'none', - outline: `2px solid ${theme.colors.primary.main}`, - outlineOffset: '-2px', - transition: 'none', - }, - '&::before': { - display: isActive ? 'block' : 'none', - content: '" "', - height: theme.spacing(3), - position: 'absolute', - left: theme.spacing(1), - top: '50%', - transform: 'translateY(-50%)', - width: theme.spacing(0.5), - borderRadius: theme.shape.radius.default, - backgroundImage: theme.colors.gradients.brandVertical, - }, - }), - listItem: css({ - boxSizing: 'border-box', - position: 'relative', - display: 'flex', - width: '100%', - ...(isChild && { - padding: theme.spacing(0, 2), - }), - }), -}); diff --git a/public/app/core/components/AppChrome/MegaMenu/NavBarMenuItemWrapper.tsx b/public/app/core/components/AppChrome/MegaMenu/NavBarMenuItemWrapper.tsx deleted file mode 100644 index d64073c6f6c..00000000000 --- a/public/app/core/components/AppChrome/MegaMenu/NavBarMenuItemWrapper.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { css } from '@emotion/css'; -import React from 'react'; - -import { GrafanaTheme2, NavModelItem } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; - -import { NavBarMenuItem } from './NavBarMenuItem'; -import { NavBarMenuSection } from './NavBarMenuSection'; -import { isMatchOrChildMatch } from './utils'; - -export function NavBarMenuItemWrapper({ - link, - activeItem, - onClose, -}: { - link: NavModelItem; - activeItem?: NavModelItem; - onClose: () => void; -}) { - const styles = useStyles2(getStyles); - - if (link.emptyMessage && !linkHasChildren(link)) { - return ( - -
      -
      {link.emptyMessage}
      -
    -
    - ); - } - - return ( - - {linkHasChildren(link) && ( -
      - {link.children.map((childLink) => { - return ( - !childLink.isCreateAction && ( - { - childLink.onClick?.(); - onClose(); - }} - target={childLink.target} - url={childLink.url} - > - {childLink.text} - - ) - ); - })} -
    - )} -
    - ); -} - -const getStyles = (theme: GrafanaTheme2) => ({ - children: css({ - display: 'flex', - flexDirection: 'column', - }), - flex: css({ - display: 'flex', - }), - itemWithoutMenu: css({ - position: 'relative', - placeItems: 'inherit', - justifyContent: 'start', - display: 'flex', - flexGrow: 1, - alignItems: 'center', - }), - fullWidth: css({ - height: '100%', - width: '100%', - }), - iconContainer: css({ - display: 'flex', - placeContent: 'center', - }), - itemWithoutMenuContent: css({ - display: 'grid', - gridAutoFlow: 'column', - gridTemplateColumns: `${theme.spacing(7)} auto`, - alignItems: 'center', - height: '100%', - }), - linkText: css({ - fontSize: theme.typography.pxToRem(14), - justifySelf: 'start', - }), - emptyMessage: css({ - color: theme.colors.text.secondary, - fontStyle: 'italic', - padding: theme.spacing(1, 1.5, 1, 7), - }), -}); - -function linkHasChildren(link: NavModelItem): link is NavModelItem & { children: NavModelItem[] } { - return Boolean(link.children && link.children.length > 0); -} diff --git a/public/app/core/components/AppChrome/MegaMenu/NavBarMenuSection.tsx b/public/app/core/components/AppChrome/MegaMenu/NavBarMenuSection.tsx deleted file mode 100644 index 978181f6b08..00000000000 --- a/public/app/core/components/AppChrome/MegaMenu/NavBarMenuSection.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { css, cx } from '@emotion/css'; -import React from 'react'; -import { useLocalStorage } from 'react-use'; - -import { GrafanaTheme2, NavModelItem } from '@grafana/data'; -import { Button, Icon, useStyles2 } from '@grafana/ui'; - -import { NavBarItemIcon } from './NavBarItemIcon'; -import { NavBarMenuItem } from './NavBarMenuItem'; -import { NavFeatureHighlight } from './NavFeatureHighlight'; -import { hasChildMatch } from './utils'; - -export function NavBarMenuSection({ - link, - activeItem, - children, - className, - onClose, -}: { - link: NavModelItem; - activeItem?: NavModelItem; - children: React.ReactNode; - className?: string; - onClose?: () => void; -}) { - const styles = useStyles2(getStyles); - const FeatureHighlightWrapper = link.highlightText ? NavFeatureHighlight : React.Fragment; - const isActive = link === activeItem; - const hasActiveChild = hasChildMatch(link, activeItem); - const [sectionExpanded, setSectionExpanded] = - useLocalStorage(`grafana.navigation.expanded[${link.text}]`, false) ?? Boolean(hasActiveChild); - - return ( - <> -
    - { - link.onClick?.(); - onClose?.(); - }} - target={link.target} - url={link.url} - > -
    - - - - {link.text} -
    -
    - {children && ( - - )} -
    - {sectionExpanded && children} - - ); -} - -const getStyles = (theme: GrafanaTheme2) => ({ - collapsibleSectionWrapper: css({ - alignItems: 'center', - display: 'flex', - }), - collapseButton: css({ - color: theme.colors.text.disabled, - padding: theme.spacing(0, 0.5), - marginRight: theme.spacing(1), - }), - collapseWrapperActive: css({ - backgroundColor: theme.colors.action.disabledBackground, - }), - collapseContent: css({ - padding: 0, - }), - labelWrapper: css({ - display: 'grid', - fontSize: theme.typography.pxToRem(14), - gridAutoFlow: 'column', - gridTemplateColumns: `${theme.spacing(7)} auto`, - placeItems: 'center', - fontWeight: theme.typography.fontWeightMedium, - }), - isActive: css({ - color: theme.colors.text.primary, - - '&::before': { - display: 'block', - content: '" "', - height: theme.spacing(3), - position: 'absolute', - left: theme.spacing(1), - top: '50%', - transform: 'translateY(-50%)', - width: theme.spacing(0.5), - borderRadius: theme.shape.radius.default, - backgroundImage: theme.colors.gradients.brandVertical, - }, - }), - hasActiveChild: css({ - color: theme.colors.text.primary, - }), -}); diff --git a/public/app/core/components/AppChrome/MegaMenu/NavFeatureHighlight.tsx b/public/app/core/components/AppChrome/MegaMenu/NavFeatureHighlight.tsx deleted file mode 100644 index 00f7eda9013..00000000000 --- a/public/app/core/components/AppChrome/MegaMenu/NavFeatureHighlight.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { css } from '@emotion/css'; -import React from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2 } from '@grafana/ui'; - -export interface Props { - children: JSX.Element; -} - -export const NavFeatureHighlight = ({ children }: Props): JSX.Element => { - const styles = useStyles2(getStyles); - return ( -
    - {children} - -
    - ); -}; - -const getStyles = (theme: GrafanaTheme2) => { - return { - highlight: css({ - backgroundColor: theme.colors.success.main, - borderRadius: theme.shape.radius.circle, - width: '6px', - height: '6px', - display: 'inline-block', - position: 'absolute', - top: '50%', - transform: 'translateY(-50%)', - }), - }; -}; diff --git a/public/app/core/components/AppChrome/MegaMenu/utils.ts b/public/app/core/components/AppChrome/MegaMenu/utils.ts index 2f180c5e674..9486c3ee5ce 100644 --- a/public/app/core/components/AppChrome/MegaMenu/utils.ts +++ b/public/app/core/components/AppChrome/MegaMenu/utils.ts @@ -29,19 +29,21 @@ export const enrichHelpItem = (helpItem: NavModelItem) => { return helpItem; }; -export const enrichWithInteractionTracking = (item: NavModelItem, expandedState: boolean) => { - const onClick = item.onClick; - item.onClick = () => { +export const enrichWithInteractionTracking = (item: NavModelItem, megaMenuDockedState: boolean) => { + // creating a new object here to not mutate the original item object + const newItem = { ...item }; + const onClick = newItem.onClick; + newItem.onClick = () => { reportInteraction('grafana_navigation_item_clicked', { - path: item.url ?? item.id, - state: expandedState ? 'expanded' : 'collapsed', + path: newItem.url ?? newItem.id, + menuIsDocked: megaMenuDockedState, }); onClick?.(); }; - if (item.children) { - item.children = item.children.map((item) => enrichWithInteractionTracking(item, expandedState)); + if (newItem.children) { + newItem.children = newItem.children.map((item) => enrichWithInteractionTracking(item, megaMenuDockedState)); } - return item; + return newItem; }; export const isMatchOrChildMatch = (itemToCheck: NavModelItem, searchItem?: NavModelItem) => { diff --git a/public/app/core/components/AppChrome/SectionNav/SectionNav.tsx b/public/app/core/components/AppChrome/SectionNav/SectionNav.tsx deleted file mode 100644 index 1b46e1a86c4..00000000000 --- a/public/app/core/components/AppChrome/SectionNav/SectionNav.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { css, cx } from '@emotion/css'; -import React, { useEffect, useState } from 'react'; -import { useLocalStorage } from 'react-use'; - -import { NavModel, GrafanaTheme2 } from '@grafana/data'; -import { useStyles2, CustomScrollbar, useTheme2 } from '@grafana/ui'; - -import { SectionNavItem } from './SectionNavItem'; -import { SectionNavToggle } from './SectionNavToggle'; - -export interface Props { - model: NavModel; -} - -export function SectionNav({ model }: Props) { - const styles = useStyles2(getStyles); - const { isExpanded, onToggleSectionNav } = useSectionNavState(); - - if (!Boolean(model.main?.children?.length)) { - return null; - } - - return ( -
    - - -
    - ); -} - -function useSectionNavState() { - const theme = useTheme2(); - - const isSmallScreen = window.matchMedia(`(max-width: ${theme.breakpoints.values.lg}px)`).matches; - const [navExpandedPreference, setNavExpandedPreference] = useLocalStorage( - 'grafana.sectionNav.expanded', - !isSmallScreen - ); - const [isExpanded, setIsExpanded] = useState(!isSmallScreen && navExpandedPreference); - - useEffect(() => { - const mediaQuery = window.matchMedia(`(max-width: ${theme.breakpoints.values.lg}px)`); - const onMediaQueryChange = (e: MediaQueryListEvent) => setIsExpanded(e.matches ? false : navExpandedPreference); - mediaQuery.addEventListener('change', onMediaQueryChange); - return () => mediaQuery.removeEventListener('change', onMediaQueryChange); - }, [navExpandedPreference, theme.breakpoints.values.lg]); - - const onToggleSectionNav = () => { - setNavExpandedPreference(!isExpanded); - setIsExpanded(!isExpanded); - }; - - return { isExpanded, onToggleSectionNav }; -} - -const getStyles = (theme: GrafanaTheme2) => { - return { - navContainer: css({ - display: 'flex', - flexDirection: 'column', - position: 'relative', - - [theme.breakpoints.up('md')]: { - flexDirection: 'row', - }, - }), - nav: css({ - display: 'flex', - flexDirection: 'column', - background: theme.colors.background.canvas, - flexShrink: 0, - transition: theme.transitions.create(['width', 'max-height']), - maxHeight: 0, - visibility: 'hidden', - [theme.breakpoints.up('md')]: { - width: 0, - maxHeight: 'unset', - }, - }), - navExpanded: css({ - maxHeight: '50vh', - visibility: 'visible', - [theme.breakpoints.up('md')]: { - width: '250px', - maxHeight: 'unset', - }, - }), - items: css({ - display: 'flex', - flexDirection: 'column', - padding: theme.spacing(2, 1, 2, 2), - minWidth: '250px', - - [theme.breakpoints.up('md')]: { - padding: theme.spacing(4.5, 1, 2, 2), - }, - }), - }; -}; diff --git a/public/app/core/components/AppChrome/SectionNav/SectionNavItem.test.tsx b/public/app/core/components/AppChrome/SectionNav/SectionNavItem.test.tsx deleted file mode 100644 index 90d92e75462..00000000000 --- a/public/app/core/components/AppChrome/SectionNav/SectionNavItem.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import React from 'react'; - -import { NavModelItem } from '@grafana/data'; - -import { SectionNavItem } from './SectionNavItem'; - -describe('SectionNavItem', () => { - it('should only show the img for a section root if both img and icon are present', () => { - const item: NavModelItem = { - text: 'Test', - icon: 'k6', - img: 'img', - children: [ - { - text: 'Child', - }, - ], - }; - - render(); - expect(screen.getByTestId('section-image')).toBeInTheDocument(); - expect(screen.queryByTestId('section-icon')).not.toBeInTheDocument(); - }); -}); diff --git a/public/app/core/components/AppChrome/SectionNav/SectionNavItem.tsx b/public/app/core/components/AppChrome/SectionNav/SectionNavItem.tsx deleted file mode 100644 index b21f8976aa9..00000000000 --- a/public/app/core/components/AppChrome/SectionNav/SectionNavItem.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { css, cx } from '@emotion/css'; -import React from 'react'; - -import { GrafanaTheme2, NavModelItem } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { reportInteraction } from '@grafana/runtime'; -import { useStyles2, Icon } from '@grafana/ui'; - -import { getActiveItem, hasChildMatch } from '../MegaMenu/utils'; - -export interface Props { - item: NavModelItem; - isSectionRoot?: boolean; - level?: number; -} - -// max level depth to render -const MAX_DEPTH = 2; - -export function SectionNavItem({ item, isSectionRoot = false, level = 0 }: Props) { - const styles = useStyles2(getStyles); - - const children = item.children?.filter((x) => !x.hideFromTabs); - const activeItem = item.children && getActiveItem(item.children, location.pathname); - - // If first root child is a section skip the bottom margin (as sections have top margin already) - const noRootMargin = isSectionRoot && Boolean(item.children![0].children?.length); - - const linkClass = cx({ - [styles.link]: true, - [styles.activeStyle]: item.active || (level === MAX_DEPTH && hasChildMatch(item, activeItem)), - [styles.isSection]: level < MAX_DEPTH && (Boolean(children?.length) || item.isSection), - [styles.isSectionRoot]: isSectionRoot, - [styles.noRootMargin]: noRootMargin, - }); - - let icon: React.ReactNode | null = null; - - if (item.img) { - icon = ; - } else if (item.icon) { - icon = ; - } - - const onItemClicked = () => { - reportInteraction('grafana_navigation_item_clicked', { - path: item.url ?? item.id, - sectionNav: true, - }); - }; - - return ( - <> - - {isSectionRoot && icon} - {item.text} - {item.tabSuffix && } - - {level < MAX_DEPTH && - children?.map((child, index) => )} - - ); -} - -const getStyles = (theme: GrafanaTheme2) => { - return { - link: css` - padding: ${theme.spacing(1, 0, 1, 1.5)}; - display: flex; - align-items: flex-start; - border-radius: ${theme.shape.radius.default}; - gap: ${theme.spacing(1)}; - height: 100%; - position: relative; - color: ${theme.colors.text.secondary}; - - &:hover, - &:focus { - text-decoration: underline; - z-index: 1; - } - `, - activeStyle: css` - label: activeTabStyle; - color: ${theme.colors.text.primary}; - font-weight: ${theme.typography.fontWeightMedium}; - background: ${theme.colors.emphasize(theme.colors.background.canvas, 0.03)}; - - &::before { - display: block; - content: ' '; - position: absolute; - left: 0; - width: 4px; - bottom: 2px; - top: 2px; - border-radius: ${theme.shape.radius.default}; - background-image: ${theme.colors.gradients.brandVertical}; - } - `, - suffix: css` - margin-left: ${theme.spacing(1)}; - `, - sectionImg: css({ - margin: '6px 0', - width: theme.spacing(2), - }), - isSectionRoot: css({ - fontSize: theme.typography.h4.fontSize, - marginTop: 0, - marginBottom: theme.spacing(2), - fontWeight: theme.typography.fontWeightMedium, - }), - isSection: css({ - color: theme.colors.text.primary, - fontSize: theme.typography.h5.fontSize, - marginTop: theme.spacing(2), - fontWeight: theme.typography.fontWeightMedium, - }), - noRootMargin: css({ - marginBottom: 0, - }), - }; -}; diff --git a/public/app/core/components/AppChrome/SectionNav/SectionNavToggle.tsx b/public/app/core/components/AppChrome/SectionNav/SectionNavToggle.tsx deleted file mode 100644 index d500d5620fe..00000000000 --- a/public/app/core/components/AppChrome/SectionNav/SectionNavToggle.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { css } from '@emotion/css'; -import classnames from 'classnames'; -import React from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Button, useTheme2 } from '@grafana/ui'; - -export interface Props { - isExpanded?: boolean; - onClick: () => void; -} - -export const SectionNavToggle = ({ isExpanded, onClick }: Props) => { - const theme = useTheme2(); - const styles = getStyles(theme); - - return ( -