NavModel: Enable adding suffix elements to tabs (#44155)

* Trigger extra events

* Extend html attributes

* Add suffix to tabs

* Add upgrade routes

* suffix => highlightText

* suffix => suffixText

* Add size prop

* Update prop name

* Convert tabSuffix to a component
This commit is contained in:
Alex Khomenko 2022-01-26 09:15:45 +02:00 committed by GitHub
parent a20894c261
commit 55e1c53e36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 128 additions and 37 deletions

View File

@ -1,3 +1,5 @@
import { ComponentType } from 'react';
export interface NavModelItem { export interface NavModelItem {
text: string; text: string;
url?: string; url?: string;
@ -18,6 +20,7 @@ export interface NavModelItem {
onClick?: () => void; onClick?: () => void;
menuItemType?: NavMenuItemType; menuItemType?: NavMenuItemType;
highlightText?: string; highlightText?: string;
tabSuffix?: ComponentType<{ className?: string }>;
} }
export enum NavSection { export enum NavSection {

View File

@ -1,5 +1,10 @@
import { Meta, Props } from '@storybook/addon-docs/blocks';
import { Badge } from './Badge';
<Meta title="MDX|Badge" component={Badge} /> <Meta title="MDX|Badge" component={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. 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.
<Props of={Badge} />

View File

@ -3,13 +3,14 @@ import { Story } from '@storybook/react';
import { Badge, BadgeProps } from '@grafana/ui'; import { Badge, BadgeProps } from '@grafana/ui';
import { iconOptions } from '../../utils/storybook/knobs'; import { iconOptions } from '../../utils/storybook/knobs';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import mdx from './Badge.mdx';
export default { export default {
title: 'Data Display/Badge', title: 'Data Display/Badge',
component: Badge, component: Badge,
decorators: [withCenteredStory], decorators: [withCenteredStory],
parameters: { parameters: {
docs: {}, docs: { page: mdx },
}, },
argTypes: { argTypes: {
icon: { options: iconOptions, control: { type: 'select' } }, icon: { options: iconOptions, control: { type: 'select' } },

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { NavModelItem } from '@grafana/data';
import { IconName } from '../../types'; import { IconName } from '../../types';
import { TabsBar } from '../Tabs/TabsBar'; import { TabsBar } from '../Tabs/TabsBar';
import { Tab } from '../Tabs/Tab'; import { Tab } from '../Tabs/Tab';
@ -8,7 +9,7 @@ interface ModalTab {
value: string; value: string;
label: string; label: string;
icon?: IconName; icon?: IconName;
labelSuffix?: () => JSX.Element; tabSuffix?: NavModelItem['tabSuffix'];
} }
interface Props { interface Props {
@ -29,7 +30,7 @@ export const ModalTabsHeader: React.FC<Props> = ({ icon, title, tabs, activeTab,
key={`${t.value}-${index}`} key={`${t.value}-${index}`}
label={t.label} label={t.label}
icon={t.icon} icon={t.icon}
suffix={t.labelSuffix} suffix={t.tabSuffix}
active={t.value === activeTab} active={t.value === activeTab}
onChangeTab={() => onChangeTab(t)} onChangeTab={() => onChangeTab(t)}
/> />

View File

@ -1,6 +1,6 @@
import React, { HTMLProps } from 'react'; import React, { HTMLProps } from 'react';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { Icon } from '../Icon/Icon'; import { Icon } from '../Icon/Icon';
@ -18,11 +18,12 @@ export interface TabProps extends HTMLProps<HTMLAnchorElement> {
onChangeTab?: (event?: React.MouseEvent<HTMLAnchorElement>) => void; onChangeTab?: (event?: React.MouseEvent<HTMLAnchorElement>) => void;
/** A number rendered next to the text. Usually used to display the number of items in a tab's view. */ /** A number rendered next to the text. Usually used to display the number of items in a tab's view. */
counter?: number | null; counter?: number | null;
suffix?: () => JSX.Element; /** Extra content, displayed after the tab label and counter */
suffix?: NavModelItem['tabSuffix'];
} }
export const Tab = React.forwardRef<HTMLAnchorElement, TabProps>( export const Tab = React.forwardRef<HTMLAnchorElement, TabProps>(
({ 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 theme = useTheme2();
const tabsStyles = getTabStyles(theme); const tabsStyles = getTabStyles(theme);
const content = () => ( const content = () => (
@ -30,7 +31,7 @@ export const Tab = React.forwardRef<HTMLAnchorElement, TabProps>(
{icon && <Icon name={icon} />} {icon && <Icon name={icon} />}
{label} {label}
{typeof counter === 'number' && <Counter value={counter} />} {typeof counter === 'number' && <Counter value={counter} />}
{suffix && suffix()} {Suffix && <Suffix className={tabsStyles.suffix} />}
</> </>
); );
@ -116,5 +117,8 @@ const getTabStyles = stylesFactory((theme: GrafanaTheme2) => {
background-image: ${theme.colors.gradients.brandHorizontal} !important; background-image: ${theme.colors.gradients.brandHorizontal} !important;
} }
`, `,
suffix: css`
margin-left: ${theme.spacing(1)};
`,
}; };
}); });

View File

@ -63,7 +63,7 @@ export function NavBarItemMenuItem({ item, state, onNavigate }: NavBarItemMenuIt
</li> </li>
{item.value.highlightText && ( {item.value.highlightText && (
<li className={styles.upgradeBoxContainer}> <li className={styles.upgradeBoxContainer}>
<UpgradeBox text={item.value.highlightText} className={styles.upgradeBox} /> <UpgradeBox text={item.value.highlightText} className={styles.upgradeBox} size={'sm'} />
</li> </li>
)} )}
</> </>

View File

@ -62,6 +62,7 @@ const Navigation = ({ children }: { children: NavModelItem[] }) => {
key={`${child.url}-${index}`} key={`${child.url}-${index}`}
icon={child.icon as IconName} icon={child.icon as IconName}
href={child.url} href={child.url}
suffix={child.tabSuffix}
/> />
) )
); );

View File

@ -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<HTMLSpanElement> {
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 (
<span className={cx(styles.badge, className)} {...htmlProps}>
{text}
</span>
);
};
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)};
`,
};
};

View File

@ -3,12 +3,15 @@ import { css, cx } from '@emotion/css';
import { Icon, LinkButton, useStyles2 } from '@grafana/ui'; import { Icon, LinkButton, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
type ComponentSize = 'sm' | 'md';
export interface Props extends HTMLAttributes<HTMLOrSVGElement> { export interface Props extends HTMLAttributes<HTMLOrSVGElement> {
text: string; text: string;
size?: ComponentSize;
} }
export const UpgradeBox = ({ text, className, ...htmlProps }: Props) => { export const UpgradeBox = ({ text, className, size = 'md', ...htmlProps }: Props) => {
const styles = useStyles2(getUpgradeBoxStyles); const styles = useStyles2((theme) => getUpgradeBoxStyles(theme, size));
return ( return (
<div className={cx(styles.box, className)} {...htmlProps}> <div className={cx(styles.box, className)} {...htmlProps}>
@ -18,7 +21,7 @@ export const UpgradeBox = ({ text, className, ...htmlProps }: Props) => {
<p className={styles.text}>{text}</p> <p className={styles.text}>{text}</p>
<LinkButton <LinkButton
variant="primary" variant="primary"
size={'sm'} size={size}
className={styles.button} className={styles.button}
href="https://grafana.com/profile/org/subscription" href="https://grafana.com/profile/org/subscription"
target="__blank" target="__blank"
@ -31,8 +34,9 @@ export const UpgradeBox = ({ text, className, ...htmlProps }: Props) => {
); );
}; };
const getUpgradeBoxStyles = (theme: GrafanaTheme2) => { const getUpgradeBoxStyles = (theme: GrafanaTheme2, size: ComponentSize) => {
const borderRadius = theme.shape.borderRadius(2); const borderRadius = theme.shape.borderRadius(2);
const fontBase = size === 'md' ? 'body' : 'bodySmall';
return { return {
box: css` box: css`
@ -43,7 +47,7 @@ const getUpgradeBoxStyles = (theme: GrafanaTheme2) => {
border: 1px solid ${theme.colors.primary.shade}; border: 1px solid ${theme.colors.primary.shade};
padding: ${theme.spacing(2)}; padding: ${theme.spacing(2)};
color: ${theme.colors.primary.text}; color: ${theme.colors.primary.text};
font-size: ${theme.typography.bodySmall.fontSize}; font-size: ${theme.typography[fontBase].fontSize};
text-align: left; text-align: left;
line-height: 16px; line-height: 16px;
`, `,
@ -52,6 +56,12 @@ const getUpgradeBoxStyles = (theme: GrafanaTheme2) => {
`, `,
button: css` button: css`
margin-top: ${theme.spacing(2)}; margin-top: ${theme.spacing(2)};
&:focus-visible {
box-shadow: none;
color: ${theme.colors.text.primary};
outline: 2px solid ${theme.colors.primary.main};
}
`, `,
icon: css` icon: css`
border: 1px solid ${theme.colors.primary.shade}; border: 1px solid ${theme.colors.primary.shade};

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { NavModelItem } from '@grafana/data';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
export interface ShareModalTabProps { export interface ShareModalTabProps {
@ -10,6 +11,6 @@ export interface ShareModalTabProps {
export interface ShareModalTabModel { export interface ShareModalTabModel {
label: string; label: string;
value: string; value: string;
labelSuffix?: () => JSX.Element; tabSuffix?: NavModelItem['tabSuffix'];
component: React.ComponentType<ShareModalTabProps>; component: React.ComponentType<ShareModalTabProps>;
} }

View File

@ -3,6 +3,7 @@ import { featureEnabled } from '@grafana/runtime';
import config from 'app/core/config'; import config from 'app/core/config';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
import { ProBadge } from 'app/core/components/Upgrade/ProBadge';
import { GenericDataSourcePlugin } from '../settings/PluginSettings'; import { GenericDataSourcePlugin } from '../settings/PluginSettings';
export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDataSourcePlugin): NavModelItem { 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 (featureEnabled('dspermissions')) {
if (contextSrv.hasPermission(AccessControlAction.DataSourcesPermissionsRead)) { if (contextSrv.hasPermission(AccessControlAction.DataSourcesPermissionsRead)) {
navModel.children!.push({ navModel.children!.push(dsPermissions);
active: false,
icon: 'lock',
id: `datasource-permissions-${dataSource.id}`,
text: 'Permissions',
url: `datasources/edit/${dataSource.id}/permissions`,
});
} }
} } else if (config.featureHighlights.enabled) {
if (featureEnabled('analytics')) {
navModel.children!.push({ navModel.children!.push({
active: false, ...dsPermissions,
icon: 'info-circle', url: dsPermissions.url + '/upgrade',
id: `datasource-insights-${dataSource.id}`, tabSuffix: ProBadge,
text: 'Insights',
url: `datasources/edit/${dataSource.id}/insights`,
}); });
} }
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({ navModel.children!.push({
active: false, ...analytics,
icon: 'database', url: analytics.url + '/upgrade',
id: `datasource-cache-${dataSource.uid}`, tabSuffix: ProBadge,
text: 'Cache', });
url: `datasources/edit/${dataSource.uid}/cache`, }
hideFromTabs: !pluginMeta.isBackend || !config.caching.enabled,
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,
}); });
} }