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 { LinkTarget } from './dataLink';
|
||||
import { IconName } from './icon';
|
||||
|
||||
export interface NavLinkDTO {
|
||||
@ -11,7 +12,7 @@ export interface NavLinkDTO {
|
||||
icon?: IconName;
|
||||
img?: string;
|
||||
url?: string;
|
||||
target?: string;
|
||||
target?: LinkTarget;
|
||||
sortWeight?: number;
|
||||
divider?: boolean;
|
||||
hideFromMenu?: boolean;
|
||||
|
@ -30,7 +30,7 @@ export interface MenuItemProps<T = any> {
|
||||
/** Url of the menu item */
|
||||
url?: string;
|
||||
/** Handler for the click behaviour */
|
||||
onClick?: (event?: React.SyntheticEvent<HTMLElement>, payload?: T) => void;
|
||||
onClick?: (event?: React.MouseEvent<HTMLElement>, payload?: T) => void;
|
||||
/** Custom MenuItem styles*/
|
||||
className?: string;
|
||||
/** Active */
|
||||
@ -115,17 +115,7 @@ export const MenuItem = React.memo(
|
||||
className={itemStyle}
|
||||
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
|
||||
href={url}
|
||||
onClick={
|
||||
onClick
|
||||
? (event) => {
|
||||
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && onClick) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onClick(event);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onKeyDown={handleKeys}
|
||||
@ -136,8 +126,11 @@ export const MenuItem = React.memo(
|
||||
aria-checked={ariaChecked}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<>
|
||||
{icon && <Icon name={icon} className={styles.icon} aria-hidden />}
|
||||
{label}
|
||||
</>
|
||||
|
||||
{hasSubMenu && (
|
||||
<SubMenu
|
||||
items={childItems}
|
||||
|
@ -16,59 +16,47 @@ export function TopNavBarMenu({ node }: TopNavBarMenuProps) {
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
const onNavigate = (item: NavModelItem) => {
|
||||
const { url, target, onClick } = item;
|
||||
onClick?.();
|
||||
|
||||
if (url) {
|
||||
window.open(url, target);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuItem url={node.url} label={node.text} className={styles.header} />
|
||||
<Menu
|
||||
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) => {
|
||||
const translationKey = item.id && menuItemTranslations[item.id];
|
||||
const itemText = translationKey ? i18n._(translationKey) : item.text;
|
||||
|
||||
return !item.target && item.url?.startsWith('/') ? (
|
||||
<MenuItem url={item.url} label={itemText} key={item.id} />
|
||||
const showExternalLinkIcon = /^https?:\/\//.test(item.url || '');
|
||||
return item.url ? (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
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({
|
||||
height: `calc(${theme.spacing(6)} - 1px)`,
|
||||
fontSize: theme.typography.h4.fontSize,
|
||||
fontWeight: theme.typography.h4.fontWeight,
|
||||
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
|
||||
fontSize: theme.typography.h5.fontSize,
|
||||
fontWeight: theme.typography.h5.fontWeight,
|
||||
padding: theme.spacing(0.5, 1),
|
||||
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
|
||||
).map((item) => enrichWithInteractionTracking(item, false));
|
||||
|
||||
const helpNode = configItems.find((item) => item.id === 'help');
|
||||
const profileNode = configItems.find((item) => item.id === 'profile');
|
||||
const signInNode = configItems.find((item) => item.id === 'signin');
|
||||
|
||||
@ -64,11 +65,13 @@ export function TopSearchBar() {
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.actions}>
|
||||
<Tooltip placement="bottom" content="Help menu (todo)">
|
||||
{helpNode && (
|
||||
<Dropdown overlay={<TopNavBarMenu node={helpNode} />}>
|
||||
<button className={styles.actionItem}>
|
||||
<Icon name="question-circle" size="lg" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
)}
|
||||
<Tooltip placement="bottom" content="Grafana news (todo)">
|
||||
<button className={styles.actionItem}>
|
||||
<Icon name="rss" size="lg" />
|
||||
|
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import { LinkTarget } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Icon, IconName } from '@grafana/ui';
|
||||
|
||||
export interface FooterLink {
|
||||
target: LinkTarget;
|
||||
text: string;
|
||||
id?: string;
|
||||
id: string;
|
||||
icon?: IconName;
|
||||
url?: string;
|
||||
}
|
||||
@ -13,16 +15,22 @@ export interface FooterLink {
|
||||
export let getFooterLinks = (): FooterLink[] => {
|
||||
return [
|
||||
{
|
||||
target: '_blank',
|
||||
id: 'documentation',
|
||||
text: 'Documentation',
|
||||
icon: 'document-info',
|
||||
url: 'https://grafana.com/docs/grafana/latest/?utm_source=grafana_footer',
|
||||
},
|
||||
{
|
||||
target: '_blank',
|
||||
id: 'support',
|
||||
text: 'Support',
|
||||
icon: 'question-circle',
|
||||
url: 'https://grafana.com/products/enterprise/?utm_source=grafana_footer',
|
||||
},
|
||||
{
|
||||
target: '_blank',
|
||||
id: 'community',
|
||||
text: 'Community',
|
||||
icon: 'comments-alt',
|
||||
url: 'https://community.grafana.com/?utm_source=grafana_footer',
|
||||
@ -45,7 +53,12 @@ export let getVersionLinks = (): FooterLink[] => {
|
||||
const links: FooterLink[] = [];
|
||||
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) {
|
||||
return links;
|
||||
@ -56,6 +69,8 @@ export let getVersionLinks = (): FooterLink[] => {
|
||||
const docsVersion = isBeta ? 'next' : 'latest';
|
||||
|
||||
links.push({
|
||||
target: '_blank',
|
||||
id: 'version',
|
||||
text: `v${buildInfo.version} (${buildInfo.commit})`,
|
||||
url: hasReleaseNotes
|
||||
? `https://grafana.com/docs/grafana/${docsVersion}/release-notes/release-notes-${versionSlug}/`
|
||||
@ -64,6 +79,7 @@ export let getVersionLinks = (): FooterLink[] => {
|
||||
|
||||
if (buildInfo.hasUpdate) {
|
||||
links.push({
|
||||
target: '_blank',
|
||||
id: 'updateVersion',
|
||||
text: `New version available!`,
|
||||
icon: 'download-alt',
|
||||
@ -109,7 +125,7 @@ Footer.displayName = 'Footer';
|
||||
|
||||
function FooterItem({ item }: { item: FooterLink }) {
|
||||
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}
|
||||
</a>
|
||||
) : (
|
||||
|
@ -109,7 +109,17 @@ function mapMenuItem<T extends NodeDatum | EdgeDatum>(item: T) {
|
||||
url={link.url}
|
||||
label={link.label}
|
||||
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'}
|
||||
/>
|
||||
);
|
||||
|
@ -152,7 +152,7 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({
|
||||
items: i.items.map((j) => {
|
||||
return {
|
||||
...j,
|
||||
onClick: (e?: React.SyntheticEvent<HTMLElement>) => {
|
||||
onClick: (e?: React.MouseEvent<HTMLElement>) => {
|
||||
if (!coords) {
|
||||
return;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user