mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Navigation: Add help menu to top search bar (#55062)
* add help menu to top search bar * fixes * handle preventDefault in node graph specifically * use icon prop of MenuItem * undo changes to ContextMenuPlugin/DataLinksContextMenu * remove unused component * revert storybook changes * Tweaks * remove unused style * stop propagation on the header so version can be highlighted * make sure useContextMenu has the exact same logic as before Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
17b2fb04e8
commit
1a0cbdeabe
@ -1,5 +1,6 @@
|
|||||||
import { ComponentType } from 'react';
|
import { ComponentType } from 'react';
|
||||||
|
|
||||||
|
import { LinkTarget } from './dataLink';
|
||||||
import { IconName } from './icon';
|
import { IconName } from './icon';
|
||||||
|
|
||||||
export interface NavLinkDTO {
|
export interface NavLinkDTO {
|
||||||
@ -11,7 +12,7 @@ export interface NavLinkDTO {
|
|||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
img?: string;
|
img?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
target?: string;
|
target?: LinkTarget;
|
||||||
sortWeight?: number;
|
sortWeight?: number;
|
||||||
divider?: boolean;
|
divider?: boolean;
|
||||||
hideFromMenu?: boolean;
|
hideFromMenu?: boolean;
|
||||||
|
@ -30,7 +30,7 @@ export interface MenuItemProps<T = any> {
|
|||||||
/** Url of the menu item */
|
/** Url of the menu item */
|
||||||
url?: string;
|
url?: string;
|
||||||
/** Handler for the click behaviour */
|
/** Handler for the click behaviour */
|
||||||
onClick?: (event?: React.SyntheticEvent<HTMLElement>, payload?: T) => void;
|
onClick?: (event?: React.MouseEvent<HTMLElement>, payload?: T) => void;
|
||||||
/** Custom MenuItem styles*/
|
/** Custom MenuItem styles*/
|
||||||
className?: string;
|
className?: string;
|
||||||
/** Active */
|
/** Active */
|
||||||
@ -115,17 +115,7 @@ export const MenuItem = React.memo(
|
|||||||
className={itemStyle}
|
className={itemStyle}
|
||||||
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
|
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
|
||||||
href={url}
|
href={url}
|
||||||
onClick={
|
onClick={onClick}
|
||||||
onClick
|
|
||||||
? (event) => {
|
|
||||||
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && onClick) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
onClick(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
onKeyDown={handleKeys}
|
onKeyDown={handleKeys}
|
||||||
@ -136,8 +126,11 @@ export const MenuItem = React.memo(
|
|||||||
aria-checked={ariaChecked}
|
aria-checked={ariaChecked}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
>
|
>
|
||||||
|
<>
|
||||||
{icon && <Icon name={icon} className={styles.icon} aria-hidden />}
|
{icon && <Icon name={icon} className={styles.icon} aria-hidden />}
|
||||||
{label}
|
{label}
|
||||||
|
</>
|
||||||
|
|
||||||
{hasSubMenu && (
|
{hasSubMenu && (
|
||||||
<SubMenu
|
<SubMenu
|
||||||
items={childItems}
|
items={childItems}
|
||||||
|
@ -16,59 +16,47 @@ export function TopNavBarMenu({ node }: TopNavBarMenuProps) {
|
|||||||
if (!node) {
|
if (!node) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const onNavigate = (item: NavModelItem) => {
|
|
||||||
const { url, target, onClick } = item;
|
|
||||||
onClick?.();
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
window.open(url, target);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu
|
||||||
<MenuItem url={node.url} label={node.text} className={styles.header} />
|
header={
|
||||||
|
<div onClick={(e) => e.stopPropagation()} className={styles.header}>
|
||||||
|
<div>{node.text}</div>
|
||||||
|
{node.subTitle && <div className={styles.subTitle}>{node.subTitle}</div>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
{node.children?.map((item) => {
|
{node.children?.map((item) => {
|
||||||
const translationKey = item.id && menuItemTranslations[item.id];
|
const translationKey = item.id && menuItemTranslations[item.id];
|
||||||
const itemText = translationKey ? i18n._(translationKey) : item.text;
|
const itemText = translationKey ? i18n._(translationKey) : item.text;
|
||||||
|
const showExternalLinkIcon = /^https?:\/\//.test(item.url || '');
|
||||||
return !item.target && item.url?.startsWith('/') ? (
|
return item.url ? (
|
||||||
<MenuItem url={item.url} label={itemText} key={item.id} />
|
<MenuItem
|
||||||
|
url={item.url}
|
||||||
|
label={itemText}
|
||||||
|
icon={showExternalLinkIcon ? 'external-link-alt' : undefined}
|
||||||
|
target={item.target}
|
||||||
|
key={item.id}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<MenuItem onClick={() => onNavigate(item)} label={itemText} key={item.id} />
|
<MenuItem icon={item.icon} onClick={item.onClick} label={itemText} key={item.id} />
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{node.subTitle && (
|
|
||||||
// Stopping the propagation of the event when clicking the subTitle so the menu
|
|
||||||
// does not close
|
|
||||||
<div onClick={(e) => e.stopPropagation()} className={styles.subtitle}>
|
|
||||||
{node.subTitle}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
subtitle: css`
|
|
||||||
background-color: transparent;
|
|
||||||
border-top: 1px solid ${theme.colors.border.weak};
|
|
||||||
color: ${theme.colors.text.secondary};
|
|
||||||
font-size: ${theme.typography.bodySmall.fontSize};
|
|
||||||
font-weight: ${theme.typography.bodySmall.fontWeight};
|
|
||||||
padding: ${theme.spacing(1)} ${theme.spacing(2)} ${theme.spacing(1)};
|
|
||||||
text-align: left;
|
|
||||||
white-space: nowrap;
|
|
||||||
`,
|
|
||||||
header: css({
|
header: css({
|
||||||
height: `calc(${theme.spacing(6)} - 1px)`,
|
fontSize: theme.typography.h5.fontSize,
|
||||||
fontSize: theme.typography.h4.fontSize,
|
fontWeight: theme.typography.h5.fontWeight,
|
||||||
fontWeight: theme.typography.h4.fontWeight,
|
padding: theme.spacing(0.5, 1),
|
||||||
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
|
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
width: '100%',
|
}),
|
||||||
background: theme.colors.background.secondary,
|
subTitle: css({
|
||||||
|
color: theme.colors.text.secondary,
|
||||||
|
fontSize: theme.typography.bodySmall.fontSize,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -44,6 +44,7 @@ export function TopSearchBar() {
|
|||||||
toggleSwitcherModal
|
toggleSwitcherModal
|
||||||
).map((item) => enrichWithInteractionTracking(item, false));
|
).map((item) => enrichWithInteractionTracking(item, false));
|
||||||
|
|
||||||
|
const helpNode = configItems.find((item) => item.id === 'help');
|
||||||
const profileNode = configItems.find((item) => item.id === 'profile');
|
const profileNode = configItems.find((item) => item.id === 'profile');
|
||||||
const signInNode = configItems.find((item) => item.id === 'signin');
|
const signInNode = configItems.find((item) => item.id === 'signin');
|
||||||
|
|
||||||
@ -64,11 +65,13 @@ export function TopSearchBar() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<Tooltip placement="bottom" content="Help menu (todo)">
|
{helpNode && (
|
||||||
|
<Dropdown overlay={<TopNavBarMenu node={helpNode} />}>
|
||||||
<button className={styles.actionItem}>
|
<button className={styles.actionItem}>
|
||||||
<Icon name="question-circle" size="lg" />
|
<Icon name="question-circle" size="lg" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Dropdown>
|
||||||
|
)}
|
||||||
<Tooltip placement="bottom" content="Grafana news (todo)">
|
<Tooltip placement="bottom" content="Grafana news (todo)">
|
||||||
<button className={styles.actionItem}>
|
<button className={styles.actionItem}>
|
||||||
<Icon name="rss" size="lg" />
|
<Icon name="rss" size="lg" />
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { LinkTarget } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { Icon, IconName } from '@grafana/ui';
|
import { Icon, IconName } from '@grafana/ui';
|
||||||
|
|
||||||
export interface FooterLink {
|
export interface FooterLink {
|
||||||
|
target: LinkTarget;
|
||||||
text: string;
|
text: string;
|
||||||
id?: string;
|
id: string;
|
||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
@ -13,16 +15,22 @@ export interface FooterLink {
|
|||||||
export let getFooterLinks = (): FooterLink[] => {
|
export let getFooterLinks = (): FooterLink[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
target: '_blank',
|
||||||
|
id: 'documentation',
|
||||||
text: 'Documentation',
|
text: 'Documentation',
|
||||||
icon: 'document-info',
|
icon: 'document-info',
|
||||||
url: 'https://grafana.com/docs/grafana/latest/?utm_source=grafana_footer',
|
url: 'https://grafana.com/docs/grafana/latest/?utm_source=grafana_footer',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
target: '_blank',
|
||||||
|
id: 'support',
|
||||||
text: 'Support',
|
text: 'Support',
|
||||||
icon: 'question-circle',
|
icon: 'question-circle',
|
||||||
url: 'https://grafana.com/products/enterprise/?utm_source=grafana_footer',
|
url: 'https://grafana.com/products/enterprise/?utm_source=grafana_footer',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
target: '_blank',
|
||||||
|
id: 'community',
|
||||||
text: 'Community',
|
text: 'Community',
|
||||||
icon: 'comments-alt',
|
icon: 'comments-alt',
|
||||||
url: 'https://community.grafana.com/?utm_source=grafana_footer',
|
url: 'https://community.grafana.com/?utm_source=grafana_footer',
|
||||||
@ -45,7 +53,12 @@ export let getVersionLinks = (): FooterLink[] => {
|
|||||||
const links: FooterLink[] = [];
|
const links: FooterLink[] = [];
|
||||||
const stateInfo = licenseInfo.stateInfo ? ` (${licenseInfo.stateInfo})` : '';
|
const stateInfo = licenseInfo.stateInfo ? ` (${licenseInfo.stateInfo})` : '';
|
||||||
|
|
||||||
links.push({ text: `${buildInfo.edition}${stateInfo}`, url: licenseInfo.licenseUrl });
|
links.push({
|
||||||
|
target: '_blank',
|
||||||
|
id: 'version',
|
||||||
|
text: `${buildInfo.edition}${stateInfo}`,
|
||||||
|
url: licenseInfo.licenseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
if (buildInfo.hideVersion) {
|
if (buildInfo.hideVersion) {
|
||||||
return links;
|
return links;
|
||||||
@ -56,6 +69,8 @@ export let getVersionLinks = (): FooterLink[] => {
|
|||||||
const docsVersion = isBeta ? 'next' : 'latest';
|
const docsVersion = isBeta ? 'next' : 'latest';
|
||||||
|
|
||||||
links.push({
|
links.push({
|
||||||
|
target: '_blank',
|
||||||
|
id: 'version',
|
||||||
text: `v${buildInfo.version} (${buildInfo.commit})`,
|
text: `v${buildInfo.version} (${buildInfo.commit})`,
|
||||||
url: hasReleaseNotes
|
url: hasReleaseNotes
|
||||||
? `https://grafana.com/docs/grafana/${docsVersion}/release-notes/release-notes-${versionSlug}/`
|
? `https://grafana.com/docs/grafana/${docsVersion}/release-notes/release-notes-${versionSlug}/`
|
||||||
@ -64,6 +79,7 @@ export let getVersionLinks = (): FooterLink[] => {
|
|||||||
|
|
||||||
if (buildInfo.hasUpdate) {
|
if (buildInfo.hasUpdate) {
|
||||||
links.push({
|
links.push({
|
||||||
|
target: '_blank',
|
||||||
id: 'updateVersion',
|
id: 'updateVersion',
|
||||||
text: `New version available!`,
|
text: `New version available!`,
|
||||||
icon: 'download-alt',
|
icon: 'download-alt',
|
||||||
@ -109,7 +125,7 @@ Footer.displayName = 'Footer';
|
|||||||
|
|
||||||
function FooterItem({ item }: { item: FooterLink }) {
|
function FooterItem({ item }: { item: FooterLink }) {
|
||||||
const content = item.url ? (
|
const content = item.url ? (
|
||||||
<a href={item.url} target="_blank" rel="noopener noreferrer" id={item.id}>
|
<a href={item.url} target={item.target} rel="noopener noreferrer" id={item.id}>
|
||||||
{item.text}
|
{item.text}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
|
@ -109,7 +109,17 @@ function mapMenuItem<T extends NodeDatum | EdgeDatum>(item: T) {
|
|||||||
url={link.url}
|
url={link.url}
|
||||||
label={link.label}
|
label={link.label}
|
||||||
ariaLabel={link.ariaLabel}
|
ariaLabel={link.ariaLabel}
|
||||||
onClick={link.onClick ? () => link.onClick?.(item) : undefined}
|
onClick={
|
||||||
|
link.onClick
|
||||||
|
? (event) => {
|
||||||
|
if (!(event?.ctrlKey || event?.metaKey || event?.shiftKey)) {
|
||||||
|
event?.preventDefault();
|
||||||
|
event?.stopPropagation();
|
||||||
|
link.onClick?.(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
target={'_self'}
|
target={'_self'}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -152,7 +152,7 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({
|
|||||||
items: i.items.map((j) => {
|
items: i.items.map((j) => {
|
||||||
return {
|
return {
|
||||||
...j,
|
...j,
|
||||||
onClick: (e?: React.SyntheticEvent<HTMLElement>) => {
|
onClick: (e?: React.MouseEvent<HTMLElement>) => {
|
||||||
if (!coords) {
|
if (!coords) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user