diff --git a/packages/manager/.changeset/pr-9983-upcoming-features-1702070124570.md b/packages/manager/.changeset/pr-9983-upcoming-features-1702070124570.md new file mode 100644 index 00000000000..6d8acb79720 --- /dev/null +++ b/packages/manager/.changeset/pr-9983-upcoming-features-1702070124570.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Replace NodeBalancer detail charts with Recharts ([#9983](https://github.com/linode/manager/pull/9983)) diff --git a/packages/manager/src/components/AreaChart.tsx b/packages/manager/src/components/AreaChart.tsx new file mode 100644 index 00000000000..d88aff1a691 --- /dev/null +++ b/packages/manager/src/components/AreaChart.tsx @@ -0,0 +1,138 @@ +import { Typography, useTheme } from '@mui/material'; +import { styled } from '@mui/material/styles'; +import { DateTime } from 'luxon'; +import React from 'react'; +import { + AreaChart as _AreaChart, + Area, + CartesianGrid, + ResponsiveContainer, + Tooltip, + TooltipProps, + XAxis, + YAxis, +} from 'recharts'; + +import { Paper } from 'src/components/Paper'; +import { roundTo } from 'src/utilities/roundTo'; + +interface AreaProps { + color: string; + dataKey: string; +} + +interface XAxisProps { + tickFormat: string; + tickGap: number; +} + +interface AreaChartProps { + areas: AreaProps[]; + data: any; + height: number; + timezone: string; + unit: string; + xAxis: XAxisProps; +} + +const humanizeLargeData = (value: number) => { + if (value >= 1000000) { + return value / 1000000 + 'M'; + } + if (value >= 1000) { + return value / 1000 + 'K'; + } + return `${value}`; +}; + +export const AreaChart = (props: AreaChartProps) => { + const { areas, data, height, timezone, unit, xAxis } = props; + + const theme = useTheme(); + + const xAxisTickFormatter = (t: number) => { + return DateTime.fromMillis(t, { zone: timezone }).toFormat( + xAxis.tickFormat + ); + }; + + const tooltipLabelFormatter = (t: number) => { + return DateTime.fromMillis(t, { zone: timezone }).toFormat( + 'LLL dd, yyyy, h:mm a' + ); + }; + + const tooltipValueFormatter = (value: number) => + `${roundTo(value)} ${unit}/s`; + + const CustomTooltip = ({ + active, + label, + payload, + }: TooltipProps) => { + if (active && payload && payload.length) { + return ( + + {tooltipLabelFormatter(label)} + {payload.map((item) => ( + + {item.dataKey}: {tooltipValueFormatter(item.value)} + + ))} + + ); + } + + return null; + }; + + return ( + + <_AreaChart data={data}> + + + + } + /> + {areas.map(({ color, dataKey }) => ( + + ))} + + + ); +}; + +const StyledPaper = styled(Paper, { + label: 'StyledPaper', +})(({ theme }) => ({ + border: `1px solid ${theme.color.border2}`, + padding: theme.spacing(1), +})); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransferHistoryChart.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransferHistoryChart.tsx deleted file mode 100644 index ae6862a260f..00000000000 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransferHistoryChart.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { Typography, useTheme } from '@mui/material'; -import { styled } from '@mui/material/styles'; -import { DateTime } from 'luxon'; -import React from 'react'; -import { - Area, - AreaChart, - CartesianGrid, - ResponsiveContainer, - Tooltip, - TooltipProps, - XAxis, - YAxis, -} from 'recharts'; - -import { Box } from 'src/components/Box'; -import { Paper } from 'src/components/Paper'; -import { NetworkUnit } from 'src/features/Longview/shared/utilities'; -import { roundTo } from 'src/utilities/roundTo'; - -interface NetworkTransferHistoryChartProps { - data: [number, null | number][]; - timezone: string; - unit: NetworkUnit; -} - -export const NetworkTransferHistoryChart = ( - props: NetworkTransferHistoryChartProps -) => { - const { data, timezone, unit } = props; - - const theme = useTheme(); - - const xAxisTickFormatter = (t: number) => { - return DateTime.fromMillis(t, { zone: timezone }).toFormat('LLL dd'); - }; - - const tooltipLabelFormatter = (t: number) => { - return DateTime.fromMillis(t, { zone: timezone }).toFormat( - 'LLL dd, yyyy, h:mm a' - ); - }; - - const tooltipValueFormatter = (value: number) => - `${roundTo(value)} ${unit}/s`; - - const CustomTooltip = ({ - active, - label, - payload, - }: TooltipProps) => { - if (active && payload && payload.length) { - return ( - - {tooltipLabelFormatter(label)} - - Public outbound traffic: {tooltipValueFormatter(payload[0].value)} - - - ); - } - - return null; - }; - - return ( - - - - - - - } - /> - - - - - ); -}; - -const StyledPaper = styled(Paper, { - label: 'StyledPaper', -})(({ theme }) => ({ - border: `1px solid ${theme.color.border2}`, - padding: theme.spacing(1), -})); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/TransferHistory.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/TransferHistory.tsx index 137ba13a639..67dbe306337 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/TransferHistory.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/TransferHistory.tsx @@ -7,6 +7,7 @@ import { DateTime, Interval } from 'luxon'; import * as React from 'react'; import PendingIcon from 'src/assets/icons/pending.svg'; +import { AreaChart } from 'src/components/AreaChart'; import { Box } from 'src/components/Box'; import { CircleProgress } from 'src/components/CircleProgress'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; @@ -28,8 +29,6 @@ import { useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { readableBytes } from 'src/utilities/unitConversions'; -import { NetworkTransferHistoryChart } from './NetworkTransferHistoryChart'; - interface Props { linodeCreated: string; linodeID: number; @@ -168,12 +167,25 @@ export const TransferHistory = React.memo((props: Props) => { }, []); return ( - + + + ); } diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx index dc0a7959987..2dc23c62495 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx @@ -4,13 +4,16 @@ import * as React from 'react'; import { useParams } from 'react-router-dom'; import PendingIcon from 'src/assets/icons/pending.svg'; +import { AreaChart } from 'src/components/AreaChart'; +import { Box } from 'src/components/Box'; import { CircleProgress } from 'src/components/CircleProgress'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; import { LineGraph } from 'src/components/LineGraph/LineGraph'; import MetricsDisplay from 'src/components/LineGraph/MetricsDisplay'; -import { Typography } from 'src/components/Typography'; import { Paper } from 'src/components/Paper'; +import { Typography } from 'src/components/Typography'; import { formatBitsPerSecond } from 'src/features/Longview/shared/utilities'; +import { useFlags } from 'src/hooks/useFlags'; import { NODEBALANCER_STATS_NOT_READY_API_MESSAGE, useNodeBalancerQuery, @@ -37,6 +40,8 @@ export const TablesPanel = () => { nodebalancer?.created ); + const flags = useFlags(); + const statsErrorString = error ? getAPIErrorOrDefault(error, 'Unable to load stats')[0].reason : undefined; @@ -80,24 +85,58 @@ export const TablesPanel = () => { const metrics = getMetrics(data); + let timeData = []; + // @TODO recharts: remove conditional code and delete old chart when we decide recharts is stable + if (flags.recharts) { + timeData = data.reduce((acc: any, point: any) => { + acc.push({ + Connections: point[1], + t: point[0], + }); + return acc; + }, []); + } + return ( - - - + {flags.recharts ? ( + + + + ) : ( + + + + )} { const renderTrafficChart = () => { const trafficIn = stats?.data.traffic.in ?? []; const trafficOut = stats?.data.traffic.out ?? []; + const timeData = []; + + // @TODO recharts: remove conditional code and delete old chart when we decide recharts is stable + if (flags.recharts && trafficIn) { + for (let i = 0; i < trafficIn.length; i++) { + timeData.push({ + 'Traffic In': trafficIn[i][1], + 'Traffic Out': trafficOut[i][1], + t: trafficIn[i][0], + }); + } + } if (statsNotReadyError) { return ( @@ -152,26 +203,52 @@ export const TablesPanel = () => { return ( - + {flags.recharts ? ( + + + + ) : ( + + )} { return ( - - Graphs - + Graphs Connections (CXN/s, 5 min avg.) @@ -223,9 +298,14 @@ const StyledHeader = styled(Typography, { const StyledTitle = styled(Typography, { label: 'StyledTitle', })(({ theme }) => ({ + alignItems: 'center', + display: 'flex', [theme.breakpoints.down('lg')]: { marginLeft: theme.spacing(), }, + [theme.breakpoints.up('md')]: { + margin: `${theme.spacing(2)} 0`, + }, })); const StyledChart = styled('div', { @@ -247,16 +327,6 @@ const StyledBottomLegend = styled('div', { padding: 10, })); -const StyledgGraphControls = styled(Typography, { - label: 'StyledgGraphControls', -})(({ theme }) => ({ - alignItems: 'center', - display: 'flex', - [theme.breakpoints.up('md')]: { - margin: `${theme.spacing(2)} 0`, - }, -})); - const StyledPanel = styled(Paper, { label: 'StyledPanel', })(({ theme }) => ({