mirror of https://github.com/grafana/grafana.git
				
				
				
			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