From 8e5ec28c23a5e142e7cde779ce00db4b148da13c Mon Sep 17 00:00:00 2001 From: Matej Kubinec <32638572+matejkubinec@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:29:13 +0200 Subject: [PATCH] PMM-13127 Add tabs back in for inventory and settings (#767) * PMM-13127 Add tabs back in for inventory and settings * PMM-13127 Refactor * PMM-13127 Remove custom mega menu item * PMM-13127 Fix loading --- public/app/percona/inventory/Tabs/Nodes.tsx | 10 +- .../app/percona/inventory/Tabs/Services.tsx | 10 +- .../settings/components/Advanced/Advanced.tsx | 10 +- .../MetricsResolution/MetricsResolution.tsx | 10 +- .../settings/components/Platform/Platform.tsx | 10 +- .../settings/components/SSHKey/SSHKey.tsx | 10 +- .../MegaMenuItem/FeatureHighlight.tsx | 36 ---- .../components/MegaMenuItem/MegaMenuItem.tsx | 162 --------------- .../MegaMenuItem/MegaMenuItemText.tsx | 123 ------------ .../shared/components/MegaMenuItem/index.ts | 1 - .../components/MegaMenuItem/utils.test.ts | 188 ------------------ .../shared/components/MegaMenuItem/utils.ts | 144 -------------- .../components/TabbedPage/TabbedPage.hooks.ts | 7 + .../TabbedPage/TabbedPage.styles.ts | 60 ++++++ .../components/TabbedPage/TabbedPage.tsx | 36 ++++ .../components/TabbedPage/TabbedPage.types.ts | 6 + .../TabbedPage/TabbedPageContents.styles.ts | 10 + .../TabbedPage/TabbedPageContents.tsx | 12 ++ .../TabbedPage/TabbedPageSelect.tsx | 33 +++ .../TabbedPage/TabbedPageSelect.types.ts | 6 + .../shared/components/TabbedPage/index.ts | 2 + 21 files changed, 202 insertions(+), 684 deletions(-) delete mode 100644 public/app/percona/shared/components/MegaMenuItem/FeatureHighlight.tsx delete mode 100644 public/app/percona/shared/components/MegaMenuItem/MegaMenuItem.tsx delete mode 100644 public/app/percona/shared/components/MegaMenuItem/MegaMenuItemText.tsx delete mode 100644 public/app/percona/shared/components/MegaMenuItem/index.ts delete mode 100644 public/app/percona/shared/components/MegaMenuItem/utils.test.ts delete mode 100644 public/app/percona/shared/components/MegaMenuItem/utils.ts create mode 100644 public/app/percona/shared/components/TabbedPage/TabbedPage.hooks.ts create mode 100644 public/app/percona/shared/components/TabbedPage/TabbedPage.styles.ts create mode 100644 public/app/percona/shared/components/TabbedPage/TabbedPage.tsx create mode 100644 public/app/percona/shared/components/TabbedPage/TabbedPage.types.ts create mode 100644 public/app/percona/shared/components/TabbedPage/TabbedPageContents.styles.ts create mode 100644 public/app/percona/shared/components/TabbedPage/TabbedPageContents.tsx create mode 100644 public/app/percona/shared/components/TabbedPage/TabbedPageSelect.tsx create mode 100644 public/app/percona/shared/components/TabbedPage/TabbedPageSelect.types.ts create mode 100644 public/app/percona/shared/components/TabbedPage/index.ts diff --git a/public/app/percona/inventory/Tabs/Nodes.tsx b/public/app/percona/inventory/Tabs/Nodes.tsx index b35e5f476c362..52a1ec7a38e0f 100644 --- a/public/app/percona/inventory/Tabs/Nodes.tsx +++ b/public/app/percona/inventory/Tabs/Nodes.tsx @@ -5,13 +5,13 @@ import { Row } from 'react-table'; import { AppEvents } from '@grafana/data'; import { Badge, Button, HorizontalGroup, Icon, Link, Modal, TagList, useStyles2 } from '@grafana/ui'; -import { Page } from 'app/core/components/Page/Page'; import { CheckboxField } from 'app/percona/shared/components/Elements/Checkbox'; import { DetailsRow } from 'app/percona/shared/components/Elements/DetailsRow/DetailsRow'; import { FeatureLoader } from 'app/percona/shared/components/Elements/FeatureLoader'; import { Action } from 'app/percona/shared/components/Elements/MultipleActions'; import { ExtendedColumn, FilterFieldTypes, Table } from 'app/percona/shared/components/Elements/Table'; import { FormElement } from 'app/percona/shared/components/Form'; +import { TabbedPage, TabbedPageContents } from 'app/percona/shared/components/TabbedPage'; import { useCancelToken } from 'app/percona/shared/components/hooks/cancelToken.hook'; import { usePerconaNavModel } from 'app/percona/shared/components/hooks/perconaNavModel'; import { RemoveNodeParams } from 'app/percona/shared/core/reducers/nodes'; @@ -299,8 +299,8 @@ export const NodesTab = () => { }, []); return ( - - + +
- )} -
- {showExpandButton && sectionExpanded && ( -
    - {linkHasChildren(link) ? ( - link.children - .filter((childLink) => !childLink.isCreateAction) - .map((childLink) => ( - - )) - ) : ( -
    {link.emptyMessage}
    - )} -
- )} - - ); -} - -const getStyles = (theme: GrafanaTheme2) => ({ - children: css({ - display: 'flex', - listStyleType: 'none', - flexDirection: 'column', - }), - collapsibleSectionWrapper: css({ - alignItems: 'center', - display: 'flex', - }), - collapseButton: css({ - color: theme.colors.text.disabled, - padding: theme.spacing(0, 0.5), - marginRight: theme.spacing(1), - }), - emptyMessage: css({ - color: theme.colors.text.secondary, - fontStyle: 'italic', - padding: theme.spacing(1, 1.5, 1, 7), - }), - iconWrapper: css({ - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - }), - labelWrapper: css({ - display: 'grid', - fontSize: theme.typography.pxToRem(14), - gridAutoFlow: 'column', - gridTemplateColumns: `${theme.spacing(7)} auto`, - alignItems: 'center', - fontWeight: theme.typography.fontWeightMedium, - }), - listItem: css({ - flex: 1, - }), - isActive: css({ - color: theme.colors.text.primary, - - '&::before': { - display: 'block', - content: '" "', - height: theme.spacing(3), - position: 'absolute', - left: theme.spacing(1), - top: '50%', - transform: 'translateY(-50%)', - width: theme.spacing(0.5), - borderRadius: theme.shape.radius.default, - backgroundImage: theme.colors.gradients.brandVertical, - }, - }), - // @PERCONA - iconSpacing: css({ - marginRight: theme.spacing(1), - }), -}); - -function linkHasChildren(link: NavModelItem): link is NavModelItem & { children: NavModelItem[] } { - return Boolean(link.children && link.children.length > 0); -} diff --git a/public/app/percona/shared/components/MegaMenuItem/MegaMenuItemText.tsx b/public/app/percona/shared/components/MegaMenuItem/MegaMenuItemText.tsx deleted file mode 100644 index 7ed3bee3c3f91..0000000000000 --- a/public/app/percona/shared/components/MegaMenuItem/MegaMenuItemText.tsx +++ /dev/null @@ -1,123 +0,0 @@ -// @PERCONA - forked from grafana 10.2.0 (no modifications) - -import { css, cx } from '@emotion/css'; -import React from 'react'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { Icon, Link, useTheme2 } from '@grafana/ui'; - -export interface Props { - children: React.ReactNode; - isActive?: boolean; - onClick?: () => void; - target?: HTMLAnchorElement['target']; - url?: string; -} - -export function MegaMenuItemText({ children, isActive, onClick, target, url }: Props) { - const theme = useTheme2(); - const styles = getStyles(theme, isActive); - - const linkContent = ( -
- {children} - - {target === '_blank' && ( - - )} -
- ); - - let element = ( - - ); - - if (url) { - element = - !target && url.startsWith('/') ? ( - - {linkContent} - - ) : ( - - {linkContent} - - ); - } - - return
{element}
; -} - -MegaMenuItemText.displayName = 'MegaMenuItemText'; - -const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive']) => ({ - button: css({ - backgroundColor: 'unset', - borderStyle: 'unset', - }), - linkContent: css({ - alignItems: 'center', - display: 'flex', - gap: '0.5rem', - height: '100%', - width: '100%', - }), - externalLinkIcon: css({ - color: theme.colors.text.secondary, - }), - element: css({ - alignItems: 'center', - boxSizing: 'border-box', - position: 'relative', - color: isActive ? theme.colors.text.primary : theme.colors.text.secondary, - padding: theme.spacing(1, 1, 1, 0), - width: '100%', - '&:hover, &:focus-visible': { - textDecoration: 'underline', - color: theme.colors.text.primary, - }, - '&:focus-visible': { - boxShadow: 'none', - outline: `2px solid ${theme.colors.primary.main}`, - outlineOffset: '-2px', - transition: 'none', - }, - '&::before': { - display: isActive ? 'block' : 'none', - content: '" "', - height: theme.spacing(3), - position: 'absolute', - left: theme.spacing(1), - top: '50%', - transform: 'translateY(-50%)', - width: theme.spacing(0.5), - borderRadius: theme.shape.radius.default, - backgroundImage: theme.colors.gradients.brandVertical, - }, - }), - wrapper: css({ - boxSizing: 'border-box', - position: 'relative', - display: 'flex', - width: '100%', - }), -}); diff --git a/public/app/percona/shared/components/MegaMenuItem/index.ts b/public/app/percona/shared/components/MegaMenuItem/index.ts deleted file mode 100644 index a2e71f384f6d5..0000000000000 --- a/public/app/percona/shared/components/MegaMenuItem/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { MegaMenuItem } from './MegaMenuItem'; diff --git a/public/app/percona/shared/components/MegaMenuItem/utils.test.ts b/public/app/percona/shared/components/MegaMenuItem/utils.test.ts deleted file mode 100644 index 11192451482ed..0000000000000 --- a/public/app/percona/shared/components/MegaMenuItem/utils.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -// @PERCONA - forked from grafana 10.2.0 (no modifications) - -import { GrafanaConfig, locationUtil, NavModelItem } from '@grafana/data'; -import { ContextSrv, setContextSrv } from 'app/core/services/context_srv'; - -import { enrichHelpItem, getActiveItem, isMatchOrChildMatch } from './utils'; - -jest.mock('app/core/app_events', () => ({ - publish: jest.fn(), -})); - -describe('enrichConfigItems', () => { - let mockHelpNode: NavModelItem; - - beforeEach(() => { - mockHelpNode = { - id: 'help', - text: 'Help', - }; - }); - - it('enhances the help node with extra child links', () => { - const contextSrv = new ContextSrv(); - setContextSrv(contextSrv); - const helpNode = enrichHelpItem(mockHelpNode); - expect(helpNode!.children).toContainEqual( - expect.objectContaining({ - text: 'Documentation', - }) - ); - expect(helpNode!.children).toContainEqual( - expect.objectContaining({ - text: 'Support', - }) - ); - expect(helpNode!.children).toContainEqual( - expect.objectContaining({ - text: 'Community', - }) - ); - expect(helpNode!.children).toContainEqual( - expect.objectContaining({ - text: 'Keyboard shortcuts', - }) - ); - }); -}); - -describe('isMatchOrChildMatch', () => { - const mockChild: NavModelItem = { - text: 'Child', - url: '/dashboards/child', - }; - const mockItemToCheck: NavModelItem = { - text: 'Dashboards', - url: '/dashboards', - children: [mockChild], - }; - - it('returns true if the itemToCheck is an exact match with the searchItem', () => { - const searchItem = mockItemToCheck; - expect(isMatchOrChildMatch(mockItemToCheck, searchItem)).toBe(true); - }); - - it('returns true if the itemToCheck has a child that matches the searchItem', () => { - const searchItem = mockChild; - expect(isMatchOrChildMatch(mockItemToCheck, searchItem)).toBe(true); - }); - - it('returns false otherwise', () => { - const searchItem: NavModelItem = { - text: 'No match', - url: '/noMatch', - }; - expect(isMatchOrChildMatch(mockItemToCheck, searchItem)).toBe(false); - }); -}); - -describe('getActiveItem', () => { - const mockNavTree: NavModelItem[] = [ - { - text: 'Item', - url: '/item', - }, - { - text: 'Item with query param', - url: '/itemWithQueryParam?foo=bar', - }, - { - text: 'Item after subpath', - url: '/subUrl/itemAfterSubpath', - }, - { - text: 'Item with children', - url: '/itemWithChildren', - children: [ - { - text: 'Child', - url: '/child', - }, - ], - }, - { - text: 'Alerting item', - url: '/alerting/list', - }, - { - text: 'Base', - url: '/', - }, - { - text: 'Starred', - url: '/dashboards?starred', - id: 'starred', - }, - { - text: 'Dashboards', - url: '/dashboards', - }, - { - text: 'More specific dashboard', - url: '/d/moreSpecificDashboard', - }, - ]; - beforeEach(() => { - locationUtil.initialize({ - config: { appSubUrl: '/subUrl' } as GrafanaConfig, - getVariablesUrlParams: () => ({}), - getTimeRangeForUrl: () => ({ from: 'now-7d', to: 'now' }), - }); - }); - - it('returns an exact match at the top level', () => { - const mockPathName = '/item'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Item', - url: '/item', - }); - }); - - it('returns an exact match ignoring root subpath', () => { - const mockPathName = '/itemAfterSubpath'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Item after subpath', - url: '/subUrl/itemAfterSubpath', - }); - }); - - it('returns an exact match ignoring query params', () => { - const mockPathName = '/itemWithQueryParam?bar=baz'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Item with query param', - url: '/itemWithQueryParam?foo=bar', - }); - }); - - it('returns an exact child match', () => { - const mockPathName = '/child'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Child', - url: '/child', - }); - }); - - it('returns the alerting link if the pathname is an alert notification', () => { - const mockPathName = '/alerting/notification/foo'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Alerting item', - url: '/alerting/list', - }); - }); - - it('returns the dashboards route link if the pathname starts with /d/', () => { - const mockPathName = '/d/foo'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'Dashboards', - url: '/dashboards', - }); - }); - - it('returns a more specific link if one exists', () => { - const mockPathName = '/d/moreSpecificDashboard'; - expect(getActiveItem(mockNavTree, mockPathName)).toEqual({ - text: 'More specific dashboard', - url: '/d/moreSpecificDashboard', - }); - }); -}); diff --git a/public/app/percona/shared/components/MegaMenuItem/utils.ts b/public/app/percona/shared/components/MegaMenuItem/utils.ts deleted file mode 100644 index de68e250d7048..0000000000000 --- a/public/app/percona/shared/components/MegaMenuItem/utils.ts +++ /dev/null @@ -1,144 +0,0 @@ -// @PERCONA - forked from grafana 10.2.0 (no modifications) - -import { locationUtil, NavModelItem } from '@grafana/data'; -import { config, reportInteraction } from '@grafana/runtime'; -import appEvents from 'app/core/app_events'; -import { getFooterLinks } from 'app/core/components/Footer/Footer'; -import { HelpModal } from 'app/core/components/help/HelpModal'; -import { t } from 'app/core/internationalization'; - -import { ShowModalReactEvent } from '../../../../types/events'; - -export const enrichHelpItem = (helpItem: NavModelItem) => { - let menuItems = helpItem.children || []; - - if (helpItem.id === 'help') { - const onOpenShortcuts = () => { - appEvents.publish(new ShowModalReactEvent({ component: HelpModal })); - }; - helpItem.children = [ - ...menuItems, - ...getFooterLinks(), - ...getEditionAndUpdateLinks(), - { - id: 'keyboard-shortcuts', - text: t('nav.help/keyboard-shortcuts', 'Keyboard shortcuts'), - icon: 'keyboard', - onClick: onOpenShortcuts, - }, - ]; - } - return helpItem; -}; - -export const enrichWithInteractionTracking = (item: NavModelItem, expandedState: boolean) => { - // creating a new object here to not mutate the original item object - const newItem = { ...item }; - const onClick = newItem.onClick; - newItem.onClick = () => { - reportInteraction('grafana_navigation_item_clicked', { - path: newItem.url ?? newItem.id, - state: expandedState ? 'expanded' : 'collapsed', - }); - onClick?.(); - }; - if (newItem.children) { - newItem.children = newItem.children.map((item) => enrichWithInteractionTracking(item, expandedState)); - } - return newItem; -}; - -export const isMatchOrChildMatch = (itemToCheck: NavModelItem, searchItem?: NavModelItem) => { - return Boolean(itemToCheck === searchItem || hasChildMatch(itemToCheck, searchItem)); -}; - -export const hasChildMatch = (itemToCheck: NavModelItem, searchItem?: NavModelItem): boolean => { - return Boolean( - itemToCheck.children?.some((child) => { - if (child === searchItem) { - return true; - } else { - return hasChildMatch(child, searchItem); - } - }) - ); -}; - -const stripQueryParams = (url?: string) => { - return url?.split('?')[0] ?? ''; -}; - -const isBetterMatch = (newMatch: NavModelItem, currentMatch?: NavModelItem) => { - const currentMatchUrl = stripQueryParams(currentMatch?.url); - const newMatchUrl = stripQueryParams(newMatch.url); - return newMatchUrl && newMatchUrl.length > currentMatchUrl?.length; -}; - -export const getActiveItem = ( - navTree: NavModelItem[], - pathname: string, - currentBestMatch?: NavModelItem -): NavModelItem | undefined => { - const dashboardLinkMatch = '/dashboards'; - - for (const link of navTree) { - const linkWithoutParams = stripQueryParams(link.url); - const linkPathname = locationUtil.stripBaseFromUrl(linkWithoutParams); - if (linkPathname && link.id !== 'starred') { - if (linkPathname === pathname) { - // exact match - currentBestMatch = link; - break; - } else if (linkPathname !== '/' && pathname.startsWith(linkPathname)) { - // partial match - if (isBetterMatch(link, currentBestMatch)) { - currentBestMatch = link; - } - } else if (linkPathname === '/alerting/list' && pathname.startsWith('/alerting/notification/')) { - // alert channel match - // TODO refactor routes such that we don't need this custom logic - currentBestMatch = link; - break; - } else if (linkPathname === dashboardLinkMatch && pathname.startsWith('/d/')) { - // dashboard match - // TODO refactor routes such that we don't need this custom logic - if (isBetterMatch(link, currentBestMatch)) { - currentBestMatch = link; - } - } - } - if (link.children) { - currentBestMatch = getActiveItem(link.children, pathname, currentBestMatch); - } - if (stripQueryParams(currentBestMatch?.url) === pathname) { - return currentBestMatch; - } - } - return currentBestMatch; -}; - -export function getEditionAndUpdateLinks(): NavModelItem[] { - const { buildInfo, licenseInfo } = config; - const stateInfo = licenseInfo.stateInfo ? ` (${licenseInfo.stateInfo})` : ''; - const links: NavModelItem[] = []; - - links.push({ - target: '_blank', - id: 'version', - text: `${buildInfo.edition}${stateInfo}`, - url: licenseInfo.licenseUrl, - icon: 'external-link-alt', - }); - - if (buildInfo.hasUpdate) { - links.push({ - target: '_blank', - id: 'updateVersion', - text: `New version available!`, - icon: 'download-alt', - url: 'https://grafana.com/grafana/download?utm_source=grafana_footer', - }); - } - - return links; -} diff --git a/public/app/percona/shared/components/TabbedPage/TabbedPage.hooks.ts b/public/app/percona/shared/components/TabbedPage/TabbedPage.hooks.ts new file mode 100644 index 0000000000000..f7af8ec44b4cf --- /dev/null +++ b/public/app/percona/shared/components/TabbedPage/TabbedPage.hooks.ts @@ -0,0 +1,7 @@ +import { NavModel } from '@grafana/data'; +import { usePageNav } from 'app/core/components/Page/usePageNav'; + +export const useTabs = (navId?: string, oldProp?: NavModel) => { + const navModel = usePageNav(navId, oldProp); + return navModel?.main?.children || []; +}; diff --git a/public/app/percona/shared/components/TabbedPage/TabbedPage.styles.ts b/public/app/percona/shared/components/TabbedPage/TabbedPage.styles.ts new file mode 100644 index 0000000000000..4262617021e0b --- /dev/null +++ b/public/app/percona/shared/components/TabbedPage/TabbedPage.styles.ts @@ -0,0 +1,60 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; + +export const getStyles = (theme: GrafanaTheme2, verticalTabs?: boolean) => ({ + Page: css` + [class*='page-inner'] { + background-color: ${theme.colors.background.canvas}; + } + + [class*='page-content'] { + display: flex; + flex-direction: ${verticalTabs ? 'row' : 'column'}; + + ${theme.breakpoints.down('lg')} { + ${verticalTabs ? 'flex-direction: column;' : ''} + } + } + `, + TabsBar: verticalTabs + ? css` + width: calc(170px + ${theme.spacing(3)}); + + & > div { + height: auto; + display: flex; + align-items: flex-start; + flex-direction: column; + } + + [role='tablist'] > div { + width: 170px; + } + + ${theme.breakpoints.down('lg')} { + display: none; + } + ` + : '', + TabSelect: verticalTabs + ? css` + width: 200px; + padding-bottom: ${theme.spacing(1)}; + + ${theme.breakpoints.up('lg')} { + display: none; + } + ` + : css` + display: none; + `, + PageBody: css` + display: flex; + flex: 1; + + [class*='page-body'] { + flex: 1; + } + `, +}); diff --git a/public/app/percona/shared/components/TabbedPage/TabbedPage.tsx b/public/app/percona/shared/components/TabbedPage/TabbedPage.tsx new file mode 100644 index 0000000000000..e439c8868e4f2 --- /dev/null +++ b/public/app/percona/shared/components/TabbedPage/TabbedPage.tsx @@ -0,0 +1,36 @@ +import { cx } from '@emotion/css'; +import React, { FC } from 'react'; + +import { Tab, TabsBar, useStyles2 } from '@grafana/ui'; +import { Page } from 'app/core/components/Page/Page'; + +import { useTabs } from './TabbedPage.hooks'; +import { getStyles } from './TabbedPage.styles'; +import { TabbedPageProps } from './TabbedPage.types'; +import { TabbedPageSelect } from './TabbedPageSelect'; + +export const TabbedPage: FC = ({ children, isLoading, vertical, ...props }) => { + const tabs = useTabs(props.navId, props.navModel); + const styles = useStyles2(getStyles, vertical); + + return ( + + + + {tabs.map((child, index) => ( + + ))} + + +
{children}
+
+
+ ); +}; diff --git a/public/app/percona/shared/components/TabbedPage/TabbedPage.types.ts b/public/app/percona/shared/components/TabbedPage/TabbedPage.types.ts new file mode 100644 index 0000000000000..48fbcc80bffde --- /dev/null +++ b/public/app/percona/shared/components/TabbedPage/TabbedPage.types.ts @@ -0,0 +1,6 @@ +import { PageProps } from 'app/core/components/Page/types'; + +export interface TabbedPageProps extends PageProps { + vertical?: boolean; + isLoading?: boolean; +} diff --git a/public/app/percona/shared/components/TabbedPage/TabbedPageContents.styles.ts b/public/app/percona/shared/components/TabbedPage/TabbedPageContents.styles.ts new file mode 100644 index 0000000000000..7fd4f3005c441 --- /dev/null +++ b/public/app/percona/shared/components/TabbedPage/TabbedPageContents.styles.ts @@ -0,0 +1,10 @@ +import { css } from '@emotion/css'; + +export const styles = { + Contents: css` + &.page-container { + margin-left: 0; + margin-right: 0; + } + `, +}; diff --git a/public/app/percona/shared/components/TabbedPage/TabbedPageContents.tsx b/public/app/percona/shared/components/TabbedPage/TabbedPageContents.tsx new file mode 100644 index 0000000000000..0b6f87af0db8f --- /dev/null +++ b/public/app/percona/shared/components/TabbedPage/TabbedPageContents.tsx @@ -0,0 +1,12 @@ +import { cx } from '@emotion/css'; +import React, { ComponentProps, FC } from 'react'; + +import { Page } from 'app/core/components/Page/Page'; + +import { styles } from './TabbedPageContents.styles'; + +export const TabbedPageContents: FC> = ({ children, className, ...props }) => ( + + {children} + +); diff --git a/public/app/percona/shared/components/TabbedPage/TabbedPageSelect.tsx b/public/app/percona/shared/components/TabbedPage/TabbedPageSelect.tsx new file mode 100644 index 0000000000000..3ee23e1184d5e --- /dev/null +++ b/public/app/percona/shared/components/TabbedPage/TabbedPageSelect.tsx @@ -0,0 +1,33 @@ +import React, { FC, useMemo } from 'react'; + +import { SelectableValue } from '@grafana/data'; +import { locationService } from '@grafana/runtime'; +import { Select } from '@grafana/ui'; + +import { TabbedPageSelectProps } from './TabbedPageSelect.types'; + +export const TabbedPageSelect: FC = ({ tabs, className }) => { + const options = useMemo( + () => + tabs.map((tab) => ({ + label: tab.text, + title: tab.text, + value: tab.id, + url: tab.url, + })), + [tabs] + ); + const value = useMemo(() => tabs.find((tab) => tab.active), [tabs]); + + return ( +
+