diff --git a/packages/grafana-data/src/types/navModel.ts b/packages/grafana-data/src/types/navModel.ts index fc9fad5e029..d4c885f11a5 100644 --- a/packages/grafana-data/src/types/navModel.ts +++ b/packages/grafana-data/src/types/navModel.ts @@ -1,3 +1,5 @@ +import { ComponentType } from 'react'; + export interface NavModelItem { text: string; url?: string; @@ -18,6 +20,7 @@ export interface NavModelItem { onClick?: () => void; menuItemType?: NavMenuItemType; highlightText?: string; + tabSuffix?: ComponentType<{ className?: string }>; } export enum NavSection { diff --git a/packages/grafana-ui/src/components/Badge/Badge.mdx b/packages/grafana-ui/src/components/Badge/Badge.mdx index 8782bceb0c1..f246c69d5c0 100644 --- a/packages/grafana-ui/src/components/Badge/Badge.mdx +++ b/packages/grafana-ui/src/components/Badge/Badge.mdx @@ -1,5 +1,10 @@ +import { Meta, Props } from '@storybook/addon-docs/blocks'; +import { Badge } from './Badge'; + -# Badge +## Badge The badge component adds meta information to other content, for example about release status or new elements. You can add any `Icon` component or use the badge without an icon. + + diff --git a/packages/grafana-ui/src/components/Badge/Badge.story.tsx b/packages/grafana-ui/src/components/Badge/Badge.story.tsx index 4d790acf15a..32aa3a63969 100644 --- a/packages/grafana-ui/src/components/Badge/Badge.story.tsx +++ b/packages/grafana-ui/src/components/Badge/Badge.story.tsx @@ -3,13 +3,14 @@ import { Story } from '@storybook/react'; import { Badge, BadgeProps } from '@grafana/ui'; import { iconOptions } from '../../utils/storybook/knobs'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import mdx from './Badge.mdx'; export default { title: 'Data Display/Badge', component: Badge, decorators: [withCenteredStory], parameters: { - docs: {}, + docs: { page: mdx }, }, argTypes: { icon: { options: iconOptions, control: { type: 'select' } }, diff --git a/packages/grafana-ui/src/components/Modal/ModalTabsHeader.tsx b/packages/grafana-ui/src/components/Modal/ModalTabsHeader.tsx index c34a7696576..a4a2820ab2c 100644 --- a/packages/grafana-ui/src/components/Modal/ModalTabsHeader.tsx +++ b/packages/grafana-ui/src/components/Modal/ModalTabsHeader.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { NavModelItem } from '@grafana/data'; import { IconName } from '../../types'; import { TabsBar } from '../Tabs/TabsBar'; import { Tab } from '../Tabs/Tab'; @@ -8,7 +9,7 @@ interface ModalTab { value: string; label: string; icon?: IconName; - labelSuffix?: () => JSX.Element; + tabSuffix?: NavModelItem['tabSuffix']; } interface Props { @@ -29,7 +30,7 @@ export const ModalTabsHeader: React.FC = ({ icon, title, tabs, activeTab, key={`${t.value}-${index}`} label={t.label} icon={t.icon} - suffix={t.labelSuffix} + suffix={t.tabSuffix} active={t.value === activeTab} onChangeTab={() => onChangeTab(t)} /> diff --git a/packages/grafana-ui/src/components/Tabs/Tab.tsx b/packages/grafana-ui/src/components/Tabs/Tab.tsx index f5839ee1e94..562eb2669ee 100644 --- a/packages/grafana-ui/src/components/Tabs/Tab.tsx +++ b/packages/grafana-ui/src/components/Tabs/Tab.tsx @@ -1,6 +1,6 @@ import React, { HTMLProps } from 'react'; import { css, cx } from '@emotion/css'; -import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Icon } from '../Icon/Icon'; @@ -18,11 +18,12 @@ export interface TabProps extends HTMLProps { onChangeTab?: (event?: React.MouseEvent) => void; /** A number rendered next to the text. Usually used to display the number of items in a tab's view. */ counter?: number | null; - suffix?: () => JSX.Element; + /** Extra content, displayed after the tab label and counter */ + suffix?: NavModelItem['tabSuffix']; } export const Tab = React.forwardRef( - ({ label, active, icon, onChangeTab, counter, suffix, className, href, ...otherProps }, ref) => { + ({ label, active, icon, onChangeTab, counter, suffix: Suffix, className, href, ...otherProps }, ref) => { const theme = useTheme2(); const tabsStyles = getTabStyles(theme); const content = () => ( @@ -30,7 +31,7 @@ export const Tab = React.forwardRef( {icon && } {label} {typeof counter === 'number' && } - {suffix && suffix()} + {Suffix && } ); @@ -116,5 +117,8 @@ const getTabStyles = stylesFactory((theme: GrafanaTheme2) => { background-image: ${theme.colors.gradients.brandHorizontal} !important; } `, + suffix: css` + margin-left: ${theme.spacing(1)}; + `, }; }); diff --git a/public/app/core/components/NavBar/NavBarItemMenuItem.tsx b/public/app/core/components/NavBar/NavBarItemMenuItem.tsx index 8d945f7be1b..27bb08ec988 100644 --- a/public/app/core/components/NavBar/NavBarItemMenuItem.tsx +++ b/public/app/core/components/NavBar/NavBarItemMenuItem.tsx @@ -63,7 +63,7 @@ export function NavBarItemMenuItem({ item, state, onNavigate }: NavBarItemMenuIt {item.value.highlightText && (
  • - +
  • )} diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index 9b6c8ff0194..ffec4446ec8 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -62,6 +62,7 @@ const Navigation = ({ children }: { children: NavModelItem[] }) => { key={`${child.url}-${index}`} icon={child.icon as IconName} href={child.url} + suffix={child.tabSuffix} /> ) ); diff --git a/public/app/core/components/Upgrade/ProBadge.tsx b/public/app/core/components/Upgrade/ProBadge.tsx new file mode 100644 index 00000000000..b8b3ede8c8f --- /dev/null +++ b/public/app/core/components/Upgrade/ProBadge.tsx @@ -0,0 +1,40 @@ +import React, { HTMLAttributes, useEffect } from 'react'; +import { css, cx } from '@emotion/css'; +import { useStyles2 } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; + +export interface Props extends HTMLAttributes { + text?: string; + /** Function to call when component initializes, e.g. event trackers */ + onLoad?: (...args: any[]) => void; +} + +export const ProBadge = ({ text = 'PRO', className, onLoad, ...htmlProps }: Props) => { + const styles = useStyles2(getStyles); + + useEffect(() => { + if (onLoad) { + onLoad(); + } + }, [onLoad]); + + return ( + + {text} + + ); +}; + +const getStyles = (theme: GrafanaTheme2) => { + return { + badge: css` + margin-left: ${theme.spacing(1.25)}; + border-radius: ${theme.shape.borderRadius(5)}; + background-color: ${theme.colors.success.main}; + padding: ${theme.spacing(0.25, 0.75)}; + color: ${theme.colors.text.maxContrast}; + font-weight: ${theme.typography.fontWeightMedium}; + font-size: ${theme.typography.pxToRem(10)}; + `, + }; +}; diff --git a/public/app/core/components/Upgrade/UpgradeBox.tsx b/public/app/core/components/Upgrade/UpgradeBox.tsx index 126b29dbdca..24bf3c1f21c 100644 --- a/public/app/core/components/Upgrade/UpgradeBox.tsx +++ b/public/app/core/components/Upgrade/UpgradeBox.tsx @@ -3,12 +3,15 @@ import { css, cx } from '@emotion/css'; import { Icon, LinkButton, useStyles2 } from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data'; +type ComponentSize = 'sm' | 'md'; + export interface Props extends HTMLAttributes { text: string; + size?: ComponentSize; } -export const UpgradeBox = ({ text, className, ...htmlProps }: Props) => { - const styles = useStyles2(getUpgradeBoxStyles); +export const UpgradeBox = ({ text, className, size = 'md', ...htmlProps }: Props) => { + const styles = useStyles2((theme) => getUpgradeBoxStyles(theme, size)); return (
    @@ -18,7 +21,7 @@ export const UpgradeBox = ({ text, className, ...htmlProps }: Props) => {

    {text}

    { ); }; -const getUpgradeBoxStyles = (theme: GrafanaTheme2) => { +const getUpgradeBoxStyles = (theme: GrafanaTheme2, size: ComponentSize) => { const borderRadius = theme.shape.borderRadius(2); + const fontBase = size === 'md' ? 'body' : 'bodySmall'; return { box: css` @@ -43,7 +47,7 @@ const getUpgradeBoxStyles = (theme: GrafanaTheme2) => { border: 1px solid ${theme.colors.primary.shade}; padding: ${theme.spacing(2)}; color: ${theme.colors.primary.text}; - font-size: ${theme.typography.bodySmall.fontSize}; + font-size: ${theme.typography[fontBase].fontSize}; text-align: left; line-height: 16px; `, @@ -52,6 +56,12 @@ const getUpgradeBoxStyles = (theme: GrafanaTheme2) => { `, button: css` margin-top: ${theme.spacing(2)}; + + &:focus-visible { + box-shadow: none; + color: ${theme.colors.text.primary}; + outline: 2px solid ${theme.colors.primary.main}; + } `, icon: css` border: 1px solid ${theme.colors.primary.shade}; diff --git a/public/app/features/dashboard/components/ShareModal/types.ts b/public/app/features/dashboard/components/ShareModal/types.ts index 8b7f47a12cf..42d45c9ea89 100644 --- a/public/app/features/dashboard/components/ShareModal/types.ts +++ b/public/app/features/dashboard/components/ShareModal/types.ts @@ -1,4 +1,5 @@ import React from 'react'; +import { NavModelItem } from '@grafana/data'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; export interface ShareModalTabProps { @@ -10,6 +11,6 @@ export interface ShareModalTabProps { export interface ShareModalTabModel { label: string; value: string; - labelSuffix?: () => JSX.Element; + tabSuffix?: NavModelItem['tabSuffix']; component: React.ComponentType; } diff --git a/public/app/features/datasources/state/navModel.ts b/public/app/features/datasources/state/navModel.ts index b75dfad3d91..2cf04410370 100644 --- a/public/app/features/datasources/state/navModel.ts +++ b/public/app/features/datasources/state/navModel.ts @@ -3,6 +3,7 @@ import { featureEnabled } from '@grafana/runtime'; import config from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { AccessControlAction } from 'app/types'; +import { ProBadge } from 'app/core/components/Upgrade/ProBadge'; import { GenericDataSourcePlugin } from '../settings/PluginSettings'; export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDataSourcePlugin): NavModelItem { @@ -48,36 +49,60 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat }); } + const dsPermissions = { + active: false, + icon: 'lock', + id: `datasource-permissions-${dataSource.id}`, + text: 'Permissions', + url: `datasources/edit/${dataSource.id}/permissions`, + }; + if (featureEnabled('dspermissions')) { if (contextSrv.hasPermission(AccessControlAction.DataSourcesPermissionsRead)) { - navModel.children!.push({ - active: false, - icon: 'lock', - id: `datasource-permissions-${dataSource.id}`, - text: 'Permissions', - url: `datasources/edit/${dataSource.id}/permissions`, - }); + navModel.children!.push(dsPermissions); } - } - - if (featureEnabled('analytics')) { + } else if (config.featureHighlights.enabled) { navModel.children!.push({ - active: false, - icon: 'info-circle', - id: `datasource-insights-${dataSource.id}`, - text: 'Insights', - url: `datasources/edit/${dataSource.id}/insights`, + ...dsPermissions, + url: dsPermissions.url + '/upgrade', + tabSuffix: ProBadge, }); } - if (featureEnabled('caching')) { + const analytics = { + active: false, + icon: 'info-circle', + id: `datasource-insights-${dataSource.id}`, + text: 'Insights', + url: `datasources/edit/${dataSource.id}/insights`, + }; + + if (featureEnabled('analytics')) { + navModel.children!.push(analytics); + } else if (config.featureHighlights.enabled) { navModel.children!.push({ - active: false, - icon: 'database', - id: `datasource-cache-${dataSource.uid}`, - text: 'Cache', - url: `datasources/edit/${dataSource.uid}/cache`, - hideFromTabs: !pluginMeta.isBackend || !config.caching.enabled, + ...analytics, + url: analytics.url + '/upgrade', + tabSuffix: ProBadge, + }); + } + + const caching = { + active: false, + icon: 'database', + id: `datasource-cache-${dataSource.uid}`, + text: 'Cache', + url: `datasources/edit/${dataSource.uid}/cache`, + hideFromTabs: !pluginMeta.isBackend || !config.caching.enabled, + }; + + if (featureEnabled('caching')) { + navModel.children!.push(caching); + } else if (config.featureHighlights.enabled) { + navModel.children!.push({ + ...caching, + url: caching.url + '/upgrade', + tabSuffix: ProBadge, }); }