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:
Alex Khomenko 2022-01-25 10:04:44 +02:00 committed by GitHub
parent c0fc60dfef
commit aead2e9157
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 161 additions and 32 deletions

View File

@ -17,6 +17,7 @@ export interface NavModelItem {
showOrgSwitcher?: boolean;
onClick?: () => void;
menuItemType?: NavMenuItemType;
highlightText?: string;
}
export enum NavSection {

View File

@ -56,20 +56,21 @@ const (
)
type NavLink struct {
Id string `json:"id,omitempty"`
Text string `json:"text,omitempty"`
Description string `json:"description,omitempty"`
Section string `json:"section,omitempty"`
SubTitle string `json:"subTitle,omitempty"`
Icon string `json:"icon,omitempty"`
Img string `json:"img,omitempty"`
Url string `json:"url,omitempty"`
Target string `json:"target,omitempty"`
SortWeight int64 `json:"sortWeight,omitempty"`
Divider bool `json:"divider,omitempty"`
HideFromMenu bool `json:"hideFromMenu,omitempty"`
HideFromTabs bool `json:"hideFromTabs,omitempty"`
Children []*NavLink `json:"children,omitempty"`
Id string `json:"id,omitempty"`
Text string `json:"text,omitempty"`
Description string `json:"description,omitempty"`
Section string `json:"section,omitempty"`
SubTitle string `json:"subTitle,omitempty"`
Icon string `json:"icon,omitempty"`
Img string `json:"img,omitempty"`
Url string `json:"url,omitempty"`
Target string `json:"target,omitempty"`
SortWeight int64 `json:"sortWeight,omitempty"`
Divider bool `json:"divider,omitempty"`
HideFromMenu bool `json:"hideFromMenu,omitempty"`
HideFromTabs bool `json:"hideFromTabs,omitempty"`
Children []*NavLink `json:"children,omitempty"`
HighlightText string `json:"highlightText,omitempty"`
}
// NavIDCfg is the id for org configuration navigation node

View File

@ -37,6 +37,7 @@ export function addBodyRenderHook(fn: ComponentType) {
export function addPageBanner(fn: ComponentType) {
pageBanners.push(fn);
}
export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState> {
container = React.createRef<HTMLDivElement>();

View File

@ -1,4 +1,4 @@
import React, { FC, useState } from 'react';
import React, { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { css, cx } from '@emotion/css';
import { cloneDeep } from 'lodash';
@ -28,7 +28,7 @@ const searchItem: NavModelItem = {
icon: 'search',
};
export const NavBar: FC = React.memo(() => {
export const NavBar = React.memo(() => {
const theme = useTheme2();
const styles = getStyles(theme);
const location = useLocation();

View File

@ -42,6 +42,7 @@ const NavBarItem = ({
menuItemType: NavMenuItemType.Section,
};
const items: NavModelItem[] = [section].concat(filteredItems);
const onNavigate = (item: NavModelItem) => {
const { url, target, onClick } = item;
if (!url) {
@ -107,6 +108,7 @@ const NavBarItem = ({
url={link.url}
onClick={link.onClick}
target={link.target}
highlightText={link.highlightText}
>
{children}
</NavBarItemWithoutMenu>

View File

@ -89,7 +89,6 @@ function getStyles(theme: GrafanaTheme2, reverseDirection?: boolean) {
top: ${reverseDirection ? 'auto' : 0};
transition: ${theme.transitions.create('opacity')};
z-index: ${theme.zIndex.sidemenu};
list-style: none;
`,
subtitle: css`
background-color: transparent;

View File

@ -9,6 +9,7 @@ import { mergeProps } from '@react-aria/utils';
import { Node } from '@react-types/shared';
import { useNavBarItemMenuContext } from './context';
import { UpgradeBox } from '../Upgrade/UpgradeBox';
export interface NavBarItemMenuItemProps {
item: Node<NavModelItem>;
@ -56,9 +57,16 @@ export function NavBarItemMenuItem({ item, state, onNavigate }: NavBarItemMenuIt
});
return (
<li {...mergeProps(menuItemProps, focusProps, keyboardProps)} ref={ref} className={styles.menuItem}>
{rendered}
</li>
<>
<li {...mergeProps(menuItemProps, focusProps, keyboardProps)} ref={ref} className={styles.menuItem}>
{rendered}
</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;
}
`,
upgradeBoxContainer: css`
padding: ${theme.spacing(1)};
`,
upgradeBox: css`
width: 300px;
`,
};
}

View File

@ -11,6 +11,7 @@ import { DismissButton, useOverlay } from '@react-aria/overlays';
import { FocusScope } from '@react-aria/focus';
import { NavBarItemMenuContext } from './context';
import { NavFeatureHighlight } from './NavFeatureHighlight';
export interface NavBarItemMenuTriggerProps extends MenuTriggerProps {
children: ReactElement;
@ -72,6 +73,12 @@ export function NavBarItemMenuTrigger(props: NavBarItemMenuTriggerProps): ReactE
// Get props for the button based on the trigger props from useMenuTrigger
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 = (
<button
className={styles.element}
@ -81,10 +88,7 @@ export function NavBarItemMenuTrigger(props: NavBarItemMenuTriggerProps): ReactE
onClick={item?.onClick}
aria-label={label}
>
<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>
{item.highlightText ? <NavFeatureHighlight>{buttonContent}</NavFeatureHighlight> : buttonContent}
</button>
);

View File

@ -1,7 +1,8 @@
import { GrafanaTheme2 } from '../../../../../packages/grafana-data';
import { css, cx } from '@emotion/css';
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 {
label: string;
@ -11,6 +12,7 @@ export interface NavBarItemWithoutMenuProps {
target?: string;
isActive?: boolean;
onClick?: () => void;
highlightText?: string;
}
export function NavBarItemWithoutMenu({
@ -21,15 +23,24 @@ export function NavBarItemWithoutMenu({
target,
isActive = false,
onClick,
highlightText,
}: NavBarItemWithoutMenuProps) {
const theme = useTheme2();
const styles = getNavBarItemWithoutMenuStyles(theme, isActive);
const content = highlightText ? (
<NavFeatureHighlight>
<span className={styles.icon}>{children}</span>
</NavFeatureHighlight>
) : (
<span className={styles.icon}>{children}</span>
);
return (
<li className={cx(styles.container, className)}>
{!url && (
<button className={styles.element} onClick={onClick} aria-label={label}>
<span className={styles.icon}>{children}</span>
{content}
</button>
)}
{url && (
@ -43,11 +54,11 @@ export function NavBarItemWithoutMenu({
onClick={onClick}
aria-haspopup="true"
>
<span className={styles.icon}>{children}</span>
{content}
</Link>
) : (
<a href={url} target={target} className={styles.element} onClick={onClick} aria-label={label}>
<span className={styles.icon}>{children}</span>
{content}
</a>
)}
</>
@ -104,6 +115,7 @@ export function getNavBarItemWithoutMenuStyles(theme: GrafanaTheme2, isActive?:
transition: none;
}
`,
icon: css`
height: 100%;
width: 100%;

View File

@ -1,4 +1,4 @@
import React, { FC, useState } from 'react';
import React, { useState } from 'react';
import { useLocation } from 'react-router-dom';
import { css, cx } from '@emotion/css';
import { cloneDeep } from 'lodash';
@ -26,7 +26,7 @@ const searchItem: NavModelItem = {
icon: 'search',
};
export const NavBarNext: FC = React.memo(() => {
export const NavBarNext = React.memo(() => {
const theme = useTheme2();
const styles = getStyles(theme);
const location = useLocation();

View 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%);
`,
};
};

View 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>Youve 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)};
`,
};
};