From 1e85a6f4fdd204968455827661bc6f5641e5a675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 6 Jul 2022 17:00:56 +0200 Subject: [PATCH] TopNav: New page layouts (#51510) * First stab at new page layouts behind feature toggle * Simplifying PageHeader * Progress on a new model that can more easily support new and old page layouts * Progress * rename folder * Progress * Minor change * fixes * Fixing tests * Make breadcrumbs work * Add tests for old Page component * Adding tests for new Page component and behavior * fixing page header test * Fixed test * AppChrome outside route * Renaming folder * Minor fix * Updated * Fixing StoragePage * Fix for banners Co-authored-by: Ashley Harrison --- .betterer.results | 9 ++ packages/grafana-data/src/types/navModel.ts | 4 - .../src/components/Tabs/VerticalTab.tsx | 96 +++++++++++++++++++ packages/grafana-ui/src/components/index.ts | 1 + public/app/AppWrapper.tsx | 5 +- public/app/angular/angular_wrappers.ts | 2 +- public/app/angular/services/nav_model_srv.ts | 1 - .../AppChrome.tsx} | 49 ++++------ .../components/AppChrome/AppChromeService.tsx | 51 ++++++++++ .../components/AppChrome/AppChromeUpdate.tsx | 22 +++++ .../{TopNav => AppChrome}/Breadcrumbs.tsx | 14 ++- .../{TopNav => AppChrome}/NavToolbar.tsx | 10 +- .../{TopNav => AppChrome}/TopSearchBar.tsx | 0 public/app/core/components/AppChrome/types.ts | 8 ++ .../core/components/ErrorPage/ErrorPage.tsx | 2 +- .../core/components/MegaMenu/NavBarMenu.tsx | 2 +- public/app/core/components/Page/Page.test.tsx | 67 +++++++++++++ public/app/core/components/Page/Page.tsx | 48 ++++------ public/app/core/components/Page/types.ts | 19 ++++ public/app/core/components/Page/usePageNav.ts | 29 ++++++ .../app/core/components/Page/usePageTitle.ts | 32 +++++++ .../components/PageHeader/PageHeader.test.tsx | 34 +++---- .../core/components/PageHeader/PageHeader.tsx | 14 +-- .../app/core/components/PageNew/Page.test.tsx | 79 +++++++++++++++ public/app/core/components/PageNew/Page.tsx | 93 ++++++++++++++++++ .../core/components/PageNew/PageContents.tsx | 14 +++ .../core/components/PageNew/PageHeader.tsx | 43 +++++++++ .../app/core/components/PageNew/PageTabs.tsx | 42 ++++++++ .../core/components/PageNew/SectionNav.tsx | 92 ++++++++++++++++++ .../core/components/TopNav/TopNavUpdate.tsx | 23 ----- public/app/core/components/TopNav/types.ts | 1 - public/app/core/navigation/GrafanaRoute.tsx | 14 +-- public/app/core/navigation/types.ts | 3 +- public/app/core/reducers/navModel.ts | 1 - public/app/core/utils/isShallowEqual.ts | 29 ++++++ .../app/features/admin/AdminEditOrgPage.tsx | 2 +- .../app/features/admin/AdminListOrgsPage.tsx | 2 +- public/app/features/admin/AdminSettings.tsx | 2 +- public/app/features/admin/UpgradePage.tsx | 2 +- public/app/features/admin/UserAdminPage.tsx | 2 +- public/app/features/admin/UserCreatePage.tsx | 2 +- .../app/features/admin/UserListAdminPage.tsx | 2 +- public/app/features/admin/ldap/LdapPage.tsx | 2 +- .../app/features/alerting/AlertRuleList.tsx | 2 +- .../alerting/EditNotificationChannelPage.tsx | 2 +- .../features/alerting/FeatureTogglePage.tsx | 2 +- .../alerting/NewNotificationChannelPage.tsx | 2 +- .../alerting/NotificationsListPage.tsx | 2 +- .../features/alerting/unified/RuleEditor.tsx | 2 +- .../components/AlertingPageWrapper.tsx | 2 +- public/app/features/api-keys/ApiKeysPage.tsx | 2 +- .../dashboard/components/DashNav/DashNav.tsx | 6 +- .../dashboard/containers/DashboardPage.tsx | 5 +- .../datasources/DataSourceDashboards.tsx | 2 +- .../datasources/DataSourcesListPage.tsx | 2 +- .../datasources/NewDataSourcePage.tsx | 2 +- .../settings/DataSourceSettingsPage.tsx | 2 +- .../features/explore/FeatureTogglePage.tsx | 2 +- .../AccessControlFolderPermissions.tsx | 2 +- .../app/features/folders/FolderAlerting.tsx | 2 +- .../folders/FolderLibraryPanelsPage.tsx | 3 +- .../features/folders/FolderPermissions.tsx | 2 +- .../features/folders/FolderSettingsPage.tsx | 2 +- .../FolderSettingsPage.test.tsx.snap | 8 +- .../components/NewDashboardsFolder.tsx | 2 +- public/app/features/invites/SignupInvited.tsx | 2 +- .../library-panels/LibraryPanelsPage.tsx | 3 +- .../features/live/pages/CloudAdminPage.tsx | 2 +- .../features/live/pages/FeatureTogglePage.tsx | 2 +- .../features/live/pages/LiveStatusPage.tsx | 2 +- .../features/live/pages/PipelineAdminPage.tsx | 2 +- .../manage-dashboards/DashboardImportPage.tsx | 2 +- .../manage-dashboards/SnapshotListPage.tsx | 2 +- .../notifications/NotificationsPage.tsx | 3 +- public/app/features/org/NewOrgPage.tsx | 2 +- public/app/features/org/OrgDetailsPage.tsx | 2 +- public/app/features/org/SelectOrgPage.tsx | 2 +- public/app/features/org/UserInvitePage.tsx | 2 +- .../features/playlist/PlaylistEditPage.tsx | 2 +- .../app/features/playlist/PlaylistNewPage.tsx | 2 +- public/app/features/playlist/PlaylistPage.tsx | 2 +- public/app/features/plugins/admin/routes.ts | 6 ++ .../plugins/components/AppPluginLoader.tsx | 6 +- .../plugins/components/AppRootPage.tsx | 2 +- .../features/profile/ChangePasswordPage.tsx | 2 +- .../features/profile/FeatureTogglePage.tsx | 2 +- .../features/profile/UserProfileEditPage.tsx | 2 +- public/app/features/sandbox/TestStuffPage.tsx | 2 +- .../search/components/DashboardListPage.tsx | 2 +- .../ServiceAccountCreatePage.test.tsx | 6 +- .../ServiceAccountCreatePage.tsx | 20 +--- .../ServiceAccountPage.test.tsx | 8 -- .../serviceaccounts/ServiceAccountPage.tsx | 10 +- .../ServiceAccountsListPage.test.tsx | 8 -- .../ServiceAccountsListPage.tsx | 7 +- public/app/features/storage/StoragePage.tsx | 2 +- public/app/features/teams/CreateTeam.test.tsx | 23 ++--- public/app/features/teams/CreateTeam.tsx | 24 +---- public/app/features/teams/TeamList.test.tsx | 9 -- public/app/features/teams/TeamList.tsx | 10 +- public/app/features/teams/TeamPages.test.tsx | 4 +- public/app/features/teams/TeamPages.tsx | 22 ++--- public/app/features/teams/TeamPermissions.tsx | 2 +- .../app/features/users/UsersListPage.test.tsx | 9 -- public/app/features/users/UsersListPage.tsx | 8 +- public/app/routes/routes.tsx | 17 ++-- 106 files changed, 927 insertions(+), 347 deletions(-) create mode 100644 packages/grafana-ui/src/components/Tabs/VerticalTab.tsx rename public/app/core/components/{TopNav/TopNavPage.tsx => AppChrome/AppChrome.tsx} (60%) create mode 100644 public/app/core/components/AppChrome/AppChromeService.tsx create mode 100644 public/app/core/components/AppChrome/AppChromeUpdate.tsx rename public/app/core/components/{TopNav => AppChrome}/Breadcrumbs.tsx (86%) rename public/app/core/components/{TopNav => AppChrome}/NavToolbar.tsx (89%) rename public/app/core/components/{TopNav => AppChrome}/TopSearchBar.tsx (100%) create mode 100644 public/app/core/components/AppChrome/types.ts create mode 100644 public/app/core/components/Page/Page.test.tsx create mode 100644 public/app/core/components/Page/types.ts create mode 100644 public/app/core/components/Page/usePageNav.ts create mode 100644 public/app/core/components/Page/usePageTitle.ts create mode 100644 public/app/core/components/PageNew/Page.test.tsx create mode 100644 public/app/core/components/PageNew/Page.tsx create mode 100644 public/app/core/components/PageNew/PageContents.tsx create mode 100644 public/app/core/components/PageNew/PageHeader.tsx create mode 100644 public/app/core/components/PageNew/PageTabs.tsx create mode 100644 public/app/core/components/PageNew/SectionNav.tsx delete mode 100644 public/app/core/components/TopNav/TopNavUpdate.tsx delete mode 100644 public/app/core/components/TopNav/types.ts create mode 100644 public/app/core/utils/isShallowEqual.ts diff --git a/.betterer.results b/.betterer.results index 477d19d5d82..08e1538cf08 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5202,6 +5202,12 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], + "public/app/core/components/PageNew/PageTabs.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "public/app/core/components/PageNew/SectionNav.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/core/components/PanelTypeFilter/PanelTypeFilter.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -7897,6 +7903,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], + "public/app/features/teams/CreateTeam.test.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], "public/app/features/teams/TeamGroupSync.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] diff --git a/packages/grafana-data/src/types/navModel.ts b/packages/grafana-data/src/types/navModel.ts index 58f79261528..453e1a1f572 100644 --- a/packages/grafana-data/src/types/navModel.ts +++ b/packages/grafana-data/src/types/navModel.ts @@ -56,10 +56,6 @@ export interface NavModel { * This is the current active tab/navigation. */ node: NavModelItem; - /** - * Describes breadcrumbs that are used in places such as data source settings., folder page and plugins page. - */ - breadcrumbs?: NavModelItem[]; } export interface NavModelBreadcrumb { diff --git a/packages/grafana-ui/src/components/Tabs/VerticalTab.tsx b/packages/grafana-ui/src/components/Tabs/VerticalTab.tsx new file mode 100644 index 00000000000..c1fdeccff2b --- /dev/null +++ b/packages/grafana-ui/src/components/Tabs/VerticalTab.tsx @@ -0,0 +1,96 @@ +import { css, cx } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; + +import { useStyles2 } from '../../themes/ThemeContext'; +import { Icon } from '../Icon/Icon'; + +import { Counter } from './Counter'; +import { TabProps } from './Tab'; + +export const VerticalTab = React.forwardRef( + ({ label, active, icon, counter, className, suffix: Suffix, onChangeTab, href, ...otherProps }, ref) => { + const tabsStyles = useStyles2(getTabStyles); + const content = () => ( + <> + {icon && } + {label} + {typeof counter === 'number' && } + {Suffix && } + + ); + + const linkClass = cx(tabsStyles.link, active && tabsStyles.activeStyle); + + return ( +
  • + + {content()} + +
  • + ); + } +); + +VerticalTab.displayName = 'Tab'; + +const getTabStyles = (theme: GrafanaTheme2) => { + return { + item: css` + list-style: none; + margin-right: ${theme.spacing(2)}; + position: relative; + display: block; + margin-bottom: 4px; + `, + link: css` + padding: 6px 12px; + display: block; + height: 100%; + cursor: pointer; + + color: ${theme.colors.text.primary}; + + svg { + margin-right: ${theme.spacing(1)}; + } + + &:hover, + &:focus { + text-decoration: underline; + } + `, + activeStyle: css` + label: activeTabStyle; + color: ${theme.colors.text.maxContrast}; + font-weight: 500; + overflow: hidden; + + &::before { + display: block; + content: ' '; + position: absolute; + left: 0; + width: 4px; + bottom: 0; + top: 0; + border-radius: 2px; + background-image: linear-gradient(0deg, #f05a28 30%, #fbca0a 99%); + } + `, + suffix: css` + margin-left: ${theme.spacing(1)}; + `, + }; +}; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 721d749c863..e9dd8b85ada 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -79,6 +79,7 @@ export { TableCellDisplayMode, TableSortByFieldState } from './Table/types'; export { TableInputCSV } from './TableInputCSV/TableInputCSV'; export { TabsBar } from './Tabs/TabsBar'; export { Tab } from './Tabs/Tab'; +export { VerticalTab } from './Tabs/VerticalTab'; export { TabContent } from './Tabs/TabContent'; export { Counter } from './Tabs/Counter'; diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx index 2b8efc69b88..14ade91ddf7 100644 --- a/public/app/AppWrapper.tsx +++ b/public/app/AppWrapper.tsx @@ -12,6 +12,7 @@ import { store } from 'app/store/store'; import { AngularRoot } from './angular/AngularRoot'; import { loadAndInitAngularIfEnabled } from './angular/loadAndInitAngularIfEnabled'; import { GrafanaApp } from './app'; +import { AppChrome } from './core/components/AppChrome/AppChrome'; import { AppNotificationList } from './core/components/AppNotifications/AppNotificationList'; import { NavBar } from './core/components/NavBar/NavBar'; import { I18nProvider } from './core/localisation'; @@ -125,7 +126,7 @@ export class AppWrapper extends React.Component {this.renderNavBar()} -
    + {pageBanners.map((Banner, index) => ( ))} @@ -137,7 +138,7 @@ export class AppWrapper extends React.Component ( ))} -
    +
    diff --git a/public/app/angular/angular_wrappers.ts b/public/app/angular/angular_wrappers.ts index 15159553cd7..30a63a5af02 100644 --- a/public/app/angular/angular_wrappers.ts +++ b/public/app/angular/angular_wrappers.ts @@ -17,7 +17,7 @@ import { QueryEditor as CloudMonitoringQueryEditor } from 'app/plugins/datasourc import EmptyListCTA from '../core/components/EmptyListCTA/EmptyListCTA'; import { Footer } from '../core/components/Footer/Footer'; -import PageHeader from '../core/components/PageHeader/PageHeader'; +import { PageHeader } from '../core/components/PageHeader/PageHeader'; import { MetricSelect } from '../core/components/Select/MetricSelect'; import { TagFilter } from '../core/components/TagFilter/TagFilter'; import { HelpModal } from '../core/components/help/HelpModal'; diff --git a/public/app/angular/services/nav_model_srv.ts b/public/app/angular/services/nav_model_srv.ts index 73506625130..bf8790e1b3b 100644 --- a/public/app/angular/services/nav_model_srv.ts +++ b/public/app/angular/services/nav_model_srv.ts @@ -71,7 +71,6 @@ export function getWarningNav(text: string, subTitle?: string): NavModel { icon: 'exclamation-triangle', }; return { - breadcrumbs: [node], node: node, main: node, }; diff --git a/public/app/core/components/TopNav/TopNavPage.tsx b/public/app/core/components/AppChrome/AppChrome.tsx similarity index 60% rename from public/app/core/components/TopNav/TopNavPage.tsx rename to public/app/core/components/AppChrome/AppChrome.tsx index 9b8220ecfab..cfa37b4e79d 100644 --- a/public/app/core/components/TopNav/TopNavPage.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -1,75 +1,64 @@ import { css, cx } from '@emotion/css'; import React, { PropsWithChildren, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { useObservable, useToggle } from 'react-use'; -import { createSelector } from 'reselect'; +import { useToggle } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { useStyles2 } from '@grafana/ui'; -import { getNavModel } from 'app/core/selectors/navModel'; -import { StoreState } from 'app/types'; import { MegaMenu } from '../MegaMenu/MegaMenu'; +import { appChromeService } from './AppChromeService'; import { NavToolbar } from './NavToolbar'; -import { topNavDefaultProps, topNavUpdates } from './TopNavUpdate'; import { TopSearchBar } from './TopSearchBar'; import { TOP_BAR_LEVEL_HEIGHT } from './types'; -export interface Props extends PropsWithChildren<{}> { - /** This is nav tree id provided by route. - * It's not enough for item navigation. For that pages will need provide an item nav model as well via TopNavUpdate - */ - navId?: string; -} +export interface Props extends PropsWithChildren<{}> {} -export function TopNavPage({ children, navId }: Props) { +export function AppChrome({ children }: Props) { const styles = useStyles2(getStyles); const [searchBarHidden, toggleSearchBar] = useToggle(false); // repace with local storage - const props = useObservable(topNavUpdates, topNavDefaultProps); - const navModel = useSelector(createSelector(getNavIndex, (navIndex) => getNavModel(navIndex, navId ?? 'home'))); const [megaMenuOpen, setMegaMenuOpen] = useState(false); + const state = appChromeService.useState(); + + if (state.chromeless || !config.featureToggles.topnav) { + return
    {children}
    ; + } return ( -
    +
    {!searchBarHidden && } setMegaMenuOpen(!megaMenuOpen)} - sectionNav={navModel.node} />
    {children}
    {megaMenuOpen && setMegaMenuOpen(false)} />} -
    + ); } -function getNavIndex(store: StoreState) { - return store.navIndex; -} - 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 0.6px 1.5px rgb(0 0 0 / 8%), 0 2px 4px rgb(0 0 0 / 6%), 0 5px 10px rgb(0 0 0 / 5%)'; return { - viewport: css({ + content: css({ display: 'flex', + flexDirection: 'column', + paddingTop: TOP_BAR_LEVEL_HEIGHT * 2, flexGrow: 1, height: '100%', }), - content: css({ - display: 'flex', - paddingTop: TOP_BAR_LEVEL_HEIGHT * 2 + 16, - flexGrow: 1, - }), contentNoSearchBar: css({ - paddingTop: TOP_BAR_LEVEL_HEIGHT + 16, + paddingTop: TOP_BAR_LEVEL_HEIGHT, }), topNav: css({ display: 'flex', diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx new file mode 100644 index 00000000000..0c113f7701d --- /dev/null +++ b/public/app/core/components/AppChrome/AppChromeService.tsx @@ -0,0 +1,51 @@ +import { useObservable } from 'react-use'; +import { BehaviorSubject } from 'rxjs'; + +import { NavModelItem } from '@grafana/data'; +import { isShallowEqual } from 'app/core/utils/isShallowEqual'; + +import { RouteDescriptor } from '../../navigation/types'; + +export interface AppChromeState { + chromeless: boolean; + sectionNav: NavModelItem; + pageNav?: NavModelItem; + actions?: React.ReactNode; +} + +const defaultSection: NavModelItem = { text: 'Grafana' }; + +export class AppChromeService { + readonly state = new BehaviorSubject({ + chromeless: true, // start out hidden to not flash it on pages without chrome + sectionNav: defaultSection, + }); + + routeMounted(route: RouteDescriptor) { + this.update({ + chromeless: route.chromeless === true, + sectionNav: defaultSection, + pageNav: undefined, + actions: undefined, + }); + } + + update(state: Partial) { + const current = this.state.getValue(); + const newState: AppChromeState = { + ...current, + ...state, + }; + + if (!isShallowEqual(current, newState)) { + this.state.next(newState); + } + } + + useState() { + // eslint-disable-next-line react-hooks/rules-of-hooks + return useObservable(this.state, this.state.getValue()); + } +} + +export const appChromeService = new AppChromeService(); diff --git a/public/app/core/components/AppChrome/AppChromeUpdate.tsx b/public/app/core/components/AppChrome/AppChromeUpdate.tsx new file mode 100644 index 00000000000..38545bfce8f --- /dev/null +++ b/public/app/core/components/AppChrome/AppChromeUpdate.tsx @@ -0,0 +1,22 @@ +import React, { useEffect } from 'react'; + +import { NavModelItem } from '@grafana/data'; + +import { appChromeService } from './AppChromeService'; + +export interface AppChromeUpdateProps { + pageNav?: NavModelItem; + actions?: React.ReactNode; +} +/** + * This needs to be moved to @grafana/ui or runtime. + * This is the way core pages and plugins update the breadcrumbs and page toolbar actions + */ +export const AppChromeUpdate = React.memo(({ pageNav, actions }: AppChromeUpdateProps) => { + useEffect(() => { + appChromeService.update({ pageNav, actions }); + }); + return null; +}); + +AppChromeUpdate.displayName = 'TopNavUpdate'; diff --git a/public/app/core/components/TopNav/Breadcrumbs.tsx b/public/app/core/components/AppChrome/Breadcrumbs.tsx similarity index 86% rename from public/app/core/components/TopNav/Breadcrumbs.tsx rename to public/app/core/components/AppChrome/Breadcrumbs.tsx index bdfad00315e..c7bc80ec1d8 100644 --- a/public/app/core/components/TopNav/Breadcrumbs.tsx +++ b/public/app/core/components/AppChrome/Breadcrumbs.tsx @@ -4,11 +4,9 @@ import React from 'react'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { useStyles2, Icon, IconName } from '@grafana/ui'; -import { TopNavProps } from './TopNavUpdate'; - -export interface Props extends TopNavProps { +export interface Props { sectionNav: NavModelItem; - subNav?: NavModelItem; + pageNav?: NavModelItem; } export interface Breadcrumb { @@ -17,9 +15,9 @@ export interface Breadcrumb { href?: string; } -export function Breadcrumbs({ sectionNav, subNav }: Props) { +export function Breadcrumbs({ sectionNav, pageNav }: Props) { const styles = useStyles2(getStyles); - const crumbs: Breadcrumb[] = [{ icon: 'home', href: '/' }]; + const crumbs: Breadcrumb[] = [{ icon: 'home-alt', href: '/' }]; function addCrumbs(node: NavModelItem) { if (node.parentItem) { @@ -31,8 +29,8 @@ export function Breadcrumbs({ sectionNav, subNav }: Props) { addCrumbs(sectionNav); - if (subNav) { - addCrumbs(subNav); + if (pageNav) { + addCrumbs(pageNav); } return ( diff --git a/public/app/core/components/TopNav/NavToolbar.tsx b/public/app/core/components/AppChrome/NavToolbar.tsx similarity index 89% rename from public/app/core/components/TopNav/NavToolbar.tsx rename to public/app/core/components/AppChrome/NavToolbar.tsx index 9eef5e98a54..d0ca6b346d6 100644 --- a/public/app/core/components/TopNav/NavToolbar.tsx +++ b/public/app/core/components/AppChrome/NavToolbar.tsx @@ -5,22 +5,22 @@ import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { IconButton, ToolbarButton, useStyles2 } from '@grafana/ui'; import { Breadcrumbs } from './Breadcrumbs'; -import { TopNavProps } from './TopNavUpdate'; import { TOP_BAR_LEVEL_HEIGHT } from './types'; -export interface Props extends TopNavProps { +export interface Props { onToggleSearchBar(): void; onToggleMegaMenu(): void; searchBarHidden?: boolean; sectionNav: NavModelItem; - subNav?: NavModelItem; + pageNav?: NavModelItem; + actions: React.ReactNode; } export function NavToolbar({ actions, searchBarHidden, sectionNav, - subNav, + pageNav, onToggleMegaMenu, onToggleSearchBar, }: Props) { @@ -31,7 +31,7 @@ export function NavToolbar({
    - +
    {actions} diff --git a/public/app/core/components/TopNav/TopSearchBar.tsx b/public/app/core/components/AppChrome/TopSearchBar.tsx similarity index 100% rename from public/app/core/components/TopNav/TopSearchBar.tsx rename to public/app/core/components/AppChrome/TopSearchBar.tsx diff --git a/public/app/core/components/AppChrome/types.ts b/public/app/core/components/AppChrome/types.ts new file mode 100644 index 00000000000..654334fdea6 --- /dev/null +++ b/public/app/core/components/AppChrome/types.ts @@ -0,0 +1,8 @@ +import { NavModelItem } from '@grafana/data'; + +export const TOP_BAR_LEVEL_HEIGHT = 40; + +export interface ToolbarUpdateProps { + pageNav?: NavModelItem; + actions?: React.ReactNode; +} diff --git a/public/app/core/components/ErrorPage/ErrorPage.tsx b/public/app/core/components/ErrorPage/ErrorPage.tsx index 8fe3e4c645e..25e77912f6c 100644 --- a/public/app/core/components/ErrorPage/ErrorPage.tsx +++ b/public/app/core/components/ErrorPage/ErrorPage.tsx @@ -7,7 +7,7 @@ import { Icon } from '@grafana/ui'; import { getNavModel } from 'app/core/selectors/navModel'; import { StoreState } from 'app/types'; -import Page from '../Page/Page'; +import { Page } from '../Page/Page'; interface ConnectedProps { navModel: NavModel; diff --git a/public/app/core/components/MegaMenu/NavBarMenu.tsx b/public/app/core/components/MegaMenu/NavBarMenu.tsx index 248d2f7aa07..2b2df363de4 100644 --- a/public/app/core/components/MegaMenu/NavBarMenu.tsx +++ b/public/app/core/components/MegaMenu/NavBarMenu.tsx @@ -9,9 +9,9 @@ import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { CustomScrollbar, Icon, IconButton, useTheme2 } from '@grafana/ui'; +import { TOP_BAR_LEVEL_HEIGHT } from '../AppChrome/types'; import { NavItem } from '../NavBar/NavBarMenu'; import { NavBarToggle } from '../NavBar/NavBarToggle'; -import { TOP_BAR_LEVEL_HEIGHT } from '../TopNav/types'; const MENU_WIDTH = '350px'; diff --git a/public/app/core/components/Page/Page.test.tsx b/public/app/core/components/Page/Page.test.tsx new file mode 100644 index 00000000000..3ac2e7edcff --- /dev/null +++ b/public/app/core/components/Page/Page.test.tsx @@ -0,0 +1,67 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { Provider } from 'react-redux'; + +import { NavModelItem } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { configureStore } from 'app/store/configureStore'; + +import { Page } from './Page'; +import { PageProps } from './types'; + +const pageNav: NavModelItem = { + text: 'Main title', + children: [ + { text: 'Child1', url: '1', active: true }, + { text: 'Child2', url: '2' }, + ], +}; + +const setup = (props: Partial) => { + config.bootData.navTree = [ + { + text: 'Section name', + id: 'section', + url: 'section', + children: [ + { text: 'Child1', id: 'child1', url: 'section/child1' }, + { text: 'Child2', id: 'child2', url: 'section/child2' }, + ], + }, + ]; + + const store = configureStore(); + + return render( + + +
    Children
    +
    +
    + ); +}; + +describe('Render', () => { + it('should render component with emtpy Page container', async () => { + setup({}); + const children = await screen.findByTestId('page-children'); + expect(children).toBeInTheDocument(); + + const pageHeader = screen.queryByRole('heading'); + expect(pageHeader).not.toBeInTheDocument(); + }); + + it('should render header when pageNav supplied', async () => { + setup({ pageNav }); + + expect(screen.getByRole('heading', { name: 'Main title' })).toBeInTheDocument(); + expect(screen.getAllByRole('tab').length).toBe(2); + }); + + it('should get header nav model from redux navIndex', async () => { + setup({ navId: 'child1' }); + + expect(screen.getByRole('heading', { name: 'Section name' })).toBeInTheDocument(); + expect(screen.getAllByRole('tab').length).toBe(2); + }); +}); diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index 121b9ba1548..643c9ee281e 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -1,45 +1,33 @@ // Libraries import { css, cx } from '@emotion/css'; -import React, { FC, HTMLAttributes, useEffect } from 'react'; +import React from 'react'; -import { GrafanaTheme2, NavModel } from '@grafana/data'; +import { GrafanaTheme2 } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { CustomScrollbar, useStyles2 } from '@grafana/ui'; -import { getTitleFromNavModel } from 'app/core/selectors/navModel'; -// Components -import { Branding } from '../Branding/Branding'; import { Footer } from '../Footer/Footer'; -import PageHeader from '../PageHeader/PageHeader'; +import { PageHeader } from '../PageHeader/PageHeader'; +import { Page as NewPage } from '../PageNew/Page'; import { PageContents } from './PageContents'; +import { PageType } from './types'; +import { usePageNav } from './usePageNav'; +import { usePageTitle } from './usePageTitle'; -interface Props extends HTMLAttributes { - children: React.ReactNode; - navModel?: NavModel; -} - -export interface PageType extends FC { - Header: typeof PageHeader; - Contents: typeof PageContents; -} - -export const Page: PageType = ({ navModel, children, className, ...otherProps }) => { +export const OldPage: PageType = ({ navId, navModel: oldNavProp, pageNav, children, className, ...otherProps }) => { const styles = useStyles2(getStyles); + const navModel = usePageNav(navId, oldNavProp); - useEffect(() => { - if (navModel) { - const title = getTitleFromNavModel(navModel); - document.title = title ? `${title} - ${Branding.AppTitle}` : Branding.AppTitle; - } else { - document.title = Branding.AppTitle; - } - }, [navModel]); + usePageTitle(navModel, pageNav); + + const pageHeaderNav = pageNav ?? navModel?.main; return (
    - {navModel && } + {pageHeaderNav && } {children}
    @@ -48,12 +36,12 @@ export const Page: PageType = ({ navModel, children, className, ...otherProps }) ); }; -Page.Header = PageHeader; -Page.Contents = PageContents; +OldPage.Header = PageHeader; +OldPage.Contents = PageContents; -export default Page; +export const Page: PageType = config.featureToggles.topnav ? NewPage : OldPage; -const getStyles = (theme: GrafanaTheme2) => ({ +const getStyles = (_: GrafanaTheme2) => ({ wrapper: css` width: 100%; flex-grow: 1; diff --git a/public/app/core/components/Page/types.ts b/public/app/core/components/Page/types.ts new file mode 100644 index 00000000000..bc8340f22fc --- /dev/null +++ b/public/app/core/components/Page/types.ts @@ -0,0 +1,19 @@ +import { FC, HTMLAttributes } from 'react'; + +import { NavModel, NavModelItem } from '@grafana/data'; + +import { PageHeader } from '../PageHeader/PageHeader'; + +import { PageContents } from './PageContents'; + +export interface PageProps extends HTMLAttributes { + children: React.ReactNode; + navId?: string; + navModel?: NavModel; + pageNav?: NavModelItem; +} + +export interface PageType extends FC { + Header: typeof PageHeader; + Contents: typeof PageContents; +} diff --git a/public/app/core/components/Page/usePageNav.ts b/public/app/core/components/Page/usePageNav.ts new file mode 100644 index 00000000000..e78a5d17aa6 --- /dev/null +++ b/public/app/core/components/Page/usePageNav.ts @@ -0,0 +1,29 @@ +import { useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; + +import { NavModel } from '@grafana/data'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { store } from 'app/store/store'; +import { StoreState } from 'app/types'; + +export function usePageNav(navId?: string, oldProp?: NavModel): NavModel | undefined { + if (oldProp) { + return oldProp; + } + + if (!navId) { + return; + } + + // Page component is used in so many tests, this simplifies not having to initialize a full redux store + if (!store) { + return; + } + + // eslint-disable-next-line react-hooks/rules-of-hooks + return useSelector(createSelector(getNavIndex, (navIndex) => getNavModel(navIndex, navId ?? 'home'))); +} + +function getNavIndex(store: StoreState) { + return store.navIndex; +} diff --git a/public/app/core/components/Page/usePageTitle.ts b/public/app/core/components/Page/usePageTitle.ts new file mode 100644 index 00000000000..b7512bb7b46 --- /dev/null +++ b/public/app/core/components/Page/usePageTitle.ts @@ -0,0 +1,32 @@ +import { useEffect } from 'react'; + +import { NavModel, NavModelItem } from '@grafana/data'; + +import { Branding } from '../Branding/Branding'; + +export function usePageTitle(navModel?: NavModel, pageNav?: NavModelItem) { + useEffect(() => { + const parts: string[] = []; + + if (pageNav) { + if (pageNav.children) { + const activePage = pageNav.children.find((x) => x.active); + if (activePage) { + parts.push(activePage.text); + } + } + parts.push(pageNav.text); + } + + if (navModel) { + if (navModel.node !== navModel.main) { + parts.push(navModel.node.text); + } + parts.push(navModel.main.text); + } + + parts.push(Branding.AppTitle); + + document.title = parts.join(' - '); + }, [navModel, pageNav]); +} diff --git a/public/app/core/components/PageHeader/PageHeader.test.tsx b/public/app/core/components/PageHeader/PageHeader.test.tsx index 2965d007142..3e5af5ee1f9 100644 --- a/public/app/core/components/PageHeader/PageHeader.test.tsx +++ b/public/app/core/components/PageHeader/PageHeader.test.tsx @@ -1,23 +1,20 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import PageHeader from './PageHeader'; +import { PageHeader } from './PageHeader'; describe('PageHeader', () => { describe('when the nav tree has a node with a title', () => { it('should render the title', async () => { const nav = { - main: { - icon: 'folder-open', - id: 'node', - subTitle: 'node subtitle', - url: '', - text: 'node', - }, - node: {}, + icon: 'folder-open', + id: 'node', + subTitle: 'node subtitle', + url: '', + text: 'node', }; - render(); + render(); expect(screen.getByRole('heading', { name: 'node' })).toBeInTheDocument(); }); @@ -26,18 +23,15 @@ describe('PageHeader', () => { describe('when the nav tree has a node with breadcrumbs and a title', () => { it('should render the title with breadcrumbs first and then title last', async () => { const nav = { - main: { - icon: 'folder-open', - id: 'child', - subTitle: 'child subtitle', - url: '', - text: 'child', - breadcrumbs: [{ title: 'Parent', url: 'parentUrl' }], - }, - node: {}, + icon: 'folder-open', + id: 'child', + subTitle: 'child subtitle', + url: '', + text: 'child', + breadcrumbs: [{ title: 'Parent', url: 'parentUrl' }], }; - render(); + render(); expect(screen.getByRole('heading', { name: 'Parent / child' })).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Parent' })).toBeInTheDocument(); diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index 116ff8186dd..45fe58f1474 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -1,14 +1,14 @@ import { css } from '@emotion/css'; import React, { FC } from 'react'; -import { NavModel, NavModelItem, NavModelBreadcrumb, GrafanaTheme2 } from '@grafana/data'; +import { NavModelItem, NavModelBreadcrumb, GrafanaTheme2 } from '@grafana/data'; import { Tab, TabsBar, Icon, IconName, useStyles2 } from '@grafana/ui'; import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem'; import { ProBadge } from '../Upgrade/ProBadge'; export interface Props { - model: NavModel; + navItem: NavModelItem; } const SelectNav = ({ children, customCss }: { children: NavModelItem[]; customCss: string }) => { @@ -75,21 +75,19 @@ const Navigation = ({ children }: { children: NavModelItem[] }) => { ); }; -export const PageHeader: FC = ({ model }) => { +export const PageHeader: FC = ({ navItem: model }) => { const styles = useStyles2(getStyles); if (!model) { return null; } - const main = model.main; - const children = main.children; return (
    - {renderHeaderTitle(main)} - {children && children.length && {children}} + {renderHeaderTitle(model)} + {model.children && model.children.length > 0 && {model.children}}
    @@ -157,5 +155,3 @@ const getStyles = (theme: GrafanaTheme2) => ({ background: ${theme.colors.background.canvas}; `, }); - -export default PageHeader; diff --git a/public/app/core/components/PageNew/Page.test.tsx b/public/app/core/components/PageNew/Page.test.tsx new file mode 100644 index 00000000000..a28ecec2b7d --- /dev/null +++ b/public/app/core/components/PageNew/Page.test.tsx @@ -0,0 +1,79 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { Provider } from 'react-redux'; + +import { NavModelItem } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { configureStore } from 'app/store/configureStore'; + +import { PageProps } from '../Page/types'; + +import { Page } from './Page'; + +const pageNav: NavModelItem = { + text: 'pageNav title', + children: [ + { text: 'pageNav child1', url: '1', active: true }, + { text: 'pageNav child2', url: '2' }, + ], +}; + +const setup = (props: Partial) => { + config.bootData.navTree = [ + { + text: 'Section name', + id: 'section', + url: 'section', + children: [ + { text: 'Child1', id: 'child1', url: 'section/child1' }, + { text: 'Child2', id: 'child2', url: 'section/child2' }, + ], + }, + ]; + + const store = configureStore(); + + return render( + + +
    Children
    +
    +
    + ); +}; + +describe('Render', () => { + it('should render component with emtpy Page container', async () => { + setup({}); + const children = await screen.findByTestId('page-children'); + expect(children).toBeInTheDocument(); + + const pageHeader = screen.queryByRole('heading'); + expect(pageHeader).not.toBeInTheDocument(); + }); + + it('should render header when pageNav supplied', async () => { + setup({ pageNav }); + + expect(screen.getByRole('heading', { name: 'pageNav title' })).toBeInTheDocument(); + expect(screen.getAllByRole('tab').length).toBe(2); + }); + + it('should render section nav model based on navId', async () => { + setup({ navId: 'child1' }); + + expect(screen.getByRole('heading', { name: 'Section name' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Child1' })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'Tab Child1' })).toBeInTheDocument(); + expect(screen.getAllByRole('tab').length).toBe(2); + }); + + it('should render section nav model based on navId and item page nav', async () => { + setup({ navId: 'child1', pageNav }); + + expect(screen.getByRole('heading', { name: '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(); + }); +}); diff --git a/public/app/core/components/PageNew/Page.tsx b/public/app/core/components/PageNew/Page.tsx new file mode 100644 index 00000000000..72bf56d951a --- /dev/null +++ b/public/app/core/components/PageNew/Page.tsx @@ -0,0 +1,93 @@ +// Libraries +import { css, cx } from '@emotion/css'; +import React, { useEffect } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { CustomScrollbar, useStyles2 } from '@grafana/ui'; + +// Components +import { appChromeService } from '../AppChrome/AppChromeService'; +import { Footer } from '../Footer/Footer'; +import { PageType } from '../Page/types'; +import { usePageNav } from '../Page/usePageNav'; +import { usePageTitle } from '../Page/usePageTitle'; + +import { PageContents } from './PageContents'; +import { PageHeader } from './PageHeader'; +import { PageTabs } from './PageTabs'; +import { SectionNav } from './SectionNav'; + +export const Page: PageType = ({ navId, navModel: oldNavProp, pageNav, children, className, ...otherProps }) => { + const styles = useStyles2(getStyles); + const navModel = usePageNav(navId, oldNavProp); + + usePageTitle(navModel, pageNav); + + const pageHeaderNav = pageNav ?? navModel?.node; + + useEffect(() => { + if (navModel || pageNav) { + appChromeService.update({ sectionNav: navModel?.node, pageNav }); + } + }, [navModel, pageNav]); + + return ( +
    +
    + {navModel && navModel.main.children && } +
    + +
    + {pageHeaderNav && } + {pageNav && pageNav.children && } + {children} +
    +
    + +
    +
    +
    + ); +}; + +Page.Header = PageHeader; +Page.Contents = PageContents; + +const getStyles = (theme: GrafanaTheme2) => { + const shadow = theme.isDark + ? `0 0.6px 1.5px -1px rgb(0 0 0),0 2px 4px -1px rgb(0 0 0 / 40%),0 5px 10px -1px rgb(0 0 0 / 23%)` + : '0 0.6px 1.5px -1px rgb(0 0 0 / 8%),0 2px 4px rgb(0 0 0 / 6%),0 5px 10px -1px rgb(0 0 0 / 5%)'; + + return { + wrapper: css` + height: 100%; + display: flex; + flex: 1 1 0; + flex-direction: column; + min-height: 0; + `, + panes: css({ + display: 'flex', + height: '100%', + width: '100%', + flexGrow: 1, + minHeight: 0, + flexDirection: 'column', + [theme.breakpoints.up('md')]: { + flexDirection: 'row', + }, + }), + pageContent: css({ + flexGrow: 1, + }), + pageInner: css({ + padding: theme.spacing(3), + boxShadow: shadow, + background: theme.colors.background.primary, + margin: theme.spacing(2, 2, 2, 1), + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + }), + }; +}; diff --git a/public/app/core/components/PageNew/PageContents.tsx b/public/app/core/components/PageNew/PageContents.tsx new file mode 100644 index 00000000000..85a04fa295a --- /dev/null +++ b/public/app/core/components/PageNew/PageContents.tsx @@ -0,0 +1,14 @@ +// Libraries +import React, { FC } from 'react'; + +import PageLoader from '../PageLoader/PageLoader'; + +interface Props { + isLoading?: boolean; + children: React.ReactNode; + className?: string; +} + +export const PageContents: FC = ({ isLoading, children }) => { + return <>{isLoading ? : children}; +}; diff --git a/public/app/core/components/PageNew/PageHeader.tsx b/public/app/core/components/PageNew/PageHeader.tsx new file mode 100644 index 00000000000..1879563c90e --- /dev/null +++ b/public/app/core/components/PageNew/PageHeader.tsx @@ -0,0 +1,43 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { NavModelItem, GrafanaTheme2 } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; + +export interface Props { + navItem: NavModelItem; +} + +export function PageHeader({ navItem }: Props) { + const styles = useStyles2(getStyles); + + return ( + <> +

    + {navItem.img && {`logo} + {navItem.text} +

    + {navItem.subTitle &&
    {navItem.subTitle}
    } + + ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + pageTitle: css({ + display: 'flex', + marginBottom: theme.spacing(3), + }), + pageSubTitle: css({ + marginBottom: theme.spacing(2), + position: 'relative', + top: theme.spacing(-1), + color: theme.colors.text.secondary, + }), + pageImg: css({ + width: '32px', + height: '32px', + marginRight: theme.spacing(2), + }), + }; +}; diff --git a/public/app/core/components/PageNew/PageTabs.tsx b/public/app/core/components/PageNew/PageTabs.tsx new file mode 100644 index 00000000000..d3fb3a73c9b --- /dev/null +++ b/public/app/core/components/PageNew/PageTabs.tsx @@ -0,0 +1,42 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { NavModelItem, GrafanaTheme2 } from '@grafana/data'; +import { IconName, useStyles2, TabsBar, Tab } from '@grafana/ui'; + +export interface Props { + navItem: NavModelItem; +} + +export function PageTabs({ navItem }: Props) { + const styles = useStyles2(getStyles); + + return ( +
    + + {navItem.children!.map((child, index) => { + return ( + !child.hideFromTabs && ( + + ) + ); + })} + +
    + ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + tabsWrapper: css({ + paddingBottom: theme.spacing(3), + }), + }; +}; diff --git a/public/app/core/components/PageNew/SectionNav.tsx b/public/app/core/components/PageNew/SectionNav.tsx new file mode 100644 index 00000000000..3570de4294f --- /dev/null +++ b/public/app/core/components/PageNew/SectionNav.tsx @@ -0,0 +1,92 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { NavModel, GrafanaTheme2 } from '@grafana/data'; +import { IconName, useStyles2, Icon, VerticalTab } from '@grafana/ui'; + +export interface Props { + model: NavModel; +} + +export function SectionNav(props: Props) { + const styles = useStyles2(getStyles); + + const main = props.model.main; + const directChildren = props.model.main.children!.filter((x) => !x.hideFromTabs && !x.children); + const nestedItems = props.model.main.children!.filter((x) => x.children && x.children.length); + + return ( + + ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + nav: css({ + display: 'flex', + flexDirection: 'column', + background: theme.colors.background.canvas, + padding: theme.spacing(3, 2), + flexShrink: 0, + [theme.breakpoints.up('md')]: { + width: '250px', + }, + }), + sectionName: css({ + display: 'flex', + gap: theme.spacing(1), + padding: theme.spacing(0.5, 0, 3, 0.25), + fontSize: theme.typography.h4.fontSize, + margin: 0, + }), + items: css({ + // paddingLeft: '9px', + }), + subSection: css({ + padding: theme.spacing(3, 0, 1, 1), + fontWeight: 500, + fontSize: '16px', + }), + }; +}; diff --git a/public/app/core/components/TopNav/TopNavUpdate.tsx b/public/app/core/components/TopNav/TopNavUpdate.tsx deleted file mode 100644 index c16ba7a2755..00000000000 --- a/public/app/core/components/TopNav/TopNavUpdate.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect } from 'react'; -import { Subject } from 'rxjs'; - -import { NavModelItem } from '@grafana/data'; - -export interface TopNavProps { - subNav?: NavModelItem; - actions?: React.ReactNode; -} - -export const topNavUpdates = new Subject(); -export const topNavDefaultProps: TopNavProps = {}; - -/** - * This needs to be moved to @grafana/ui or runtime. - * This is the way core pages and plugins update the breadcrumbs and page toolbar actions - */ -export function TopNavUpdate(props: TopNavProps) { - useEffect(() => { - topNavUpdates.next(props); - }); - return null; -} diff --git a/public/app/core/components/TopNav/types.ts b/public/app/core/components/TopNav/types.ts deleted file mode 100644 index 9171a881c48..00000000000 --- a/public/app/core/components/TopNav/types.ts +++ /dev/null @@ -1 +0,0 @@ -export const TOP_BAR_LEVEL_HEIGHT = 40; diff --git a/public/app/core/navigation/GrafanaRoute.tsx b/public/app/core/navigation/GrafanaRoute.tsx index 7b2e7445b0d..17a2e93d3b9 100644 --- a/public/app/core/navigation/GrafanaRoute.tsx +++ b/public/app/core/navigation/GrafanaRoute.tsx @@ -2,9 +2,9 @@ import React from 'react'; // @ts-ignore import Drop from 'tether-drop'; -import { config, locationSearchToObject, navigationLogger, reportPageview } from '@grafana/runtime'; +import { locationSearchToObject, navigationLogger, reportPageview } from '@grafana/runtime'; -import { TopNavPage } from '../components/TopNav/TopNavPage'; +import { appChromeService } from '../components/AppChrome/AppChromeService'; import { keybindingSrv } from '../services/keybindingSrv'; import { GrafanaRouteComponentProps } from './types'; @@ -13,6 +13,8 @@ export interface Props extends Omit { export class GrafanaRoute extends React.Component { componentDidMount() { + appChromeService.routeMounted(this.props.route); + this.updateBodyClassNames(); this.cleanupDOM(); // unbinds all and re-bind global keybindins @@ -71,12 +73,6 @@ export class GrafanaRoute extends React.Component { navigationLogger('GrafanaRoute', false, 'Rendered', props.route); const RouteComponent = props.route.component; - const routeElement = ; - - if (config.featureToggles.topnav && !props.route.navHidden) { - return {routeElement}; - } - - return routeElement; + return ; } } diff --git a/public/app/core/navigation/types.ts b/public/app/core/navigation/types.ts index d34089aca8f..cffda8ede63 100644 --- a/public/app/core/navigation/types.ts +++ b/public/app/core/navigation/types.ts @@ -17,7 +17,6 @@ export interface RouteDescriptor { pageClass?: string; /** Can be used like an id for the route if the same component is used by many routes */ routeName?: string; - navHidden?: boolean; + chromeless?: boolean; exact?: boolean; - navId?: string; } diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index bdb256bfe1a..6be4673248d 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -32,7 +32,6 @@ function buildWarningNav(text: string, subTitle?: string): NavModel { icon: 'exclamation-triangle', }; return { - breadcrumbs: [node], node: node, main: node, }; diff --git a/public/app/core/utils/isShallowEqual.ts b/public/app/core/utils/isShallowEqual.ts new file mode 100644 index 00000000000..8d772c8af5b --- /dev/null +++ b/public/app/core/utils/isShallowEqual.ts @@ -0,0 +1,29 @@ +// From https://github.com/streamich/fast-shallow-equal + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isShallowEqual(a: any, b: any) { + if (a === b) { + return true; + } + + if (!(a instanceof Object) || !(b instanceof Object)) { + return false; + } + + var keys = Object.keys(a); + var length = keys.length; + + for (let i = 0; i < length; i++) { + if (!(keys[i] in b)) { + return false; + } + } + + for (let i = 0; i < length; i++) { + if (a[keys[i]] !== b[keys[i]]) { + return false; + } + } + + return length === Object.keys(b).length; +} diff --git a/public/app/features/admin/AdminEditOrgPage.tsx b/public/app/features/admin/AdminEditOrgPage.tsx index 44bc10e0134..8ab6d856d77 100644 --- a/public/app/features/admin/AdminEditOrgPage.tsx +++ b/public/app/features/admin/AdminEditOrgPage.tsx @@ -6,7 +6,7 @@ import { useAsyncFn } from 'react-use'; import { UrlQueryValue } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { Form, Field, Input, Button, Legend, Alert } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; diff --git a/public/app/features/admin/AdminListOrgsPage.tsx b/public/app/features/admin/AdminListOrgsPage.tsx index 6fff515aa09..94757ff3715 100644 --- a/public/app/features/admin/AdminListOrgsPage.tsx +++ b/public/app/features/admin/AdminListOrgsPage.tsx @@ -4,7 +4,7 @@ import useAsyncFn from 'react-use/lib/useAsyncFn'; import { getBackendSrv } from '@grafana/runtime'; import { LinkButton } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; import { contextSrv } from 'app/core/services/context_srv'; import { AccessControlAction } from 'app/types'; diff --git a/public/app/features/admin/AdminSettings.tsx b/public/app/features/admin/AdminSettings.tsx index e2e2a0c7ecb..f0b90e6e2b8 100644 --- a/public/app/features/admin/AdminSettings.tsx +++ b/public/app/features/admin/AdminSettings.tsx @@ -4,7 +4,7 @@ import { useAsync } from 'react-use'; import { NavModel } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; import { StoreState } from 'app/types'; diff --git a/public/app/features/admin/UpgradePage.tsx b/public/app/features/admin/UpgradePage.tsx index 3678cb244c6..07b93187d28 100644 --- a/public/app/features/admin/UpgradePage.tsx +++ b/public/app/features/admin/UpgradePage.tsx @@ -4,8 +4,8 @@ import { connect } from 'react-redux'; import { GrafanaTheme2, NavModel } from '@grafana/data'; import { LinkButton, useStyles2 } from '@grafana/ui'; +import { Page } from 'app/core/components/Page/Page'; -import Page from '../../core/components/Page/Page'; import { getNavModel } from '../../core/selectors/navModel'; import { StoreState } from '../../types'; diff --git a/public/app/features/admin/UserAdminPage.tsx b/public/app/features/admin/UserAdminPage.tsx index 264e9ce943a..14865464d1e 100644 --- a/public/app/features/admin/UserAdminPage.tsx +++ b/public/app/features/admin/UserAdminPage.tsx @@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { NavModel } from '@grafana/data'; import { featureEnabled } from '@grafana/runtime'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; diff --git a/public/app/features/admin/UserCreatePage.tsx b/public/app/features/admin/UserCreatePage.tsx index b98e82a3e6a..e68a20eeebf 100644 --- a/public/app/features/admin/UserCreatePage.tsx +++ b/public/app/features/admin/UserCreatePage.tsx @@ -5,7 +5,7 @@ import { useHistory } from 'react-router-dom'; import { NavModel } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { Form, Button, Input, Field } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from '../../core/selectors/navModel'; import { StoreState } from '../../types'; diff --git a/public/app/features/admin/UserListAdminPage.tsx b/public/app/features/admin/UserListAdminPage.tsx index 7fa5809ae4f..f7435d397df 100644 --- a/public/app/features/admin/UserListAdminPage.tsx +++ b/public/app/features/admin/UserListAdminPage.tsx @@ -13,7 +13,7 @@ import { useStyles2, FilterInput, } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { contextSrv } from 'app/core/core'; diff --git a/public/app/features/admin/ldap/LdapPage.tsx b/public/app/features/admin/ldap/LdapPage.tsx index 28adbdc13ad..f0959e6f70e 100644 --- a/public/app/features/admin/ldap/LdapPage.tsx +++ b/public/app/features/admin/ldap/LdapPage.tsx @@ -5,7 +5,7 @@ import { NavModel } from '@grafana/data'; import { featureEnabled } from '@grafana/runtime'; import { Alert, Button, LegacyForms } from '@grafana/ui'; const { FormField } = LegacyForms; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 10de691e571..a19d2ab7bbd 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -5,7 +5,7 @@ import { SelectableValue } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { Button, FilterInput, LinkButton, Select, VerticalGroup } from '@grafana/ui'; import appEvents from 'app/core/app_events'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; import { AlertRule, StoreState } from 'app/types'; diff --git a/public/app/features/alerting/EditNotificationChannelPage.tsx b/public/app/features/alerting/EditNotificationChannelPage.tsx index 45aba248222..7303f7ccfd3 100644 --- a/public/app/features/alerting/EditNotificationChannelPage.tsx +++ b/public/app/features/alerting/EditNotificationChannelPage.tsx @@ -4,7 +4,7 @@ import { MapDispatchToProps, MapStateToProps } from 'react-redux'; import { NavModel } from '@grafana/data'; import { config } from '@grafana/runtime'; import { Form, Spinner } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; diff --git a/public/app/features/alerting/FeatureTogglePage.tsx b/public/app/features/alerting/FeatureTogglePage.tsx index 63f62796f94..e6def5ec3f5 100644 --- a/public/app/features/alerting/FeatureTogglePage.tsx +++ b/public/app/features/alerting/FeatureTogglePage.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { useNavModel } from 'app/core/hooks/useNavModel'; export default function FeatureTogglePage() { diff --git a/public/app/features/alerting/NewNotificationChannelPage.tsx b/public/app/features/alerting/NewNotificationChannelPage.tsx index 418029fb2e5..93d6ca43865 100644 --- a/public/app/features/alerting/NewNotificationChannelPage.tsx +++ b/public/app/features/alerting/NewNotificationChannelPage.tsx @@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { config } from '@grafana/runtime'; import { Form } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; import { NotificationChannelDTO, StoreState } from '../../types'; diff --git a/public/app/features/alerting/NotificationsListPage.tsx b/public/app/features/alerting/NotificationsListPage.tsx index 9a5e5b3f4eb..6f11e260aac 100644 --- a/public/app/features/alerting/NotificationsListPage.tsx +++ b/public/app/features/alerting/NotificationsListPage.tsx @@ -4,7 +4,7 @@ import { useAsyncFn } from 'react-use'; import { getBackendSrv } from '@grafana/runtime'; import { HorizontalGroup, Button, LinkButton } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { appEvents } from 'app/core/core'; import { useNavModel } from 'app/core/hooks/useNavModel'; import { AlertNotification } from 'app/types/alerting'; diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/RuleEditor.tsx index 57d66fc319f..e550148abbe 100644 --- a/public/app/features/alerting/unified/RuleEditor.tsx +++ b/public/app/features/alerting/unified/RuleEditor.tsx @@ -5,7 +5,7 @@ import { useAsync } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; import { Alert, LinkButton, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { useCleanup } from 'app/core/hooks/useCleanup'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { RuleIdentifier } from 'app/types/unified-alerting'; diff --git a/public/app/features/alerting/unified/components/AlertingPageWrapper.tsx b/public/app/features/alerting/unified/components/AlertingPageWrapper.tsx index 21a7826139e..bb87e4a7a4e 100644 --- a/public/app/features/alerting/unified/components/AlertingPageWrapper.tsx +++ b/public/app/features/alerting/unified/components/AlertingPageWrapper.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react'; import { useSelector } from 'react-redux'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; import { StoreState } from 'app/types/store'; diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 9df08efd22e..4cb20857620 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -6,7 +6,7 @@ import { rangeUtil } from '@grafana/data'; import { InlineField, InlineSwitch, VerticalGroup } from '@grafana/ui'; import appEvents from 'app/core/app_events'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { getNavModel } from 'app/core/selectors/navModel'; diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 09101d40d5a..d306ad6022b 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -5,7 +5,7 @@ import { useLocation } from 'react-router-dom'; import { locationUtil, textUtil } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { ButtonGroup, ModalsController, ToolbarButton, PageToolbar, useForceUpdate } from '@grafana/ui'; -import { TopNavUpdate } from 'app/core/components/TopNav/TopNavUpdate'; +import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import config from 'app/core/config'; import { toggleKioskMode } from 'app/core/navigation/kiosk'; import { DashboardCommentsModal } from 'app/features/dashboard/components/DashboardComments/DashboardCommentsModal'; @@ -277,8 +277,8 @@ export const DashNav = React.memo((props) => { if (config.featureToggles.topnav) { return ( - } /> ); diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index d32eb5c1817..017e6adc8a1 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -5,7 +5,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { GrafanaTheme2, TimeRange } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { locationService } from '@grafana/runtime'; +import { config, locationService } from '@grafana/runtime'; import { CustomScrollbar, stylesFactory, Themeable2, withTheme2 } from '@grafana/ui'; import { notifyApp } from 'app/core/actions'; import { Branding } from 'app/core/components/Branding/Branding'; @@ -387,7 +387,8 @@ export class UnthemedDashboardPage extends PureComponent { * Styles */ export const getStyles = stylesFactory((theme: GrafanaTheme2, kioskMode: KioskMode) => { - const contentPadding = kioskMode !== KioskMode.Full ? theme.spacing(0, 2, 2) : theme.spacing(2); + const contentPadding = + kioskMode === KioskMode.Full || config.featureToggles.topnav ? theme.spacing(2) : theme.spacing(0, 2, 2); return { dashboardContainer: css` width: 100%; diff --git a/public/app/features/datasources/DataSourceDashboards.tsx b/public/app/features/datasources/DataSourceDashboards.tsx index 9673716ddc3..9033a94ecc6 100644 --- a/public/app/features/datasources/DataSourceDashboards.tsx +++ b/public/app/features/datasources/DataSourceDashboards.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; import { PluginDashboard, StoreState } from 'app/types'; diff --git a/public/app/features/datasources/DataSourcesListPage.tsx b/public/app/features/datasources/DataSourcesListPage.tsx index f03e7ede1af..4535c5b0f76 100644 --- a/public/app/features/datasources/DataSourcesListPage.tsx +++ b/public/app/features/datasources/DataSourcesListPage.tsx @@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { IconName } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import PageActionBar from 'app/core/components/PageActionBar/PageActionBar'; import { contextSrv } from 'app/core/core'; import { getNavModel } from 'app/core/selectors/navModel'; diff --git a/public/app/features/datasources/NewDataSourcePage.tsx b/public/app/features/datasources/NewDataSourcePage.tsx index b7943e8af90..2f226c69784 100644 --- a/public/app/features/datasources/NewDataSourcePage.tsx +++ b/public/app/features/datasources/NewDataSourcePage.tsx @@ -5,7 +5,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { DataSourcePluginMeta, GrafanaTheme2, NavModel } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Card, LinkButton, List, PluginSignatureBadge, FilterInput, useStyles2 } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { StoreState } from 'app/types'; import { PluginsErrorsInfo } from '../plugins/components/PluginsErrorsInfo'; diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx index 68e2a4f2384..3630fb0d48f 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx @@ -6,7 +6,7 @@ import { selectors } from '@grafana/e2e-selectors'; import { Alert, Button } from '@grafana/ui'; import { cleanUpAction } from 'app/core/actions/cleanUp'; import appEvents from 'app/core/app_events'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; diff --git a/public/app/features/explore/FeatureTogglePage.tsx b/public/app/features/explore/FeatureTogglePage.tsx index 54a00691aaa..a10df73f183 100644 --- a/public/app/features/explore/FeatureTogglePage.tsx +++ b/public/app/features/explore/FeatureTogglePage.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; export default function FeatureTogglePage() { const styles = useStyles2( diff --git a/public/app/features/folders/AccessControlFolderPermissions.tsx b/public/app/features/folders/AccessControlFolderPermissions.tsx index 7e5e1b4d3e5..325b69eb406 100644 --- a/public/app/features/folders/AccessControlFolderPermissions.tsx +++ b/public/app/features/folders/AccessControlFolderPermissions.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Permissions } from 'app/core/components/AccessControl'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; diff --git a/public/app/features/folders/FolderAlerting.tsx b/public/app/features/folders/FolderAlerting.tsx index 939676a751c..25785b74949 100644 --- a/public/app/features/folders/FolderAlerting.tsx +++ b/public/app/features/folders/FolderAlerting.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useAsync } from 'react-use'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; import { StoreState } from 'app/types'; diff --git a/public/app/features/folders/FolderLibraryPanelsPage.tsx b/public/app/features/folders/FolderLibraryPanelsPage.tsx index aa27c268d76..a645f34bb51 100644 --- a/public/app/features/folders/FolderLibraryPanelsPage.tsx +++ b/public/app/features/folders/FolderLibraryPanelsPage.tsx @@ -2,7 +2,8 @@ import React, { useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { useAsync } from 'react-use'; -import Page from '../../core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; + import { GrafanaRouteComponentProps } from '../../core/navigation/types'; import { getNavModel } from '../../core/selectors/navModel'; import { StoreState } from '../../types'; diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index ed69935fa4f..59f9263af87 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { Tooltip, Icon, Button } from '@grafana/ui'; import { SlideDown } from 'app/core/components/Animations/SlideDown'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import AddPermission from 'app/core/components/PermissionList/AddPermission'; import PermissionList from 'app/core/components/PermissionList/PermissionList'; import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo'; diff --git a/public/app/features/folders/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx index 16cf9a0392a..ecb47bb0de2 100644 --- a/public/app/features/folders/FolderSettingsPage.tsx +++ b/public/app/features/folders/FolderSettingsPage.tsx @@ -4,7 +4,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { Button, LegacyForms } from '@grafana/ui'; const { Input } = LegacyForms; import appEvents from 'app/core/app_events'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; import { StoreState } from 'app/types'; diff --git a/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap index 32186afa57a..1a334028040 100644 --- a/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap +++ b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should enable save button 1`] = ` -
    - + `; exports[`Render should render component 1`] = ` -
    - + `; diff --git a/public/app/features/folders/components/NewDashboardsFolder.tsx b/public/app/features/folders/components/NewDashboardsFolder.tsx index 72617c8fbfe..cd387a79817 100644 --- a/public/app/features/folders/components/NewDashboardsFolder.tsx +++ b/public/app/features/folders/components/NewDashboardsFolder.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Button, Input, Form, Field } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; import { StoreState } from 'app/types'; diff --git a/public/app/features/invites/SignupInvited.tsx b/public/app/features/invites/SignupInvited.tsx index 4d862d6377d..d12da280e07 100644 --- a/public/app/features/invites/SignupInvited.tsx +++ b/public/app/features/invites/SignupInvited.tsx @@ -3,7 +3,7 @@ import { useAsync } from 'react-use'; import { getBackendSrv } from '@grafana/runtime'; import { Button, Field, Form, Input } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { getConfig } from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; diff --git a/public/app/features/library-panels/LibraryPanelsPage.tsx b/public/app/features/library-panels/LibraryPanelsPage.tsx index 09342b0e5cf..af435231e7a 100644 --- a/public/app/features/library-panels/LibraryPanelsPage.tsx +++ b/public/app/features/library-panels/LibraryPanelsPage.tsx @@ -1,7 +1,8 @@ import React, { FC, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import Page from '../../core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; + import { GrafanaRouteComponentProps } from '../../core/navigation/types'; import { getNavModel } from '../../core/selectors/navModel'; import { StoreState } from '../../types'; diff --git a/public/app/features/live/pages/CloudAdminPage.tsx b/public/app/features/live/pages/CloudAdminPage.tsx index fe3face9db2..449c4c06c33 100644 --- a/public/app/features/live/pages/CloudAdminPage.tsx +++ b/public/app/features/live/pages/CloudAdminPage.tsx @@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react'; import { GrafanaTheme } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { useStyles } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { useNavModel } from 'app/core/hooks/useNavModel'; import { GrafanaCloudBackend } from './types'; diff --git a/public/app/features/live/pages/FeatureTogglePage.tsx b/public/app/features/live/pages/FeatureTogglePage.tsx index a1d3e635c5a..35b19509a05 100644 --- a/public/app/features/live/pages/FeatureTogglePage.tsx +++ b/public/app/features/live/pages/FeatureTogglePage.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { useNavModel } from 'app/core/hooks/useNavModel'; export default function FeatureTogglePage() { diff --git a/public/app/features/live/pages/LiveStatusPage.tsx b/public/app/features/live/pages/LiveStatusPage.tsx index 569853591af..485aacfe8d5 100644 --- a/public/app/features/live/pages/LiveStatusPage.tsx +++ b/public/app/features/live/pages/LiveStatusPage.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { useNavModel } from 'app/core/hooks/useNavModel'; export default function CloudAdminPage() { diff --git a/public/app/features/live/pages/PipelineAdminPage.tsx b/public/app/features/live/pages/PipelineAdminPage.tsx index 2face908dc6..7d33b38b595 100644 --- a/public/app/features/live/pages/PipelineAdminPage.tsx +++ b/public/app/features/live/pages/PipelineAdminPage.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState, ChangeEvent } from 'react'; import { getBackendSrv } from '@grafana/runtime'; import { Input } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { useNavModel } from 'app/core/hooks/useNavModel'; import { AddNewRule } from './AddNewRule'; diff --git a/public/app/features/manage-dashboards/DashboardImportPage.tsx b/public/app/features/manage-dashboards/DashboardImportPage.tsx index 1085441536a..365a7eabd1b 100644 --- a/public/app/features/manage-dashboards/DashboardImportPage.tsx +++ b/public/app/features/manage-dashboards/DashboardImportPage.tsx @@ -20,7 +20,7 @@ import { withTheme2, } from '@grafana/ui'; import appEvents from 'app/core/app_events'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { getNavModel } from 'app/core/selectors/navModel'; import { StoreState } from 'app/types'; diff --git a/public/app/features/manage-dashboards/SnapshotListPage.tsx b/public/app/features/manage-dashboards/SnapshotListPage.tsx index 5aa91777a34..ae16793ca0f 100644 --- a/public/app/features/manage-dashboards/SnapshotListPage.tsx +++ b/public/app/features/manage-dashboards/SnapshotListPage.tsx @@ -2,7 +2,7 @@ import React, { FC } from 'react'; import { MapStateToProps, connect } from 'react-redux'; import { NavModel } from '@grafana/data'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; import { StoreState } from 'app/types'; diff --git a/public/app/features/notifications/NotificationsPage.tsx b/public/app/features/notifications/NotificationsPage.tsx index 04b6b192a23..22c64301384 100644 --- a/public/app/features/notifications/NotificationsPage.tsx +++ b/public/app/features/notifications/NotificationsPage.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import Page from '../../core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; + import { GrafanaRouteComponentProps } from '../../core/navigation/types'; import { getNavModel } from '../../core/selectors/navModel'; import { StoreState } from '../../types'; diff --git a/public/app/features/org/NewOrgPage.tsx b/public/app/features/org/NewOrgPage.tsx index 34d2619110b..6a4fc75a490 100644 --- a/public/app/features/org/NewOrgPage.tsx +++ b/public/app/features/org/NewOrgPage.tsx @@ -2,7 +2,7 @@ import React, { FC } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Button, Input, Field, Form } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { getConfig } from 'app/core/config'; import { StoreState } from 'app/types'; diff --git a/public/app/features/org/OrgDetailsPage.tsx b/public/app/features/org/OrgDetailsPage.tsx index 5059ad5dd25..57c21a83a63 100644 --- a/public/app/features/org/OrgDetailsPage.tsx +++ b/public/app/features/org/OrgDetailsPage.tsx @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { NavModel } from '@grafana/data'; import { VerticalGroup } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences'; import { contextSrv } from 'app/core/core'; import { getNavModel } from 'app/core/selectors/navModel'; diff --git a/public/app/features/org/SelectOrgPage.tsx b/public/app/features/org/SelectOrgPage.tsx index 273f699cc65..2d39f619074 100644 --- a/public/app/features/org/SelectOrgPage.tsx +++ b/public/app/features/org/SelectOrgPage.tsx @@ -4,7 +4,7 @@ import { useEffectOnce } from 'react-use'; import { config } from '@grafana/runtime'; import { Button, HorizontalGroup } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { StoreState, UserOrg } from 'app/types'; import { getUserOrganizations, setUserOrganization } from './state/actions'; diff --git a/public/app/features/org/UserInvitePage.tsx b/public/app/features/org/UserInvitePage.tsx index 8afa065a0b7..097ead15a32 100644 --- a/public/app/features/org/UserInvitePage.tsx +++ b/public/app/features/org/UserInvitePage.tsx @@ -2,7 +2,7 @@ import React, { FC } from 'react'; import { connect } from 'react-redux'; import { NavModel } from '@grafana/data'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { getNavModel } from 'app/core/selectors/navModel'; import { StoreState } from 'app/types/store'; diff --git a/public/app/features/playlist/PlaylistEditPage.tsx b/public/app/features/playlist/PlaylistEditPage.tsx index c98ba7ee00d..24eb413ef12 100644 --- a/public/app/features/playlist/PlaylistEditPage.tsx +++ b/public/app/features/playlist/PlaylistEditPage.tsx @@ -4,7 +4,7 @@ import { connect, MapStateToProps } from 'react-redux'; import { NavModel } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { useStyles2 } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; import { StoreState } from 'app/types'; diff --git a/public/app/features/playlist/PlaylistNewPage.tsx b/public/app/features/playlist/PlaylistNewPage.tsx index c08d288ec99..e7ef84960b8 100644 --- a/public/app/features/playlist/PlaylistNewPage.tsx +++ b/public/app/features/playlist/PlaylistNewPage.tsx @@ -4,7 +4,7 @@ import { connect, MapStateToProps } from 'react-redux'; import { NavModel } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { useStyles2 } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; import { StoreState } from 'app/types'; diff --git a/public/app/features/playlist/PlaylistPage.tsx b/public/app/features/playlist/PlaylistPage.tsx index 62e85085d2f..e8a997f0232 100644 --- a/public/app/features/playlist/PlaylistPage.tsx +++ b/public/app/features/playlist/PlaylistPage.tsx @@ -4,7 +4,7 @@ import { useDebounce } from 'react-use'; import { NavModel } from '@grafana/data'; import { ConfirmModal } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import PageActionBar from 'app/core/components/PageActionBar/PageActionBar'; import { getNavModel } from 'app/core/selectors/navModel'; import { contextSrv } from 'app/core/services/context_srv'; diff --git a/public/app/features/plugins/admin/routes.ts b/public/app/features/plugins/admin/routes.ts index 02a54c1c6c8..53a573ccbf4 100644 --- a/public/app/features/plugins/admin/routes.ts +++ b/public/app/features/plugins/admin/routes.ts @@ -7,16 +7,19 @@ import { PluginAdminRoutes } from './types'; const DEFAULT_ROUTES = [ { path: '/plugins', + navId: 'plugins', routeName: PluginAdminRoutes.Home, component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './pages/Browse')), }, { path: '/plugins/browse', + navId: 'plugins', routeName: PluginAdminRoutes.Browse, component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './pages/Browse')), }, { path: '/plugins/:pluginId/', + navId: 'plugins', routeName: PluginAdminRoutes.Details, component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginPage" */ './pages/PluginDetails')), }, @@ -25,16 +28,19 @@ const DEFAULT_ROUTES = [ const ADMIN_ROUTES = [ { path: '/admin/plugins', + navId: 'admin-plugins', routeName: PluginAdminRoutes.HomeAdmin, component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './pages/Browse')), }, { path: '/admin/plugins/browse', + navId: 'admin-plugins', routeName: PluginAdminRoutes.BrowseAdmin, component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './pages/Browse')), }, { path: '/admin/plugins/:pluginId/', + navId: 'admin-plugins', routeName: PluginAdminRoutes.DetailsAdmin, component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginPage" */ './pages/PluginDetails')), }, diff --git a/public/app/features/plugins/components/AppPluginLoader.tsx b/public/app/features/plugins/components/AppPluginLoader.tsx index fe263276930..37516513f1a 100644 --- a/public/app/features/plugins/components/AppPluginLoader.tsx +++ b/public/app/features/plugins/components/AppPluginLoader.tsx @@ -3,7 +3,7 @@ import { useLocation, useParams } from 'react-router-dom'; import { NavModel } from '@grafana/data'; import { getWarningNav } from 'app/angular/services/nav_model_srv'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { useImportAppPlugin } from '../hooks/useImportAppPlugin'; @@ -23,13 +23,13 @@ export const AppPluginLoader = ({ id, basePath }: AppPluginLoaderProps) => { const { pathname } = useLocation(); if (error) { - return ; + return ; } return ( <> {loading && } - {nav && } + {nav && } {!loading && plugin && plugin.root && ( ({ getBackendSrv: () => ({ post: postMock, @@ -26,6 +27,7 @@ jest.mock('@grafana/runtime', () => ({ licenseUrl: '', }, appSubUrl: '', + featureToggles: {}, }, })); @@ -52,7 +54,7 @@ const setup = (propOverrides: Partial) => { Object.assign(props, propOverrides); - render(); + render(); }; describe('ServiceAccountCreatePage tests', () => { diff --git a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx index dc6bf19741d..5d2faca95f7 100644 --- a/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountCreatePage.tsx @@ -1,34 +1,24 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { connect } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { NavModel } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import { Form, Button, Input, Field } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { fetchBuiltinRoles, fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api'; import { contextSrv } from 'app/core/core'; import { AccessControlAction, OrgRole, Role, ServiceAccountCreateApiResponse, ServiceAccountDTO } from 'app/types'; -import { getNavModel } from '../../core/selectors/navModel'; -import { StoreState } from '../../types'; import { OrgRolePicker } from '../admin/OrgRolePicker'; -export interface Props { - navModel: NavModel; -} - -const mapStateToProps = (state: StoreState) => ({ - navModel: getNavModel(state.navIndex, 'serviceaccounts'), -}); +export interface Props {} const createServiceAccount = async (sa: ServiceAccountDTO) => getBackendSrv().post('/api/serviceaccounts/', sa); const updateServiceAccount = async (id: number, sa: ServiceAccountDTO) => getBackendSrv().patch(`/api/serviceaccounts/${id}`, sa); -export const ServiceAccountCreatePageUnconnected = ({ navModel }: Props): JSX.Element => { +export const ServiceAccountCreatePage = ({}: Props): JSX.Element => { const [roleOptions, setRoleOptions] = useState([]); const [builtinRoles, setBuiltinRoles] = useState<{ [key: string]: Role[] }>({}); const [pendingRoles, setPendingRoles] = useState([]); @@ -109,7 +99,7 @@ export const ServiceAccountCreatePageUnconnected = ({ navModel }: Props): JSX.El }; return ( - +

    Create service account

    @@ -152,4 +142,4 @@ export const ServiceAccountCreatePageUnconnected = ({ navModel }: Props): JSX.El ); }; -export default connect(mapStateToProps)(ServiceAccountCreatePageUnconnected); +export default ServiceAccountCreatePage; diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx index 142b4045507..d13d484cdc4 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.test.tsx @@ -23,14 +23,6 @@ const setup = (propOverrides: Partial) => { const updateServiceAccountMock = jest.fn(); const props: Props = { - navModel: { - main: { - text: 'Configuration', - }, - node: { - text: 'Service accounts', - }, - }, serviceAccount: {} as ServiceAccountDTO, tokens: [], builtInRoles: {}, diff --git a/public/app/features/serviceaccounts/ServiceAccountPage.tsx b/public/app/features/serviceaccounts/ServiceAccountPage.tsx index 82fed275471..75d4d9add87 100644 --- a/public/app/features/serviceaccounts/ServiceAccountPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountPage.tsx @@ -2,12 +2,11 @@ import { css } from '@emotion/css'; import React, { useEffect, useState } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { getTimeZone, GrafanaTheme2, NavModel } from '@grafana/data'; +import { getTimeZone, GrafanaTheme2 } from '@grafana/data'; import { Button, ConfirmModal, IconButton, useStyles2 } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { getNavModel } from 'app/core/selectors/navModel'; import { AccessControlAction, ApiKey, Role, ServiceAccountDTO, StoreState } from 'app/types'; import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal'; @@ -24,7 +23,6 @@ import { } from './state/actionsServiceAccountPage'; interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { - navModel: NavModel; serviceAccount?: ServiceAccountDTO; tokens: ApiKey[]; isLoading: boolean; @@ -34,7 +32,6 @@ interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> { function mapStateToProps(state: StoreState) { return { - navModel: getNavModel(state.navIndex, 'serviceaccounts'), serviceAccount: state.serviceAccountProfile.serviceAccount, tokens: state.serviceAccountProfile.tokens, isLoading: state.serviceAccountProfile.isLoading, @@ -58,7 +55,6 @@ const connector = connect(mapStateToProps, mapDispatchToProps); export type Props = OwnProps & ConnectedProps; export const ServiceAccountPageUnconnected = ({ - navModel, match, serviceAccount, tokens, @@ -131,7 +127,7 @@ export const ServiceAccountPageUnconnected = ({ }; return ( - + {serviceAccount && (
    diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx index e2222f4f628..59a66ade42a 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.test.tsx @@ -26,14 +26,6 @@ const setup = (propOverrides: Partial) => { const getApiKeysMigrationInfoMock = jest.fn(); const closeApiKeysMigrationInfoMock = jest.fn(); const props: Props = { - navModel: { - main: { - text: 'Configuration', - }, - node: { - text: 'Service accounts', - }, - }, builtInRoles: {}, isLoading: false, page: 0, diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx index 6043c9b5e8e..1d87856501a 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx @@ -6,10 +6,9 @@ import { connect, ConnectedProps } from 'react-redux'; import { GrafanaTheme2, OrgRole } from '@grafana/data'; import { Alert, ConfirmModal, FilterInput, Icon, LinkButton, RadioButtonGroup, Tooltip, useStyles2 } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; import { contextSrv } from 'app/core/core'; -import { getNavModel } from 'app/core/selectors/navModel'; import { StoreState, ServiceAccountDTO, AccessControlAction, ServiceAccountStateFilter } from 'app/types'; import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal'; @@ -33,7 +32,6 @@ export type Props = OwnProps & ConnectedProps; function mapStateToProps(state: StoreState) { return { - navModel: getNavModel(state.navIndex, 'serviceaccounts'), ...state.serviceAccounts, }; } @@ -54,7 +52,6 @@ const mapDispatchToProps = { const connector = connect(mapStateToProps, mapDispatchToProps); export const ServiceAccountsListPageUnconnected = ({ - navModel, serviceAccounts, isLoading, roleOptions, @@ -169,7 +166,7 @@ export const ServiceAccountsListPageUnconnected = ({ }; return ( - + {apiKeysMigrated && showApiKeysMigrationInfo && ( { jest.clearAllMocks(); @@ -14,23 +14,12 @@ const mockPost = jest.fn(() => { return Promise.resolve({}); }); -jest.mock('@grafana/runtime', () => ({ - getBackendSrv: () => { - return { - post: mockPost, - }; - }, - config: { - buildInfo: {}, - licenseInfo: {}, - }, -})); +setBackendSrv({ + post: mockPost, +} as any as BackendSrv); const setup = () => { - const props: Props = { - navModel: { node: {}, main: {} } as NavModel, - }; - return render(); + return render(); }; describe('Create team', () => { diff --git a/public/app/features/teams/CreateTeam.tsx b/public/app/features/teams/CreateTeam.tsx index 1c35b134e4b..b0fabfcef5f 100644 --- a/public/app/features/teams/CreateTeam.tsx +++ b/public/app/features/teams/CreateTeam.tsx @@ -1,24 +1,16 @@ import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; -import { NavModel } from '@grafana/data'; import { getBackendSrv, locationService } from '@grafana/runtime'; import { Button, Form, Field, Input, FieldSet } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { getNavModel } from 'app/core/selectors/navModel'; -import { StoreState } from 'app/types'; - -export interface Props { - navModel: NavModel; -} interface TeamDTO { name: string; email: string; } -export class CreateTeam extends PureComponent { +export class CreateTeam extends PureComponent { create = async (formModel: TeamDTO) => { const result = await getBackendSrv().post('/api/teams', formModel); if (result.teamId) { @@ -27,10 +19,8 @@ export class CreateTeam extends PureComponent { } }; render() { - const { navModel } = this.props; - return ( - + {({ register, errors }) => ( @@ -58,10 +48,4 @@ export class CreateTeam extends PureComponent { } } -function mapStateToProps(state: StoreState) { - return { - navModel: getNavModel(state.navIndex, 'teams'), - }; -} - -export default connect(mapStateToProps)(CreateTeam); +export default CreateTeam; diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index 9575b0c1ed0..e81bd56ef65 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { mockToolkitActionCreator } from 'test/core/redux/mocks'; -import { NavModel } from '@grafana/data'; import { contextSrv, User } from 'app/core/services/context_srv'; import { OrgRole, Team } from '../../types'; @@ -20,14 +19,6 @@ jest.mock('app/core/config', () => { const setup = (propOverrides?: object) => { const props: Props = { - navModel: { - main: { - text: 'Configuration', - }, - node: { - text: 'Team List', - }, - } as NavModel, teams: [] as Team[], loadTeams: jest.fn(), deleteTeam: jest.fn(), diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 7f2cda0992b..e744fdd7993 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,13 +1,11 @@ import React, { PureComponent } from 'react'; -import { NavModel } from '@grafana/data'; import { DeleteButton, LinkButton, FilterInput, VerticalGroup, HorizontalGroup, Pagination } from '@grafana/ui'; import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { config } from 'app/core/config'; -import { getNavModel } from 'app/core/selectors/navModel'; import { contextSrv, User } from 'app/core/services/context_srv'; import { AccessControlAction, Role, StoreState, Team } from 'app/types'; @@ -20,7 +18,6 @@ import { getSearchQuery, getTeams, getTeamsCount, getTeamsSearchPage, isPermissi const pageLimit = 30; export interface Props { - navModel: NavModel; teams: Team[]; searchQuery: string; searchPage: number; @@ -224,10 +221,10 @@ export class TeamList extends PureComponent { } render() { - const { hasFetched, navModel } = this.props; + const { hasFetched } = this.props; return ( - + {this.renderList()} ); @@ -236,7 +233,6 @@ export class TeamList extends PureComponent { function mapStateToProps(state: StoreState) { return { - navModel: getNavModel(state.navIndex, 'teams'), teams: getTeams(state.teams), searchQuery: getSearchQuery(state.teams), searchPage: getTeamsSearchPage(state.teams), diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx index 12d024e0fb9..7184ec3931b 100644 --- a/public/app/features/teams/TeamPages.test.tsx +++ b/public/app/features/teams/TeamPages.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { Provider } from 'react-redux'; -import { NavModel, createTheme } from '@grafana/data'; +import { createTheme } from '@grafana/data'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { User } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; @@ -68,7 +68,7 @@ const setup = (propOverrides?: object) => { }, } as any, }), - navModel: { node: {}, main: {} } as NavModel, + pageNav: { text: 'Cool team ' }, teamId: 1, loadTeam: jest.fn(), loadTeamMembers: jest.fn(), diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index f72b7f0b532..6c0968237c6 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -2,10 +2,10 @@ import { includes } from 'lodash'; import React, { PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { NavModel } from '@grafana/data'; +import { NavModelItem } from '@grafana/data'; import { featureEnabled } from '@grafana/runtime'; import { Themeable2, withTheme2 } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { UpgradeBox } from 'app/core/components/Upgrade/UpgradeBox'; import config from 'app/core/config'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; @@ -51,11 +51,11 @@ function mapStateToProps(state: StoreState, props: OwnProps) { } const pageName = props.match.params.page ?? defaultPage; const teamLoadingNav = getTeamLoadingNav(pageName as string); - const navModel = getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav); + const pageNav = getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav).main; const members = getTeamMembers(state.team); return { - navModel, + pageNav, teamId: teamId, pageName: pageName, team, @@ -118,20 +118,20 @@ export class TeamPages extends PureComponent { return text1.toLocaleLowerCase() === text2.toLocaleLowerCase(); }; - hideTabsFromNonTeamAdmin = (navModel: NavModel, isSignedInUserTeamAdmin: boolean) => { + hideTabsFromNonTeamAdmin = (pageNav: NavModelItem, isSignedInUserTeamAdmin: boolean) => { if (contextSrv.accessControlEnabled()) { - return navModel; + return pageNav; } - if (!isSignedInUserTeamAdmin && navModel.main && navModel.main.children) { - navModel.main.children + if (!isSignedInUserTeamAdmin && pageNav && pageNav.children) { + pageNav.children .filter((navItem) => !this.textsAreEqual(navItem.text, PageTypes.Members)) .map((navItem) => { navItem.hideFromTabs = true; }); } - return navModel; + return pageNav; }; renderPage(isSignedInUserTeamAdmin: boolean): React.ReactNode { @@ -183,11 +183,11 @@ export class TeamPages extends PureComponent { } render() { - const { team, navModel, members, editorsCanAdmin, signedInUser } = this.props; + const { team, pageNav, members, editorsCanAdmin, signedInUser } = this.props; const isTeamAdmin = isSignedInUserTeamAdmin({ members, editorsCanAdmin, signedInUser }); return ( - + {team && Object.keys(team).length !== 0 && this.renderPage(isTeamAdmin)} diff --git a/public/app/features/teams/TeamPermissions.tsx b/public/app/features/teams/TeamPermissions.tsx index 51a17bc388e..2d4d23386e8 100644 --- a/public/app/features/teams/TeamPermissions.tsx +++ b/public/app/features/teams/TeamPermissions.tsx @@ -18,7 +18,7 @@ const TeamPermissions = (props: TeamPermissionsProps) => { return ( ({ const setup = (propOverrides?: object) => { const store = configureStore(); const props: Props = { - navModel: { - main: { - text: 'Configuration', - }, - node: { - text: 'Users', - }, - } as NavModel, users: [] as OrgUser[], invitees: [] as Invitee[], searchQuery: '', diff --git a/public/app/features/users/UsersListPage.tsx b/public/app/features/users/UsersListPage.tsx index 6205a86a7c1..7cab296441d 100644 --- a/public/app/features/users/UsersListPage.tsx +++ b/public/app/features/users/UsersListPage.tsx @@ -3,9 +3,8 @@ import { connect, ConnectedProps } from 'react-redux'; import { renderMarkdown } from '@grafana/data'; import { HorizontalGroup, Pagination, VerticalGroup } from '@grafana/ui'; -import Page from 'app/core/components/Page/Page'; +import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; -import { getNavModel } from 'app/core/selectors/navModel'; import { OrgUser, OrgRole, StoreState } from 'app/types'; import InviteesTable from '../invites/InviteesTable'; @@ -21,7 +20,6 @@ import { getUsers, getUsersSearchQuery, getUsersSearchPage } from './state/selec function mapStateToProps(state: StoreState) { const searchQuery = getUsersSearchQuery(state.users); return { - navModel: getNavModel(state.navIndex, 'users'), users: getUsers(state.users), searchQuery: getUsersSearchQuery(state.users), searchPage: getUsersSearchPage(state.users), @@ -125,11 +123,11 @@ export class UsersListPage extends PureComponent { } render() { - const { navModel, hasFetched } = this.props; + const { hasFetched } = this.props; const externalUserMngInfoHtml = this.externalUserMngInfoHtml; return ( - + <> diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 848b5cf8e2f..35015f93c54 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -33,7 +33,6 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/d/:uid/:slug?', - navId: 'dashboards', pageClass: 'page-dashboard', routeName: DashboardRoutes.Normal, component: SafeDynamicImport( @@ -91,7 +90,6 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/datasources', - navId: 'datasources', component: SafeDynamicImport( () => import(/* webpackChunkName: "DataSourcesListPage"*/ 'app/features/datasources/DataSourcesListPage') ), @@ -193,7 +191,6 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/org/users', - navId: 'users', component: SafeDynamicImport( () => import(/* webpackChunkName: "UsersListPage" */ 'app/features/users/UsersListPage') ), @@ -206,7 +203,6 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/org/apikeys', - navId: 'apikeys', roles: () => contextSrv.evaluatePermission(() => ['Admin'], [AccessControlAction.ActionAPIKeysRead]), component: SafeDynamicImport( () => import(/* webpackChunkName: "ApiKeysPage" */ 'app/features/api-keys/ApiKeysPage') @@ -235,7 +231,6 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: '/org/teams', - navId: 'teams', roles: () => contextSrv.evaluatePermission( () => (config.editorsCanAdmin ? ['Editor', 'Admin'] : ['Admin']), @@ -328,7 +323,7 @@ export function getAppRoutes(): RouteDescriptor[] { path: '/login', component: LoginPage, pageClass: 'login-page sidemenu-hidden', - navHidden: true, + chromeless: true, }, { path: '/invite/:code', @@ -336,7 +331,7 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "SignupInvited" */ 'app/features/invites/SignupInvited') ), pageClass: 'sidemenu-hidden', - navHidden: true, + chromeless: true, }, { path: '/verify', @@ -346,7 +341,7 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName "VerifyEmailPage"*/ 'app/core/components/Signup/VerifyEmailPage') ), pageClass: 'login-page sidemenu-hidden', - navHidden: true, + chromeless: true, }, { path: '/signup', @@ -354,12 +349,12 @@ export function getAppRoutes(): RouteDescriptor[] { ? () => : SafeDynamicImport(() => import(/* webpackChunkName "SignupPage"*/ 'app/core/components/Signup/SignupPage')), pageClass: 'sidemenu-hidden login-page', - navHidden: true, + chromeless: true, }, { path: '/user/password/send-reset-email', pageClass: 'sidemenu-hidden', - navHidden: true, + chromeless: true, component: SafeDynamicImport( () => import(/* webpackChunkName: "SendResetMailPage" */ 'app/core/components/ForgottenPassword/SendResetMailPage') @@ -374,7 +369,7 @@ export function getAppRoutes(): RouteDescriptor[] { ) ), pageClass: 'sidemenu-hidden login-page', - navHidden: true, + chromeless: true, }, { path: '/dashboard/snapshots',