mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Navigation: allow adding extra content (#44048)
* Add PRO badge * Allow adding extra content * Add extra content for the new navbar * Use highlight text instead of extra content * Trigger extra events * Remove ExtraContent * Update public/app/core/components/NavBar/NavFeatureHighlight.tsx Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com> * Remove redundant i * Add UpgradeBox * Move highlight to menu trigger * Clear navbar next * Cleanup * Fix UpgradeBox styles * Add arrow icon Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
parent
c0fc60dfef
commit
aead2e9157
@ -17,6 +17,7 @@ export interface NavModelItem {
|
|||||||
showOrgSwitcher?: boolean;
|
showOrgSwitcher?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
menuItemType?: NavMenuItemType;
|
menuItemType?: NavMenuItemType;
|
||||||
|
highlightText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum NavSection {
|
export enum NavSection {
|
||||||
|
@ -70,6 +70,7 @@ type NavLink struct {
|
|||||||
HideFromMenu bool `json:"hideFromMenu,omitempty"`
|
HideFromMenu bool `json:"hideFromMenu,omitempty"`
|
||||||
HideFromTabs bool `json:"hideFromTabs,omitempty"`
|
HideFromTabs bool `json:"hideFromTabs,omitempty"`
|
||||||
Children []*NavLink `json:"children,omitempty"`
|
Children []*NavLink `json:"children,omitempty"`
|
||||||
|
HighlightText string `json:"highlightText,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NavIDCfg is the id for org configuration navigation node
|
// NavIDCfg is the id for org configuration navigation node
|
||||||
|
@ -37,6 +37,7 @@ export function addBodyRenderHook(fn: ComponentType) {
|
|||||||
export function addPageBanner(fn: ComponentType) {
|
export function addPageBanner(fn: ComponentType) {
|
||||||
pageBanners.push(fn);
|
pageBanners.push(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState> {
|
export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState> {
|
||||||
container = React.createRef<HTMLDivElement>();
|
container = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { FC, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
@ -28,7 +28,7 @@ const searchItem: NavModelItem = {
|
|||||||
icon: 'search',
|
icon: 'search',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NavBar: FC = React.memo(() => {
|
export const NavBar = React.memo(() => {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
@ -42,6 +42,7 @@ const NavBarItem = ({
|
|||||||
menuItemType: NavMenuItemType.Section,
|
menuItemType: NavMenuItemType.Section,
|
||||||
};
|
};
|
||||||
const items: NavModelItem[] = [section].concat(filteredItems);
|
const items: NavModelItem[] = [section].concat(filteredItems);
|
||||||
|
|
||||||
const onNavigate = (item: NavModelItem) => {
|
const onNavigate = (item: NavModelItem) => {
|
||||||
const { url, target, onClick } = item;
|
const { url, target, onClick } = item;
|
||||||
if (!url) {
|
if (!url) {
|
||||||
@ -107,6 +108,7 @@ const NavBarItem = ({
|
|||||||
url={link.url}
|
url={link.url}
|
||||||
onClick={link.onClick}
|
onClick={link.onClick}
|
||||||
target={link.target}
|
target={link.target}
|
||||||
|
highlightText={link.highlightText}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</NavBarItemWithoutMenu>
|
</NavBarItemWithoutMenu>
|
||||||
|
@ -89,7 +89,6 @@ function getStyles(theme: GrafanaTheme2, reverseDirection?: boolean) {
|
|||||||
top: ${reverseDirection ? 'auto' : 0};
|
top: ${reverseDirection ? 'auto' : 0};
|
||||||
transition: ${theme.transitions.create('opacity')};
|
transition: ${theme.transitions.create('opacity')};
|
||||||
z-index: ${theme.zIndex.sidemenu};
|
z-index: ${theme.zIndex.sidemenu};
|
||||||
list-style: none;
|
|
||||||
`,
|
`,
|
||||||
subtitle: css`
|
subtitle: css`
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -9,6 +9,7 @@ import { mergeProps } from '@react-aria/utils';
|
|||||||
import { Node } from '@react-types/shared';
|
import { Node } from '@react-types/shared';
|
||||||
|
|
||||||
import { useNavBarItemMenuContext } from './context';
|
import { useNavBarItemMenuContext } from './context';
|
||||||
|
import { UpgradeBox } from '../Upgrade/UpgradeBox';
|
||||||
|
|
||||||
export interface NavBarItemMenuItemProps {
|
export interface NavBarItemMenuItemProps {
|
||||||
item: Node<NavModelItem>;
|
item: Node<NavModelItem>;
|
||||||
@ -56,9 +57,16 @@ export function NavBarItemMenuItem({ item, state, onNavigate }: NavBarItemMenuIt
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<li {...mergeProps(menuItemProps, focusProps, keyboardProps)} ref={ref} className={styles.menuItem}>
|
<li {...mergeProps(menuItemProps, focusProps, keyboardProps)} ref={ref} className={styles.menuItem}>
|
||||||
{rendered}
|
{rendered}
|
||||||
</li>
|
</li>
|
||||||
|
{item.value.highlightText && (
|
||||||
|
<li className={styles.upgradeBoxContainer}>
|
||||||
|
<UpgradeBox text={item.value.highlightText} className={styles.upgradeBox} />
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,5 +91,11 @@ function getStyles(theme: GrafanaTheme2, isFocused: boolean, isSection: boolean)
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
upgradeBoxContainer: css`
|
||||||
|
padding: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
upgradeBox: css`
|
||||||
|
width: 300px;
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import { DismissButton, useOverlay } from '@react-aria/overlays';
|
|||||||
import { FocusScope } from '@react-aria/focus';
|
import { FocusScope } from '@react-aria/focus';
|
||||||
|
|
||||||
import { NavBarItemMenuContext } from './context';
|
import { NavBarItemMenuContext } from './context';
|
||||||
|
import { NavFeatureHighlight } from './NavFeatureHighlight';
|
||||||
|
|
||||||
export interface NavBarItemMenuTriggerProps extends MenuTriggerProps {
|
export interface NavBarItemMenuTriggerProps extends MenuTriggerProps {
|
||||||
children: ReactElement;
|
children: ReactElement;
|
||||||
@ -72,6 +73,12 @@ export function NavBarItemMenuTrigger(props: NavBarItemMenuTriggerProps): ReactE
|
|||||||
// Get props for the button based on the trigger props from useMenuTrigger
|
// Get props for the button based on the trigger props from useMenuTrigger
|
||||||
const { buttonProps } = useButton(menuTriggerProps, ref);
|
const { buttonProps } = useButton(menuTriggerProps, ref);
|
||||||
|
|
||||||
|
const buttonContent = (
|
||||||
|
<span className={styles.icon}>
|
||||||
|
{item?.icon && <Icon name={item.icon as IconName} size="xl" />}
|
||||||
|
{item?.img && <img src={item.img} alt={`${item.text} logo`} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
let element = (
|
let element = (
|
||||||
<button
|
<button
|
||||||
className={styles.element}
|
className={styles.element}
|
||||||
@ -81,10 +88,7 @@ export function NavBarItemMenuTrigger(props: NavBarItemMenuTriggerProps): ReactE
|
|||||||
onClick={item?.onClick}
|
onClick={item?.onClick}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
>
|
>
|
||||||
<span className={styles.icon}>
|
{item.highlightText ? <NavFeatureHighlight>{buttonContent}</NavFeatureHighlight> : buttonContent}
|
||||||
{item?.icon && <Icon name={item.icon as IconName} size="xl" />}
|
|
||||||
{item?.img && <img src={item.img} alt={`${item.text} logo`} />}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { GrafanaTheme2 } from '../../../../../packages/grafana-data';
|
|
||||||
import { css, cx } from '@emotion/css';
|
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { Link, useTheme2 } from '../../../../../packages/grafana-ui';
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Link, useTheme2 } from '@grafana/ui';
|
||||||
|
import { NavFeatureHighlight } from './NavFeatureHighlight';
|
||||||
|
|
||||||
export interface NavBarItemWithoutMenuProps {
|
export interface NavBarItemWithoutMenuProps {
|
||||||
label: string;
|
label: string;
|
||||||
@ -11,6 +12,7 @@ export interface NavBarItemWithoutMenuProps {
|
|||||||
target?: string;
|
target?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
highlightText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NavBarItemWithoutMenu({
|
export function NavBarItemWithoutMenu({
|
||||||
@ -21,15 +23,24 @@ export function NavBarItemWithoutMenu({
|
|||||||
target,
|
target,
|
||||||
isActive = false,
|
isActive = false,
|
||||||
onClick,
|
onClick,
|
||||||
|
highlightText,
|
||||||
}: NavBarItemWithoutMenuProps) {
|
}: NavBarItemWithoutMenuProps) {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getNavBarItemWithoutMenuStyles(theme, isActive);
|
const styles = getNavBarItemWithoutMenuStyles(theme, isActive);
|
||||||
|
|
||||||
|
const content = highlightText ? (
|
||||||
|
<NavFeatureHighlight>
|
||||||
|
<span className={styles.icon}>{children}</span>
|
||||||
|
</NavFeatureHighlight>
|
||||||
|
) : (
|
||||||
|
<span className={styles.icon}>{children}</span>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={cx(styles.container, className)}>
|
<li className={cx(styles.container, className)}>
|
||||||
{!url && (
|
{!url && (
|
||||||
<button className={styles.element} onClick={onClick} aria-label={label}>
|
<button className={styles.element} onClick={onClick} aria-label={label}>
|
||||||
<span className={styles.icon}>{children}</span>
|
{content}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{url && (
|
{url && (
|
||||||
@ -43,11 +54,11 @@ export function NavBarItemWithoutMenu({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
>
|
>
|
||||||
<span className={styles.icon}>{children}</span>
|
{content}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<a href={url} target={target} className={styles.element} onClick={onClick} aria-label={label}>
|
<a href={url} target={target} className={styles.element} onClick={onClick} aria-label={label}>
|
||||||
<span className={styles.icon}>{children}</span>
|
{content}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -104,6 +115,7 @@ export function getNavBarItemWithoutMenuStyles(theme: GrafanaTheme2, isActive?:
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
||||||
icon: css`
|
icon: css`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { FC, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
@ -26,7 +26,7 @@ const searchItem: NavModelItem = {
|
|||||||
icon: 'search',
|
icon: 'search',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NavBarNext: FC = React.memo(() => {
|
export const NavBarNext = React.memo(() => {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
33
public/app/core/components/NavBar/NavFeatureHighlight.tsx
Normal file
33
public/app/core/components/NavBar/NavFeatureHighlight.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavFeatureHighlight = ({ children }: Props): JSX.Element => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{children}
|
||||||
|
<span className={styles.highlight} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
highlight: css`
|
||||||
|
background-color: ${theme.colors.success.main};
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
62
public/app/core/components/Upgrade/UpgradeBox.tsx
Normal file
62
public/app/core/components/Upgrade/UpgradeBox.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React, { HTMLAttributes } from 'react';
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { Icon, LinkButton, useStyles2 } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
|
||||||
|
export interface Props extends HTMLAttributes<HTMLOrSVGElement> {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpgradeBox = ({ text, className, ...htmlProps }: Props) => {
|
||||||
|
const styles = useStyles2(getUpgradeBoxStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx(styles.box, className)} {...htmlProps}>
|
||||||
|
<Icon name={'arrow-up'} className={styles.icon} />
|
||||||
|
<div>
|
||||||
|
<h6>You’ve found a Pro feature!</h6>
|
||||||
|
<p className={styles.text}>{text}</p>
|
||||||
|
<LinkButton
|
||||||
|
variant="primary"
|
||||||
|
size={'sm'}
|
||||||
|
className={styles.button}
|
||||||
|
href="https://grafana.com/profile/org/subscription"
|
||||||
|
target="__blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Upgrade to Pro
|
||||||
|
</LinkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUpgradeBoxStyles = (theme: GrafanaTheme2) => {
|
||||||
|
const borderRadius = theme.shape.borderRadius(2);
|
||||||
|
|
||||||
|
return {
|
||||||
|
box: css`
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
border-radius: ${borderRadius};
|
||||||
|
background: ${theme.colors.primary.transparent};
|
||||||
|
border: 1px solid ${theme.colors.primary.shade};
|
||||||
|
padding: ${theme.spacing(2)};
|
||||||
|
color: ${theme.colors.primary.text};
|
||||||
|
font-size: ${theme.typography.bodySmall.fontSize};
|
||||||
|
text-align: left;
|
||||||
|
line-height: 16px;
|
||||||
|
`,
|
||||||
|
text: css`
|
||||||
|
margin-bottom: 0;
|
||||||
|
`,
|
||||||
|
button: css`
|
||||||
|
margin-top: ${theme.spacing(2)};
|
||||||
|
`,
|
||||||
|
icon: css`
|
||||||
|
border: 1px solid ${theme.colors.primary.shade};
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: ${theme.spacing(0.5, 1, 0.5, 0.5)};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user