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 {
text: string;
url?: string;
@ -18,6 +20,7 @@ export interface NavModelItem {
onClick?: () => void;
menuItemType?: NavMenuItemType;
highlightText?: string;
tabSuffix?: ComponentType<{ className?: string }>;
}
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} />
# 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.
<Props of={Badge} />

View File

@ -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' } },

View File

@ -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<Props> = ({ 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)}
/>

View File

@ -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<HTMLAnchorElement> {
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. */
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>(
({ 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<HTMLAnchorElement, TabProps>(
{icon && <Icon name={icon} />}
{label}
{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;
}
`,
suffix: css`
margin-left: ${theme.spacing(1)};
`,
};
});

View File

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

View File

@ -62,6 +62,7 @@ const Navigation = ({ children }: { children: NavModelItem[] }) => {
key={`${child.url}-${index}`}
icon={child.icon as IconName}
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 { GrafanaTheme2 } from '@grafana/data';
type ComponentSize = 'sm' | 'md';
export interface Props extends HTMLAttributes<HTMLOrSVGElement> {
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 (
<div className={cx(styles.box, className)} {...htmlProps}>
@ -18,7 +21,7 @@ export const UpgradeBox = ({ text, className, ...htmlProps }: Props) => {
<p className={styles.text}>{text}</p>
<LinkButton
variant="primary"
size={'sm'}
size={size}
className={styles.button}
href="https://grafana.com/profile/org/subscription"
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 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};

View File

@ -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<ShareModalTabProps>;
}

View File

@ -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,
});
}