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:
Leo 2022-09-17 18:17:00 +02:00 committed by GitHub
parent 17b2fb04e8
commit 1a0cbdeabe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 73 additions and 62 deletions

View File

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

View File

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

View File

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

View File

@ -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" />

View File

@ -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>
) : ( ) : (

View File

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

View File

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