mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
feat: Horizontal bar chart (#7680)
This commit is contained in:
@@ -6,6 +6,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Add a new chart to show the split between passed and failed findings. [(#7680)](https://github.com/prowler-cloud/prowler/pull/7680)
|
||||
- Added `Accordion` component. [(#7700)](https://github.com/prowler-cloud/prowler/pull/7700)
|
||||
|
||||
---
|
||||
@@ -21,6 +22,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)
|
||||
|
||||
@@ -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
|
||||
* <HorizontalSplitBar
|
||||
* valueA={54}
|
||||
* valueB={38}
|
||||
* valueSuffix="%"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
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<HTMLDivElement>(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 (
|
||||
<div ref={containerRef} className={cn("flex w-full flex-col", className)}>
|
||||
<div className="flex w-full justify-center">
|
||||
<div
|
||||
className={cn(
|
||||
`w-full ${barHeight} flex items-center justify-center rounded-full`,
|
||||
emptyColor,
|
||||
)}
|
||||
>
|
||||
<span className="text-xs font-medium text-gray-600">
|
||||
{emptyText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div ref={containerRef} className={cn("flex w-full flex-col", className)}>
|
||||
<div className="flex items-center justify-center">
|
||||
<div
|
||||
className="flex items-center justify-end gap-2"
|
||||
style={{ width: `${halfWidth}px` }}
|
||||
>
|
||||
{/* Left label */}
|
||||
<div
|
||||
className={cn("text-xs font-medium", labelColor)}
|
||||
aria-label={`${formattedValueA} ${tooltipContentA ? tooltipContentA : ""}`}
|
||||
>
|
||||
{valA > 0 ? formattedValueA : "0"}
|
||||
</div>
|
||||
{/* Left bar */}
|
||||
{valA > 0 && (
|
||||
<Tooltip
|
||||
content={`${formattedValueA} ${tooltipContentA ? tooltipContentA : ""}`}
|
||||
className="text-xs"
|
||||
>
|
||||
<div
|
||||
className={cn(`${barHeight} rounded-l-full`, colorA)}
|
||||
style={{
|
||||
width: `${barWidthA}px`,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Central separator */}
|
||||
<div
|
||||
className="flex-shrink-0 bg-background"
|
||||
style={{ width: `${separatorWidth}px` }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="flex items-center justify-start gap-2"
|
||||
style={{ width: `${halfWidth}px` }}
|
||||
>
|
||||
{/* Right bar */}
|
||||
{valB > 0 && (
|
||||
<Tooltip
|
||||
content={`${formattedValueB} ${tooltipContentB ? tooltipContentB : ""}`}
|
||||
className="text-xs"
|
||||
>
|
||||
<div
|
||||
className={cn(`${barHeight} rounded-r-full`, colorB)}
|
||||
style={{
|
||||
width: `${barWidthB}px`,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Right label */}
|
||||
<div
|
||||
className={cn("text-xs font-medium", labelColor)}
|
||||
aria-label={`${formattedValueB} ${tooltipContentB ? tooltipContentB : ""}`}
|
||||
>
|
||||
{valB > 0 ? formattedValueB : "0"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HorizontalSplitBar;
|
||||
Reference in New Issue
Block a user