From 804c2cf0587bff9b20180b4a74caf1ef2212945f Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Mon, 12 May 2025 11:14:10 +0200 Subject: [PATCH] feat: Horizontal bar chart (#7680) --- ui/CHANGELOG.md | 1 + .../ui/chart/horizontal-split-chart.tsx | 241 ++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 ui/components/ui/chart/horizontal-split-chart.tsx diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 8c9c12db4e..4056619953 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -27,6 +27,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Add a button to download the CSV report in compliance card. [(#7665)](https://github.com/prowler-cloud/prowler/pull/7665) - Show loading state while checking provider connection. [(#7669)](https://github.com/prowler-cloud/prowler/pull/7669) + ### 🔄 Changed - Finding URLs now include the ID, allowing them to be shared within the organization. [(#7654)](https://github.com/prowler-cloud/prowler/pull/7654) diff --git a/ui/components/ui/chart/horizontal-split-chart.tsx b/ui/components/ui/chart/horizontal-split-chart.tsx new file mode 100644 index 0000000000..b3b4c783a3 --- /dev/null +++ b/ui/components/ui/chart/horizontal-split-chart.tsx @@ -0,0 +1,241 @@ +"use client"; + +import { Tooltip } from "@nextui-org/react"; +import * as React from "react"; +import { useEffect, useState } from "react"; + +import { cn } from "@/lib/utils"; + +interface HorizontalSplitBarProps { + /** + * First value (left) + */ + valueA: number; + /** + * Second value (right) + */ + valueB: number; + /** + * Additional CSS classes for the main container + */ + className?: string; + /** + * Color for value A (Tailwind classes) + * @default "bg-system-success" + */ + colorA?: string; + /** + * Color for value B (Tailwind classes) + * @default "bg-system-error" + */ + colorB?: string; + /** + * Value format suffix (like "%", "$", etc.) + * Will be appended to the values when displayed + * @example "%" + */ + valueSuffix?: string; + /** + * Bar height + * @default "h-4" + */ + barHeight?: string; + /** + * Color for the empty state (when both values are 0) + * @default "bg-gray-300" + */ + emptyColor?: string; + /** + * Text to display when there is no data + * @default "No data available" + */ + emptyText?: string; + /** + * Minimum width for small values (in pixels) + * @default 25 + */ + minBarWidth?: number; + /** + * Custom tooltip content for value A (optional) + * If not provided, the formatted value will be used + */ + tooltipContentA?: string; + /** + * Custom tooltip content for value B (optional) + * If not provided, the formatted value will be used + */ + tooltipContentB?: string; + /** + * Text color for labels + * @default "text-gray-700" + */ + labelColor?: string; +} + +/** + * Horizontal split bar chart component that displays two values + * with bars growing from a central separator. + * + * @example + * ```tsx + * + * ``` + */ +export const HorizontalSplitBar = ({ + valueA, + valueB, + className, + colorA = "bg-system-success", + colorB = "bg-system-error", + valueSuffix = "", + barHeight = "h-4", + emptyColor = "bg-gray-300", + emptyText = "No data available", + minBarWidth = 25, + tooltipContentA, + tooltipContentB, + labelColor = "text-gray-700", +}: HorizontalSplitBarProps) => { + // Reference to the container to measure its width + const containerRef = React.useRef(null); + const [maxContainerWidth, setMaxContainerWidth] = useState(0); + + // Effect to measure the container width + useEffect(() => { + if (containerRef.current) { + const updateWidth = () => { + const containerWidth = containerRef.current?.clientWidth || 0; + setMaxContainerWidth(containerWidth); + }; + + updateWidth(); + + window.addEventListener("resize", updateWidth); + return () => window.removeEventListener("resize", updateWidth); + } + }, []); + + // Ensure values are positive + const valA = Math.max(0, valueA); + const valB = Math.max(0, valueB); + + const hasNoData = valA === 0 && valB === 0; + const formattedValueA = `${valA}${valueSuffix}`; + const formattedValueB = `${valB}${valueSuffix}`; + + if (hasNoData) { + return ( +
+
+
+ + {emptyText} + +
+
+
+ ); + } + + const availableWidth = Math.max(0, maxContainerWidth); + const halfWidth = availableWidth / 2; + const separatorWidth = 1; + + let rawWidthA = valA; + let rawWidthB = valB; + + // Determine if we need to scale to fit in available space + const maxSideWidth = halfWidth - separatorWidth / 2; + const needsScaling = rawWidthA > maxSideWidth || rawWidthB > maxSideWidth; + + if (needsScaling) { + // Calculate scale factor based on the largest value + const maxRawWidth = Math.max(rawWidthA, rawWidthB); + const scaleFactor = maxSideWidth / maxRawWidth; + + // Apply the scale factor to both sides + rawWidthA = rawWidthA * scaleFactor; + rawWidthB = rawWidthB * scaleFactor; + } + + // Apply minimum width if needed + const barWidthA = Math.max(rawWidthA, valA > 0 ? minBarWidth : 0); + const barWidthB = Math.max(rawWidthB, valB > 0 ? minBarWidth : 0); + + return ( +
+
+
+ {/* Left label */} +
+ {valA > 0 ? formattedValueA : "0"} +
+ {/* Left bar */} + {valA > 0 && ( + +
+ + )} +
+ + {/* Central separator */} +
+ +
+ {/* Right bar */} + {valB > 0 && ( + +
+ + )} + {/* Right label */} +
+ {valB > 0 ? formattedValueB : "0"} +
+
+
+
+ ); +}; + +export default HorizontalSplitBar;