diff --git a/src/lib/components/globalhealthbar/CustomTooltip.tsx b/src/lib/components/globalhealthbar/CustomTooltip.tsx new file mode 100644 index 0000000000..836ea6b220 --- /dev/null +++ b/src/lib/components/globalhealthbar/CustomTooltip.tsx @@ -0,0 +1,88 @@ +import { useEffect, useRef, useState } from 'react'; +import styled, { css, useTheme } from 'styled-components'; +import { Box } from '../../next'; +import { Text, FormattedDateTime, Wrap, spacing } from '../../index'; + +const TootlipContainer = styled.div<{ tooltipInset }>` + ${(props) => { + const theme = useTheme(); + return css` + border: 1px solid ${theme.border}; + width: 24rem; + color: ${theme.textSecondary}; + background-color: ${theme.backgroundLevel1}; + border-radius: 4px; + position: absolute; + inset: ${props.tooltipInset.top}px auto auto ${props.tooltipInset.left}px; + padding: ${spacing.r8}; + font-size: 1rem; + `; + }} +`; + +export const CustomTooltip = (props) => { + const { tooltipData, coordinate } = props; + const tooltipRef = useRef(null); + const [tooltipInset, setTooltipInset] = useState({ top: 0, left: 0 }); + + useEffect(() => { + if (tooltipRef.current) { + // console.log('tooltip', tooltipRef.current); + // console.log('tooltipCoord', tooltipRef.current.getBoundingClientRect()); + // left and top < 0 = tooltip is out of the screen + // right or bottom > window.innerWidth or window.innerheight = tooltip is out of the screen + + setTooltipInset({ + left: coordinate.x - tooltipRef.current.offsetWidth / 2, + top: coordinate.y + 20, + }); + } + }, [tooltipRef.current, coordinate]); + if (tooltipData) { + const { payload, name } = tooltipData[0]; + const tooltipName = name.replace('range', ''); + return ( + + + View details on Alert Page + + + + Severity: + {payload[`${tooltipName}Severity`]} + + + Start: + + + + + + End: + + + + + + Description: + {payload[`${tooltipName}Description`]} + + + + ); + } + + return null; +}; diff --git a/src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx b/src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx index 2a2fbf017a..ea5fd4b6e8 100644 --- a/src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx +++ b/src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx @@ -1,33 +1,11 @@ -import { - Bar, - BarChart, - XAxis, - YAxis, - Tooltip, - Rectangle, - Customized, - CartesianGrid, -} from 'recharts'; -import styled, { useTheme } from 'styled-components'; -import { useEffect, useRef, useState } from 'react'; -import { Wrap, spacing } from '../../spacing'; - -import { FocusVisibleStyle } from '../buttonv2/Buttonv2.component'; -import { - DATE_FORMATER, - FormattedDateTime, - TIME_FORMATER, - TIME_SECOND_FORMATER, -} from '../date/FormattedDateTime'; +import { Bar, BarChart, XAxis, YAxis, Tooltip, Rectangle } from 'recharts'; +import { useTheme } from 'styled-components'; +import { useEffect, useState } from 'react'; import { useHistoryAlert } from './HistoryProvider'; -import { Box } from '../box/Box'; -import { Text } from '../text/Text.component'; -import { Icon } from '../icon/Icon.component'; -import { get } from 'styled-system'; -import { RectRadius } from 'recharts/types/shape/Rectangle'; +import { getDataListOptions, getRadius, getTickFormatter } from './utils'; +import { HistoryAlertSlider } from './HistorySlider'; +import { CustomTooltip } from './CustomTooltip'; -export const TOP = 'top'; -export const BOTTOM = 'bottom'; export type GlobalHealthProps = { id: string; alerts: { @@ -39,130 +17,22 @@ export type GlobalHealthProps = { start: string; end: string; }; - -export const StyledRange = styled.input` - width: 600px; - padding: 0; /* nécessaire pour IE */ - margin: 0; - margin-top: -1px; - appearance: none; /* nécessaire pour IE */ - -moz-appearance: none; /* nécessaire pour Firefox */ - -webkit-appearance: none; /* nécessaire pour Chrome */ - font: inherit; /* même rendu suivant font document */ - outline: none; - opacity: 1; - background: transparent; /* sert pour couleur de fond de la zone de déplacement */ - box-sizing: content-box; /* même modèle de boîte pour tous */ - transition: opacity 0.2s; - cursor: pointer; - position: absolute; - z-index: 10; - height: 16px; - :focus-visible::-webkit-slider-thumb { - ${FocusVisibleStyle} - } - /*==============================*/ - /* cursor */ - /*==============================*/ - &::-webkit-slider-thumb { - -webkit-appearance: none; - padding: 0; - appearance: none; - margin: 0; - width: 3px; - height: 16px; - background-color: ${(props) => props.theme.selectedActive}; - } - &::-moz-range-thumb { - margin: 0; - width: 2px; - height: 16px; - background-color: ${(props) => props.theme.selectedActive}; - border: none; - } -`; - -export const StyledDataList = styled.datalist` - display: none; -`; +const barWidth = 600; // width of the bar chart export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) { const history = useHistoryAlert(); - const [isVisible, setIsVisible] = useState(false); - const [isFocused, setIsFocused] = useState(false); const [tooltipData, setTooltipData] = useState(null); - const popoverRef = useRef(null); const theme = useTheme(); useEffect(() => { - setIsVisible(true); - }, [history.selectedDate]); - - // TODO : Border radius on alerts close to the start and end - // TODO : + if (history.selectedDate === 0) { + history.setSelectedDate(endDate); + } + }, []); - // TODO change tooltip position if it's out of the screen const startDate = new Date(start).getTime(); const endDate = new Date(end).getTime(); - const oneHour = 60 * 60 * 1000; - const oneDay = 24 * oneHour; - const oneWeek = 7 * oneDay; - const span = endDate - startDate; - - // get 20%, 40%, 60%, 80% of span interval for ticks - // value = (percentage * (max - min) / 100) + min - const getDataListOptions = (startDate, endDate) => { - const oneDay = 24 * 60 * 60 * 1000; - const span = endDate - startDate; - if (span === 7 * oneDay) { - return Array.from({ length: 6 }, (_, i) => endDate - (i + 1) * oneDay); - } - return Array.from({ length: 4 }, (_, i) => endDate - ((i + 1) / 5) * span); - }; - const getStep = (startDate, endDate) => { - const oneHour = 60 * 60 * 1000; - const oneDay = 24 * oneHour; - const span = endDate - startDate; - if (span === 7 * oneDay) { - return oneHour; - } else if (span === oneDay) { - return oneHour / 4; - } else if (span === oneHour) { - return 60 * 1000; - } - }; - const getRadius = (start, end, startDate, endDate): RectRadius => { - const marge = span >= oneDay ? 0.011 * span : 0; - // TODO need to correct the conditions (don't take into || only &&) - if (start === startDate && end === endDate) { - return [15, 15, 15, 15]; - } else if (start <= startDate + marge && end >= endDate - marge) { - return [6, 6, 6, 6]; - } else if (start === startDate) { - return [15, 0, 0, 15]; - } else if (end === endDate) { - return [0, 15, 15, 0]; - } else if (start <= startDate + marge) { - return [6, 0, 0, 6]; - } else if (end >= endDate - marge) { - return [0, 6, 6, 0]; - } else { - return [0, 0, 0, 0]; - } - }; - - const setHistoryTooltipPosition = (): string => { - const history = useHistoryAlert(); - if (history.selectedDate && popoverRef.current) { - const width = - ((history.selectedDate - startDate) / (endDate - startDate)) * 600; - const leftPosition = width - popoverRef.current.offsetWidth / 2; - return `auto auto -4px ${leftPosition}px`; - } - return 'auto'; - }; - const data = [ { start: startDate, @@ -188,165 +58,46 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) { key.startsWith('rangecritical'), ); - const toggleVisibility = (e) => { - if (!isFocused) { - if (e.type === 'mouseleave') setIsVisible(false); - } - if (e.type === 'mouseenter') setIsVisible(true); - if (e.type === 'blur') { - setIsVisible(false); - setIsFocused(false); - } - if (e.type === 'focus') { - setIsVisible(true); - setIsFocused(true); - } - }; - const CustomTooltip = (props) => { - const { tooltipData, coordinate } = props; - const tooltipRef = useRef(null); - const [tooltipInset, setTooltipInset] = useState({ top: 0, left: 0 }); - - useEffect(() => { - if (tooltipRef.current) { - // console.log('tooltip', tooltipRef.current); - // console.log('tooltipCoord', tooltipRef.current.getBoundingClientRect()); - // left and top < 0 = tooltip is out of the screen - // right or bottom > window.innerWidth or window.innerheight = tooltip is out of the screen - - setTooltipInset({ - left: coordinate.x - tooltipRef.current.offsetWidth / 2, - top: coordinate.y + 20, - }); - } - }, [tooltipRef.current, coordinate]); - if (tooltipData) { - const { payload, name } = tooltipData[0]; - const tooltipName = name.replace('range', ''); - return ( -
- - View details on Alert Page - - - - Severity: - {payload[`${tooltipName}Severity`]} - - - Start: - - - - End: - - - - Description: - {payload[`${tooltipName}Description`]} - - -
- ); - } - - return null; + const rectangleRenderer = (props, key) => { + const { x, y, height, fill } = props; + + const start = props[key][0] < startDate ? startDate : props[key][0]; + const end = props[key][1] > endDate ? endDate : props[key][1]; + const relativeSize = (end - start) / (endDate - startDate); + return ( + + ); }; return ( -
- {history.selectedDate !== null && ( -
-
-
- -
- -
- - { - if (e.target.valueAsNumber > endDate) - history.setSelectedDate(endDate); - if (e.target.valueAsNumber < startDate) - history.setSelectedDate(startDate); - history.setSelectedDate(+e.target.value); - }} - /> - - {getDataListOptions(startDate, endDate).map((date) => ( - - ))} - -
- )} +
+ - {span === oneWeek ? ( - <> - - {DATE_FORMATER.format(new Date(payload.value))} - - - - {TIME_FORMATER.format(new Date(payload.value))} - - - ) : span === oneDay ? ( - DATE_FORMATER.format(new Date(payload.value)) + - ' ' + - TIME_FORMATER.format(new Date(payload.value)) - ) : ( - TIME_SECOND_FORMATER.format(new Date(payload.value)) + {getTickFormatter( + startDate, + endDate, + new Date(payload.value), )} @@ -414,6 +153,7 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) { fill={theme.statusHealthy} radius={15} yAxisId="background" + isAnimationActive={false} /> {warningKeys.map((key) => ( @@ -426,25 +166,7 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) { }} onPointerLeave={() => setTooltipData(null)} fill={theme.statusWarning} - shape={(props) => { - const { x, y, height, fill } = props; - - const start = - props[key][0] < startDate ? startDate : props[key][0]; - const end = props[key][1] > endDate ? endDate : props[key][1]; - const test = (end - start) / (endDate - startDate); - return ( - - ); - }} + shape={(props) => rectangleRenderer(props, key)} > ))} @@ -459,25 +181,7 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) { setTooltipData(e.tooltipPayload); }} onPointerLeave={() => setTooltipData(null)} - shape={(props) => { - const { x, y, height, fill } = props; - - const start = - props[key][0] < startDate ? startDate : props[key][0]; - const end = props[key][1] > endDate ? endDate : props[key][1]; - const test = (end - start) / (endDate - startDate); - return ( - - ); - }} + shape={(props) => rectangleRenderer(props, key)} /> ))} diff --git a/src/lib/components/globalhealthbar/HistorySlider.tsx b/src/lib/components/globalhealthbar/HistorySlider.tsx new file mode 100644 index 0000000000..9a2755d327 --- /dev/null +++ b/src/lib/components/globalhealthbar/HistorySlider.tsx @@ -0,0 +1,131 @@ +import { useEffect, useRef, useState } from 'react'; +import styled, { css, useTheme } from 'styled-components'; +import { FormattedDateTime, Icon, spacing } from '../../index'; +import { FocusVisibleStyle } from '../buttonv2/Buttonv2.component'; +import { useHistoryAlert } from './HistoryProvider'; +import { getStep, setHistoryTooltipPosition } from './utils'; + +const StyledRange = styled.input` + width: 600px; + padding: 0; /* nécessaire pour IE */ + margin: 0; + margin-top: 2px; + appearance: none; /* nécessaire pour IE */ + -moz-appearance: none; /* nécessaire pour Firefox */ + -webkit-appearance: none; /* nécessaire pour Chrome */ + font: inherit; /* même rendu suivant font document */ + outline: none; + opacity: 1; + background: transparent; /* sert pour couleur de fond de la zone de déplacement */ + box-sizing: content-box; /* même modèle de boîte pour tous */ + transition: opacity 0.2s; + cursor: pointer; + position: absolute; + z-index: 10; + height: 16px; + :focus-visible::-webkit-slider-thumb { + ${FocusVisibleStyle} + } + /*==============================*/ + /* cursor */ + /*==============================*/ + &::-webkit-slider-thumb { + -webkit-appearance: none; + padding: 0; + appearance: none; + margin: 0; + width: 2px; + height: 16px; + background-color: ${(props) => props.theme.selectedActive}; + } + &::-moz-range-thumb { + margin: 0; + width: 2px; + height: 16px; + background-color: ${(props) => props.theme.selectedActive}; + border: none; + } +`; + +const HistoryContainer = styled.div` + width: 100%; + position: relative; +`; + +const HistoryTooltipContainer = styled.div<{ inset: string }>` + position: absolute; + display: flex; + inset: ${(props) => props.inset}; + align-items: center; + flex-direction: column; + gap: ${spacing.r2}; +`; + +const HistoryTooltip = styled.div` + ${(props) => { + const theme = useTheme(); + return css` + padding: ${spacing.r4} ${spacing.r8}; + white-space: 'nowrap'; + border: 1px solid ${theme.border}; + border-radius: '4px'; + color: ${theme.textSecondary}; + `; + }} +`; + +export const HistoryAlertSlider = ({ start, end, startDate, endDate }) => { + const history = useHistoryAlert(); + const popoverRef = useRef(null); + const [tooltipPosition, setTooltipPosition] = useState('auto'); + useEffect(() => { + if (popoverRef.current) { + setTooltipPosition( + setHistoryTooltipPosition( + startDate, + endDate, + popoverRef.current, + history.selectedDate, + ), + ); + } + }, [history.selectedDate, startDate, endDate, popoverRef.current]); + + if (!history.selectedDate) { + return null; + } + return ( + + + + + + + + + { + if (e.target.valueAsNumber > endDate) + history.setSelectedDate(endDate); + if (e.target.valueAsNumber < startDate) + history.setSelectedDate(startDate); + history.setSelectedDate(+e.target.value); + }} + /> + + ); +}; diff --git a/src/lib/components/globalhealthbar/utils.tsx b/src/lib/components/globalhealthbar/utils.tsx new file mode 100644 index 0000000000..39f8c0e460 --- /dev/null +++ b/src/lib/components/globalhealthbar/utils.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { RectRadius } from 'recharts/types/shape/Rectangle'; +import { + DATE_FORMATER, + TIME_FORMATER, + TIME_SECOND_FORMATER, +} from '../date/FormattedDateTime'; + +const oneHour = 60 * 60 * 1000; +const oneDay = 24 * oneHour; + +export const setHistoryTooltipPosition = ( + startDate: number, + endDate: number, + popover: HTMLDivElement, + selectedDate: number | null, +): string => { + if (selectedDate && popover) { + const width = ((selectedDate - startDate) / (endDate - startDate)) * 600; + const leftPosition = width - popover.offsetWidth / 2; + return `auto auto -4px ${leftPosition}px`; + } + return 'auto'; +}; + +export const getRadius = ( + start: number, + end: number, + startDate: number, + endDate: number, +): RectRadius => { + const span = endDate - startDate; + const marge = span >= oneDay ? 0.011 * span : 0; + let radius = [0, 0]; + let rightRadius = [0, 0]; + + if (start === startDate) { + radius = [15, 15]; + } else if (start <= startDate + marge) { + radius = [6, 6]; + } + if (end === endDate) { + rightRadius = [15, 15]; + } else if (end >= endDate - marge) { + rightRadius = [6, 6]; + } + radius.splice(1, 0, ...rightRadius); + + return radius as RectRadius; +}; + +export const getStep = (startDate: number, endDate: number): number => { + const span = endDate - startDate; + if (span === 7 * oneDay) { + return oneHour; + } else if (span === oneDay) { + return oneHour / 4; + } else return 60 * 1000; +}; + +export const getDataListOptions = ( + startDate: number, + endDate: number, +): number[] => { + const span = endDate - startDate; + if (span === 7 * oneDay) { + return Array.from({ length: 6 }, (_, i) => endDate - (i + 1) * oneDay); + } + return Array.from({ length: 4 }, (_, i) => endDate - ((i + 1) / 5) * span); +}; + +export const getTickFormatter = ( + startDate: number, + endDate: number, + payloadValue: Date, +): React.ReactNode => { + const span = endDate - startDate; + if (span === 7 * oneDay) { + return ( + <> + + {DATE_FORMATER.format(payloadValue)} + + + + {TIME_FORMATER.format(payloadValue)} + + + ); + } + if (span === oneDay) { + return ( + DATE_FORMATER.format(payloadValue) + + ' ' + + TIME_FORMATER.format(payloadValue) + ); + } else return TIME_SECOND_FORMATER.format(payloadValue); +};