diff --git a/packages/grafana-data/src/types/navModel.ts b/packages/grafana-data/src/types/navModel.ts index 23457500d37..fc9fad5e029 100644 --- a/packages/grafana-data/src/types/navModel.ts +++ b/packages/grafana-data/src/types/navModel.ts @@ -17,6 +17,7 @@ export interface NavModelItem { showOrgSwitcher?: boolean; onClick?: () => void; menuItemType?: NavMenuItemType; + highlightText?: string; } export enum NavSection { diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go index 59388d6c293..5afb1e62064 100644 --- a/pkg/api/dtos/index.go +++ b/pkg/api/dtos/index.go @@ -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 diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx index d3234f35a37..4bb5b60ce7f 100644 --- a/public/app/AppWrapper.tsx +++ b/public/app/AppWrapper.tsx @@ -37,6 +37,7 @@ export function addBodyRenderHook(fn: ComponentType) { export function addPageBanner(fn: ComponentType) { pageBanners.push(fn); } + export class AppWrapper extends React.Component { container = React.createRef(); diff --git a/public/app/core/components/NavBar/NavBar.tsx b/public/app/core/components/NavBar/NavBar.tsx index 81430427f64..a6eb62b503f 100644 --- a/public/app/core/components/NavBar/NavBar.tsx +++ b/public/app/core/components/NavBar/NavBar.tsx @@ -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(); diff --git a/public/app/core/components/NavBar/NavBarItem.tsx b/public/app/core/components/NavBar/NavBarItem.tsx index 84648fb38d2..c82d04b526b 100644 --- a/public/app/core/components/NavBar/NavBarItem.tsx +++ b/public/app/core/components/NavBar/NavBarItem.tsx @@ -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} diff --git a/public/app/core/components/NavBar/NavBarItemMenu.tsx b/public/app/core/components/NavBar/NavBarItemMenu.tsx index 000b996dc0e..bff23a204a1 100644 --- a/public/app/core/components/NavBar/NavBarItemMenu.tsx +++ b/public/app/core/components/NavBar/NavBarItemMenu.tsx @@ -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; diff --git a/public/app/core/components/NavBar/NavBarItemMenuItem.tsx b/public/app/core/components/NavBar/NavBarItemMenuItem.tsx index 785a4f3ac0a..8d945f7be1b 100644 --- a/public/app/core/components/NavBar/NavBarItemMenuItem.tsx +++ b/public/app/core/components/NavBar/NavBarItemMenuItem.tsx @@ -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; @@ -56,9 +57,16 @@ export function NavBarItemMenuItem({ item, state, onNavigate }: NavBarItemMenuIt }); return ( -
  • - {rendered} -
  • + <> +
  • + {rendered} +
  • + {item.value.highlightText && ( +
  • + +
  • + )} + ); } @@ -83,5 +91,11 @@ function getStyles(theme: GrafanaTheme2, isFocused: boolean, isSection: boolean) transition: none; } `, + upgradeBoxContainer: css` + padding: ${theme.spacing(1)}; + `, + upgradeBox: css` + width: 300px; + `, }; } diff --git a/public/app/core/components/NavBar/NavBarItemMenuTrigger.tsx b/public/app/core/components/NavBar/NavBarItemMenuTrigger.tsx index c93dcf0ff73..e048043cecc 100644 --- a/public/app/core/components/NavBar/NavBarItemMenuTrigger.tsx +++ b/public/app/core/components/NavBar/NavBarItemMenuTrigger.tsx @@ -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 = ( + + {item?.icon && } + {item?.img && {`${item.text}} + + ); let element = ( ); diff --git a/public/app/core/components/NavBar/NavBarItemWithoutMenu.tsx b/public/app/core/components/NavBar/NavBarItemWithoutMenu.tsx index c1f97555550..e76f828e93d 100644 --- a/public/app/core/components/NavBar/NavBarItemWithoutMenu.tsx +++ b/public/app/core/components/NavBar/NavBarItemWithoutMenu.tsx @@ -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 ? ( + + {children} + + ) : ( + {children} + ); + return (
  • {!url && ( )} {url && ( @@ -43,11 +54,11 @@ export function NavBarItemWithoutMenu({ onClick={onClick} aria-haspopup="true" > - {children} + {content} ) : ( - {children} + {content} )} @@ -104,6 +115,7 @@ export function getNavBarItemWithoutMenuStyles(theme: GrafanaTheme2, isActive?: transition: none; } `, + icon: css` height: 100%; width: 100%; diff --git a/public/app/core/components/NavBar/NavBarNext.tsx b/public/app/core/components/NavBar/NavBarNext.tsx index 41d9f8fed02..5cfd043f6b1 100644 --- a/public/app/core/components/NavBar/NavBarNext.tsx +++ b/public/app/core/components/NavBar/NavBarNext.tsx @@ -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(); diff --git a/public/app/core/components/NavBar/NavFeatureHighlight.tsx b/public/app/core/components/NavBar/NavFeatureHighlight.tsx new file mode 100644 index 00000000000..c698e959516 --- /dev/null +++ b/public/app/core/components/NavBar/NavFeatureHighlight.tsx @@ -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 ( +
    + {children} + +
    + ); +}; + +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%); + `, + }; +}; diff --git a/public/app/core/components/Upgrade/UpgradeBox.tsx b/public/app/core/components/Upgrade/UpgradeBox.tsx new file mode 100644 index 00000000000..126b29dbdca --- /dev/null +++ b/public/app/core/components/Upgrade/UpgradeBox.tsx @@ -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 { + text: string; +} + +export const UpgradeBox = ({ text, className, ...htmlProps }: Props) => { + const styles = useStyles2(getUpgradeBoxStyles); + + return ( +
    + +
    +
    You’ve found a Pro feature!
    +

    {text}

    + + Upgrade to Pro + +
    +
    + ); +}; + +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)}; + `, + }; +};