1
0
Fork 0
traefik/webui/src/components/resources/TraefikResourceStatsCard.tsx

216 lines
5.1 KiB
TypeScript

import { Box, Card, Flex, H3, Skeleton, styled, Text } from '@traefiklabs/faency'
import { Chart as ChartJs, ArcElement, Tooltip } from 'chart.js'
import { ReactNode, useEffect, useMemo, useState } from 'react'
import { Doughnut } from 'react-chartjs-2'
import { FaArrowRightLong } from 'react-icons/fa6'
import { Link as RouterLink, useNavigate } from 'react-router-dom'
import Status, { colorByStatus, StatusType } from './Status'
import { capitalizeFirstLetter } from 'utils/string'
ChartJs.register(ArcElement, Tooltip)
const Link = styled(RouterLink, {
textDecoration: 'none',
'&:hover': {
textDecoration: 'none',
},
})
type StatsCardType = {
children: ReactNode
}
const StatsCard = ({ children, ...props }: StatsCardType) => (
<Card
css={{
display: 'flex',
flexDirection: 'column',
padding: '16px',
overflow: 'hidden',
}}
{...props}
>
{children}
</Card>
)
export type TraefikResourceStatsType = {
title?: string
errors: number
total: number
warnings: number
}
export type TraefikResourceStatsCardProps = TraefikResourceStatsType & {
linkTo: string
}
export type DataType = {
datasets: {
backgroundColor: string[]
data: (string | number)[]
}[]
labels?: string[]
}
const getPercent = (total: number, value: number) => (total > 0 ? ((value * 100) / total).toFixed(0) : 0)
const STATS_ATTRIBUTES: { status: StatusType; label: string }[] = [
{
status: 'enabled',
label: 'success',
},
{
status: 'warning',
label: 'warnings',
},
{
status: 'disabled',
label: 'errors',
},
]
const CustomLegend = ({
status,
label,
count,
total,
linkTo,
}: {
status: StatusType
label: string
count: number
total: number
linkTo: string
}) => {
return (
<Link to={`${linkTo}?status=${status}`}>
<Flex css={{ alignItems: 'center', p: '$2' }}>
<Status status={status} />
<Flex css={{ flexDirection: 'column', flex: 1 }}>
<Text css={{ fontWeight: 600 }}>{capitalizeFirstLetter(label)}</Text>
<Text size={1} css={{ color: 'hsl(0, 0%, 56%)' }} data-testid={`${label}-pc`}>
{getPercent(total, count)}%
</Text>
</Flex>
<Text size={5} css={{ fontWeight: 700 }} data-testid={`${label}-count`}>
{count}
</Text>
</Flex>
</Link>
)
}
const TraefikResourceStatsCard = ({ title, errors, total, warnings, linkTo }: TraefikResourceStatsCardProps) => {
const navigate = useNavigate()
const defaultData = {
datasets: [
{
backgroundColor: [colorByStatus.enabled],
data: [1],
},
],
}
const [data, setData] = useState<DataType>(defaultData)
const counts = useMemo(
() => ({
success: total - (errors + warnings),
warnings,
errors,
}),
[errors, total, warnings],
)
useEffect(() => {
if (counts.success + counts.warnings + counts.errors === 0) {
setData(defaultData)
return
}
const newData = {
datasets: [
{
backgroundColor: [colorByStatus.enabled, colorByStatus.warning, colorByStatus.error],
data: [counts.success, counts.warnings, counts.errors],
},
],
labels: ['Success', 'Warnings', 'Errors'],
}
setData(newData)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [errors, warnings, total, counts])
const options = {
animation: {
duration: 1000,
},
plugins: {
legend: {
display: false,
},
},
tooltips: {
enabled: true,
},
maintainAspectRatio: false,
onClick: (_, activeEl) => {
navigate(`${linkTo}?status=${STATS_ATTRIBUTES[activeEl[0].index].status}`)
},
}
if (!errors && !total && !warnings) return null
return (
<StatsCard data-testid="card">
{title && (
<Flex css={{ pb: '$3', mb: '$2' }}>
{title && (
<Flex align="center" justify="space-between" css={{ flex: '1' }}>
<H3 css={{ fontSize: '$6' }}>{title}</H3>
<Link to={linkTo as string}>
<Flex align="center" gap={1} css={{ color: '$primary' }}>
<Text css={{ fontWeight: 500, color: '$primary' }}>Explore</Text>
<FaArrowRightLong />
</Flex>
</Link>
</Flex>
)}
</Flex>
)}
<Flex css={{ flex: '1' }}>
<Box css={{ width: '50%' }}>
<Doughnut data={data} options={options} />
</Box>
<Box css={{ width: '50%' }}>
{STATS_ATTRIBUTES.map((i) => (
<CustomLegend key={`${title}-${i.label}`} {...i} count={counts[i.label]} total={total} linkTo={linkTo} />
))}
</Box>
</Flex>
</StatsCard>
)
}
export const StatsCardSkeleton = () => {
return (
<StatsCard>
<Flex gap={2}>
<Skeleton css={{ width: '80%', height: 150 }} />
<Flex direction="column" gap={2} css={{ flex: 1 }}>
<Skeleton />
<Skeleton />
<Skeleton />
</Flex>
</Flex>
</StatsCard>
)
}
export default TraefikResourceStatsCard