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;
|
||||
onClick?: () => void;
|
||||
menuItemType?: NavMenuItemType;
|
||||
highlightText?: string;
|
||||
}
|
||||
|
||||
export enum NavSection {
|
||||
|
@ -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
|
||||
|
@ -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>();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
||||
|
@ -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%;
|
||||
|
@ -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();
|
||||
|
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