mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Navigation: Adds profile menu to top nav bar (#54720)
This commit is contained in:
parent
9cc828a00e
commit
ab79976a8d
@ -0,0 +1,74 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { i18n } from '@lingui/core';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||||
|
import { Menu, MenuItem, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
import menuItemTranslations from '../../NavBar/navBarItem-translations';
|
||||||
|
|
||||||
|
export interface TopNavBarMenuProps {
|
||||||
|
node: NavModelItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TopNavBarMenu({ node }: TopNavBarMenuProps) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
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} />
|
||||||
|
{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} />
|
||||||
|
) : (
|
||||||
|
<MenuItem onClick={() => onNavigate(item)} 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)}`,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
width: '100%',
|
||||||
|
background: theme.colors.background.secondary,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
@ -1,14 +1,38 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React from 'react';
|
import { cloneDeep } from 'lodash';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2, NavSection } from '@grafana/data';
|
||||||
import { Dropdown, FilterInput, Icon, Menu, MenuItem, Tooltip, useStyles2 } from '@grafana/ui';
|
import { Dropdown, FilterInput, Icon, Tooltip, useStyles2, toIconName } from '@grafana/ui';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
|
import { StoreState } from 'app/types';
|
||||||
|
|
||||||
|
import { enrichConfigItems, enrichWithInteractionTracking } from '../NavBar/utils';
|
||||||
|
import { OrgSwitcher } from '../OrgSwitcher';
|
||||||
|
|
||||||
|
import { TopNavBarMenu } from './TopBar/TopNavBarMenu';
|
||||||
import { TOP_BAR_LEVEL_HEIGHT } from './types';
|
import { TOP_BAR_LEVEL_HEIGHT } from './types';
|
||||||
|
|
||||||
export function TopSearchBar() {
|
export function TopSearchBar() {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
const location = useLocation();
|
||||||
|
const navBarTree = useSelector((state: StoreState) => state.navBarTree);
|
||||||
|
const navTree = cloneDeep(navBarTree);
|
||||||
|
const [showSwitcherModal, setShowSwitcherModal] = useState(false);
|
||||||
|
const toggleSwitcherModal = () => {
|
||||||
|
setShowSwitcherModal(!showSwitcherModal);
|
||||||
|
};
|
||||||
|
const configItems = enrichConfigItems(
|
||||||
|
navTree.filter((item) => item.section === NavSection.Config),
|
||||||
|
location,
|
||||||
|
toggleSwitcherModal
|
||||||
|
).map((item) => enrichWithInteractionTracking(item, false));
|
||||||
|
|
||||||
|
const profileNode = configItems.find((item) => item.id === 'profile');
|
||||||
|
const signInNode = configItems.find((item) => item.id === 'signin');
|
||||||
|
const signInIconName = signInNode?.icon && toIconName(signInNode.icon);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@ -31,31 +55,26 @@ export function TopSearchBar() {
|
|||||||
<Icon name="rss" size="lg" />
|
<Icon name="rss" size="lg" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip placement="bottom" content="User profile (todo)">
|
{signInNode && (
|
||||||
<Dropdown overlay={ProfileMenu}>
|
<Tooltip placement="bottom" content="Sign in">
|
||||||
|
<a className={styles.actionItem} href={signInNode.url} target={signInNode.target}>
|
||||||
|
{signInIconName && <Icon name={signInIconName} size="lg" />}
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{profileNode && (
|
||||||
|
<Dropdown overlay={<TopNavBarMenu node={profileNode} />}>
|
||||||
<button className={styles.actionItem}>
|
<button className={styles.actionItem}>
|
||||||
<img src={contextSrv.user.gravatarUrl} />
|
<img src={contextSrv.user.gravatarUrl} />
|
||||||
</button>
|
</button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Tooltip>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{showSwitcherModal && <OrgSwitcher onDismiss={toggleSwitcherModal} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This is just temporary, needs syncing with the backend option like DisableSignoutMenu
|
|
||||||
*/
|
|
||||||
export function ProfileMenu() {
|
|
||||||
return (
|
|
||||||
<Menu>
|
|
||||||
<MenuItem url="profile" label="Your profile" />
|
|
||||||
<MenuItem url="profile/notifications" label="Your notifications" />
|
|
||||||
<MenuItem url="logout" label="Sign out" />
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
container: css({
|
container: css({
|
||||||
|
Loading…
Reference in New Issue
Block a user