diff --git a/.changeset/swift-carpets-yawn.md b/.changeset/swift-carpets-yawn.md new file mode 100644 index 0000000000..d242b3cf81 --- /dev/null +++ b/.changeset/swift-carpets-yawn.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Adding hover message to the Gauge and an info icon to the GaugeCard. diff --git a/packages/core-components/src/components/ProgressBars/Gauge.tsx b/packages/core-components/src/components/ProgressBars/Gauge.tsx index 0336ebc990..bb91358133 100644 --- a/packages/core-components/src/components/ProgressBars/Gauge.tsx +++ b/packages/core-components/src/components/ProgressBars/Gauge.tsx @@ -17,7 +17,8 @@ import { BackstagePalette, BackstageTheme } from '@backstage/theme'; import { makeStyles, useTheme } from '@material-ui/core/styles'; import { Circle } from 'rc-progress'; -import React from 'react'; +import { useHover } from '../../hooks'; +import React, { ReactNode } from 'react'; /** @public */ export type GaugeClassKey = 'root' | 'overlay' | 'circle' | 'colorUnknown'; @@ -37,6 +38,15 @@ const useStyles = makeStyles( fontWeight: 'bold', color: theme.palette.textContrast, }, + hoveringMessageCompliant: { + fontSize: 13, + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + position: 'absolute', + wordBreak: 'break-all', + display: 'inline-block', + }, circle: { width: '80%', transform: 'translate(10%, 0)', @@ -53,6 +63,7 @@ export type GaugeProps = { inverse?: boolean; unit?: string; max?: number; + hoverMessage?: ReactNode; getColor?: GaugePropsGetColor; }; @@ -104,10 +115,11 @@ export const getProgressColor: GaugePropsGetColor = ({ */ export function Gauge(props: GaugeProps) { + const [hoverRef, isHovering] = useHover() as any; const { getColor = getProgressColor } = props; const classes = useStyles(props); const { palette } = useTheme(); - const { value, fractional, inverse, unit, max } = { + const { value, fractional, inverse, unit, max, hoverMessage } = { ...defaultGaugeProps, ...props, }; @@ -116,7 +128,7 @@ export function Gauge(props: GaugeProps) { const asActual = max !== 100 ? Math.round(value) : asPercentage; return ( -
+
-
- {isNaN(value) ? 'N/A' : `${asActual}${unit}`} -
+ {hoverMessage && isHovering ? ( +
{hoverMessage}
+ ) : ( +
+ {isNaN(value) ? 'N/A' : `${asActual}${unit}`} +
+ )}
); } diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx index 64a6cc1b16..5ce96310b1 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.stories.tsx @@ -118,3 +118,71 @@ export const StaticColor = () => ( ); + +export const InfoMessage = () => ( + + + + + + + + + + + + + + +); + +export const HoverMessage = () => ( + + + + + + + + + + + + + + +); diff --git a/packages/core-components/src/components/ProgressBars/GaugeCard.tsx b/packages/core-components/src/components/ProgressBars/GaugeCard.tsx index 9e6c19ebfe..745c37d480 100644 --- a/packages/core-components/src/components/ProgressBars/GaugeCard.tsx +++ b/packages/core-components/src/components/ProgressBars/GaugeCard.tsx @@ -15,7 +15,7 @@ */ import { makeStyles } from '@material-ui/core/styles'; -import React from 'react'; +import React, { ReactNode } from 'react'; import { BottomLinkProps } from '../../layout/BottomLink'; import { InfoCard, InfoCardVariants } from '../../layout/InfoCard'; import { Gauge, GaugePropsGetColor } from './Gauge'; @@ -26,6 +26,8 @@ type Props = { variant?: InfoCardVariants; /** Progress in % specified as decimal, e.g. "0.23" */ progress: number; + hoverMessage?: ReactNode; + iconInfoMessage?: string; inverse?: boolean; deepLink?: BottomLinkProps; getColor?: GaugePropsGetColor; @@ -52,11 +54,21 @@ const useStyles = makeStyles( */ export function GaugeCard(props: Props) { const classes = useStyles(props); - const { title, subheader, progress, inverse, deepLink, variant, getColor } = - props; + const { + title, + subheader, + progress, + inverse, + deepLink, + hoverMessage, + iconInfoMessage, + variant, + getColor, + } = props; const gaugeProps = { inverse, + hoverMessage, getColor, value: progress, }; @@ -68,6 +80,7 @@ export function GaugeCard(props: Props) { subheader={subheader} deepLink={deepLink} variant={variant} + iconInfoMessage={iconInfoMessage} > diff --git a/packages/core-components/src/hooks/index.ts b/packages/core-components/src/hooks/index.ts index 07e585bf27..afaf68e7d9 100644 --- a/packages/core-components/src/hooks/index.ts +++ b/packages/core-components/src/hooks/index.ts @@ -16,6 +16,7 @@ export { useQueryParamState } from './useQueryParamState'; export { useSupportConfig } from './useSupportConfig'; +export { useHover } from './useHover'; export type { SupportConfig, SupportItem, diff --git a/packages/core-components/src/hooks/useHover.ts b/packages/core-components/src/hooks/useHover.ts new file mode 100644 index 0000000000..478f1fb4e3 --- /dev/null +++ b/packages/core-components/src/hooks/useHover.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useEffect, useRef, useState } from 'react'; + +export const useHover = () => { + const [value, setValue] = useState(false); + + const ref = useRef(null); + const handleMouseOver = () => setValue(true); + const handleMouseOut = () => setValue(false); + + useEffect(() => { + const node = ref.current as any; + if (node) { + node.addEventListener('mouseenter', handleMouseOver) as any; + node.addEventListener('mouseleave', handleMouseOut) as any; + + return () => { + node.removeEventListener('mouseenter', handleMouseOver); + node.removeEventListener('mouseleave', handleMouseOut); + }; + } + return () => { + setValue(false); + }; + }, [ref]); + + return [ref, value]; +}; diff --git a/packages/core-components/src/layout/InfoCard/InfoCard.tsx b/packages/core-components/src/layout/InfoCard/InfoCard.tsx index f1e67904f1..70ffd2e0cd 100644 --- a/packages/core-components/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core-components/src/layout/InfoCard/InfoCard.tsx @@ -24,6 +24,8 @@ import classNames from 'classnames'; import React, { ReactNode } from 'react'; import { BottomLink, BottomLinkProps } from '../BottomLink'; import { ErrorBoundary, ErrorBoundaryProps } from '../ErrorBoundary'; +import Info from '@material-ui/icons/Info'; +import Tooltip from '@material-ui/core/Tooltip'; /** @public */ export type InfoCardClassKey = @@ -55,6 +57,15 @@ const useStyles = makeStyles( headerAvatar: {}, headerAction: {}, headerContent: {}, + leftIcon: { + float: 'right', + }, + tooltip: { + fontSize: 14, + }, + subheader: { + float: 'left', + }, }), { name: 'BackstageInfoCard' }, ); @@ -134,6 +145,7 @@ type Props = { children?: ReactNode; headerStyle?: object; headerProps?: CardHeaderProps; + iconInfoMessage?: ReactNode; action?: ReactNode; actionsClassName?: string; actions?: ReactNode; @@ -162,6 +174,7 @@ export function InfoCard(props: Props): JSX.Element { children, headerStyle, headerProps, + iconInfoMessage, action, actionsClassName, actions, @@ -194,6 +207,23 @@ export function InfoCard(props: Props): JSX.Element { }); } + const cardSubTitle = () => { + return ( +
+ {subheader &&
{subheader}
} + {iconInfoMessage && ( + + + + )} +
+ ); + }; + const errProps: ErrorBoundaryProps = errorBoundaryProps || (slackChannel ? { slackChannel } : {}); @@ -211,7 +241,7 @@ export function InfoCard(props: Props): JSX.Element { content: classes.headerContent, }} title={title} - subheader={subheader} + subheader={cardSubTitle()} action={action} style={{ ...headerStyle }} titleTypographyProps={titleTypographyProps}