mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-31 07:44:21 +00:00
feat(ui): add Finding Severity Over Time chart to overview page (#9405)
This commit is contained in:
1 parent
9c387d5742
commit
7f12832808
100 files changed
+1267
-948
No files matched your search
@@ -1,29 +1,55 @@
|
||||
export interface ChartLegendItem {
|
||||
label: string;
|
||||
color: string;
|
||||
dataKey?: string;
|
||||
}
|
||||
|
||||
interface ChartLegendProps {
|
||||
items: ChartLegendItem[];
|
||||
selectedItem?: string | null;
|
||||
onItemClick?: (dataKey: string) => void;
|
||||
}
|
||||
|
||||
export function ChartLegend({ items }: ChartLegendProps) {
|
||||
export function ChartLegend({
|
||||
items,
|
||||
selectedItem,
|
||||
onItemClick,
|
||||
}: ChartLegendProps) {
|
||||
const isInteractive = !!onItemClick;
|
||||
|
||||
return (
|
||||
<div className="border-border-neutral-tertiary bg-bg-neutral-tertiary inline-flex items-center gap-2 rounded-full border">
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={`legend-${index}`}
|
||||
className="flex items-center gap-2 px-4 py-3"
|
||||
>
|
||||
<div
|
||||
className="h-3 w-3 rounded"
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
<span className="text-text-neutral-secondary text-sm font-medium">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{items.map((item, index) => {
|
||||
const dataKey = item.dataKey ?? item.label.toLowerCase();
|
||||
const isSelected = selectedItem === dataKey;
|
||||
const isFaded = selectedItem !== null && !isSelected;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`legend-${index}`}
|
||||
type="button"
|
||||
className={`flex items-center gap-2 px-4 py-3 transition-opacity duration-200 ${
|
||||
isInteractive
|
||||
? "cursor-pointer hover:opacity-80"
|
||||
: "cursor-default"
|
||||
} ${isFaded ? "opacity-30" : "opacity-100"}`}
|
||||
onClick={() => onItemClick?.(dataKey)}
|
||||
disabled={!isInteractive}
|
||||
>
|
||||
<div
|
||||
className={`h-3 w-3 rounded ${isSelected ? "ring-2 ring-offset-1" : ""}`}
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
// @ts-expect-error ring-color is a valid Tailwind CSS variable
|
||||
"--tw-ring-color": item.color,
|
||||
}}
|
||||
/>
|
||||
<span className="text-text-neutral-secondary text-sm font-medium">
|
||||
{item.label}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Dot } from "recharts";
|
||||
|
||||
import { LineDataPoint } from "../types";
|
||||
|
||||
export interface PointClickData {
|
||||
point: LineDataPoint;
|
||||
dataKey?: string;
|
||||
}
|
||||
|
||||
interface CustomActiveDotProps {
|
||||
cx?: number;
|
||||
cy?: number;
|
||||
payload?: LineDataPoint;
|
||||
dataKey: string;
|
||||
color: string;
|
||||
isFaded: boolean;
|
||||
onPointClick?: (data: PointClickData) => void;
|
||||
onMouseEnter: () => void;
|
||||
onMouseLeave: () => void;
|
||||
}
|
||||
|
||||
export const CustomActiveDot = ({
|
||||
cx,
|
||||
cy,
|
||||
payload,
|
||||
dataKey,
|
||||
color,
|
||||
isFaded,
|
||||
onPointClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}: CustomActiveDotProps) => {
|
||||
if (cx === undefined || cy === undefined) return null;
|
||||
|
||||
// Don't render active dot for faded lines
|
||||
if (isFaded) return null;
|
||||
|
||||
return (
|
||||
<Dot
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={6}
|
||||
fill={color}
|
||||
style={{ cursor: onPointClick ? "pointer" : "default" }}
|
||||
onClick={() => {
|
||||
if (onPointClick && payload) {
|
||||
onPointClick({ point: payload, dataKey });
|
||||
}
|
||||
}}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,27 +1,55 @@
|
||||
export const AXIS_FONT_SIZE = 14;
|
||||
const TODAY_FONT_SIZE = 12;
|
||||
const MONTH_FONT_SIZE = 11;
|
||||
|
||||
interface CustomXAxisTickProps {
|
||||
x: number;
|
||||
y: number;
|
||||
index?: number;
|
||||
payload: {
|
||||
value: string | number;
|
||||
};
|
||||
visibleTicksCount?: number;
|
||||
}
|
||||
|
||||
const getTodayFormatted = () => {
|
||||
const getTodayISO = () => {
|
||||
const today = new Date();
|
||||
return today.toLocaleDateString("en-US", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
return today.toISOString().split("T")[0];
|
||||
};
|
||||
|
||||
const getMonthName = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString("en-US", { month: "short" });
|
||||
};
|
||||
|
||||
const getDayNumber = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.getDate();
|
||||
};
|
||||
|
||||
const getMonthFromDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.getMonth();
|
||||
};
|
||||
|
||||
export const CustomXAxisTickWithToday = Object.assign(
|
||||
function CustomXAxisTickWithToday(props: CustomXAxisTickProps) {
|
||||
const { x, y, payload } = props;
|
||||
const todayFormatted = getTodayFormatted();
|
||||
const isToday = String(payload.value) === todayFormatted;
|
||||
function CustomXAxisTickWithToday(
|
||||
props: CustomXAxisTickProps & { data?: Array<{ date: string }> },
|
||||
) {
|
||||
const { x, y, payload, index = 0, data = [] } = props;
|
||||
const dateStr = String(payload.value);
|
||||
const todayISO = getTodayISO();
|
||||
const isToday = dateStr === todayISO;
|
||||
|
||||
const dayNumber = getDayNumber(dateStr);
|
||||
const currentMonth = getMonthFromDate(dateStr);
|
||||
|
||||
// Show month name if it's the first tick or if the month changed from previous tick
|
||||
const isFirstTick = index === 0;
|
||||
const previousDate = index > 0 && data[index - 1]?.date;
|
||||
const previousMonth = previousDate ? getMonthFromDate(previousDate) : -1;
|
||||
const monthChanged = currentMonth !== previousMonth;
|
||||
const showMonth = isFirstTick || monthChanged;
|
||||
|
||||
return (
|
||||
<g transform={`translate(${x},${y})`}>
|
||||
@@ -33,12 +61,23 @@ export const CustomXAxisTickWithToday = Object.assign(
|
||||
fill="var(--color-text-neutral-secondary)"
|
||||
fontSize={AXIS_FONT_SIZE}
|
||||
>
|
||||
{payload.value}
|
||||
{dayNumber}
|
||||
</text>
|
||||
{showMonth && (
|
||||
<text
|
||||
x={0}
|
||||
y={42}
|
||||
textAnchor="middle"
|
||||
fill="var(--color-text-neutral-tertiary)"
|
||||
fontSize={MONTH_FONT_SIZE}
|
||||
>
|
||||
{getMonthName(dateStr)}
|
||||
</text>
|
||||
)}
|
||||
{isToday && (
|
||||
<text
|
||||
x={0}
|
||||
y={36}
|
||||
y={showMonth ? 56 : 42}
|
||||
textAnchor="middle"
|
||||
fill="var(--color-text-neutral-secondary)"
|
||||
fontSize={TODAY_FONT_SIZE}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Dot } from "recharts";
|
||||
|
||||
interface CustomDotProps {
|
||||
cx?: number;
|
||||
cy?: number;
|
||||
color: string;
|
||||
isFaded: boolean;
|
||||
}
|
||||
|
||||
export const CustomDot = ({ cx, cy, color, isFaded }: CustomDotProps) => {
|
||||
if (cx === undefined || cy === undefined) return null;
|
||||
|
||||
return <Dot cx={cx} cy={cy} r={4} fill={color} opacity={isFaded ? 0.2 : 1} />;
|
||||
};
|
||||
Reference in new issue
Block a user