mirror of https://github.com/grafana/grafana.git
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:
parent
a20894c261
commit
55e1c53e36
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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' } },
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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)};
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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)};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue