mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
refactor(graphs): graph components kebab case (#8966)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
10
Makefile
10
Makefile
@@ -46,6 +46,14 @@ help: ## Show this help.
|
||||
@echo "Prowler Makefile"
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
|
||||
##@ Build no cache
|
||||
build-no-cache-dev:
|
||||
docker compose -f docker-compose-dev.yml build --no-cache api-dev worker-dev worker-beat
|
||||
|
||||
##@ Development Environment
|
||||
run-api-dev: ## Start development environment with API, PostgreSQL, Valkey, and workers
|
||||
docker compose -f docker-compose-dev.yml up api-dev postgres valkey worker-dev worker-beat --build
|
||||
docker compose -f docker-compose-dev.yml up api-dev postgres valkey worker-dev worker-beat
|
||||
|
||||
##@ Development Environment
|
||||
build-and-run-api-dev: build-no-cache-dev run-api-dev
|
||||
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Bar,
|
||||
BarChart as RechartsBar,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
import { ChartTooltip } from "./shared/ChartTooltip";
|
||||
import { CHART_COLORS, LAYOUT_OPTIONS } from "./shared/constants";
|
||||
import { getSeverityColorByName } from "./shared/utils";
|
||||
import { BarDataPoint, LayoutOption } from "./types";
|
||||
|
||||
interface BarChartProps {
|
||||
data: BarDataPoint[];
|
||||
layout?: LayoutOption;
|
||||
xLabel?: string;
|
||||
yLabel?: string;
|
||||
height?: number;
|
||||
showValues?: boolean;
|
||||
}
|
||||
|
||||
const CustomLabel = ({ x, y, width, height, value, data }: any) => {
|
||||
const percentage = data.percentage;
|
||||
return (
|
||||
<text
|
||||
x={x + width + 10}
|
||||
y={y + height / 2}
|
||||
fill={CHART_COLORS.textSecondary}
|
||||
fontSize={12}
|
||||
textAnchor="start"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
{percentage !== undefined
|
||||
? `${percentage}% • ${value.toLocaleString()}`
|
||||
: value.toLocaleString()}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
|
||||
export function BarChart({
|
||||
data,
|
||||
layout = LAYOUT_OPTIONS.horizontal,
|
||||
xLabel,
|
||||
yLabel,
|
||||
height = 400,
|
||||
showValues = true,
|
||||
}: BarChartProps) {
|
||||
const isHorizontal = layout === LAYOUT_OPTIONS.horizontal;
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<RechartsBar
|
||||
data={data}
|
||||
layout={layout}
|
||||
margin={{ top: 20, right: showValues ? 100 : 30, left: 20, bottom: 20 }}
|
||||
>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
stroke={CHART_COLORS.gridLine}
|
||||
horizontal={isHorizontal}
|
||||
vertical={!isHorizontal}
|
||||
/>
|
||||
{isHorizontal ? (
|
||||
<>
|
||||
<XAxis
|
||||
type="number"
|
||||
tick={{ fill: CHART_COLORS.textSecondary, fontSize: 12 }}
|
||||
label={
|
||||
xLabel
|
||||
? {
|
||||
value: xLabel,
|
||||
position: "insideBottom",
|
||||
offset: -10,
|
||||
fill: CHART_COLORS.textSecondary,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey="name"
|
||||
type="category"
|
||||
width={100}
|
||||
tick={{ fill: CHART_COLORS.textSecondary, fontSize: 12 }}
|
||||
label={
|
||||
yLabel
|
||||
? {
|
||||
value: yLabel,
|
||||
angle: -90,
|
||||
position: "insideLeft",
|
||||
fill: CHART_COLORS.textSecondary,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fill: CHART_COLORS.textSecondary, fontSize: 12 }}
|
||||
label={
|
||||
xLabel
|
||||
? {
|
||||
value: xLabel,
|
||||
position: "insideBottom",
|
||||
offset: -10,
|
||||
fill: CHART_COLORS.textSecondary,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<YAxis
|
||||
type="number"
|
||||
tick={{ fill: CHART_COLORS.textSecondary, fontSize: 12 }}
|
||||
label={
|
||||
yLabel
|
||||
? {
|
||||
value: yLabel,
|
||||
angle: -90,
|
||||
position: "insideLeft",
|
||||
fill: CHART_COLORS.textSecondary,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<Bar
|
||||
dataKey="value"
|
||||
radius={4}
|
||||
label={
|
||||
showValues && isHorizontal
|
||||
? (props: any) => (
|
||||
<CustomLabel {...props} data={data[props.index]} />
|
||||
)
|
||||
: false
|
||||
}
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={
|
||||
entry.color ||
|
||||
getSeverityColorByName(entry.name) ||
|
||||
CHART_COLORS.defaultColor
|
||||
}
|
||||
opacity={1}
|
||||
className="transition-opacity hover:opacity-80"
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</RechartsBar>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Rectangle, ResponsiveContainer, Sankey, Tooltip } from "recharts";
|
||||
|
||||
import { CHART_COLORS, SEVERITY_COLORS } from "./shared/constants";
|
||||
|
||||
interface SankeyNode {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface SankeyLink {
|
||||
source: number;
|
||||
target: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface SankeyChartProps {
|
||||
data: {
|
||||
nodes: SankeyNode[];
|
||||
links: SankeyLink[];
|
||||
};
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const COLORS: Record<string, string> = {
|
||||
Success: "var(--color-success)",
|
||||
Fail: "var(--color-destructive)",
|
||||
AWS: "var(--color-orange)",
|
||||
Azure: "var(--color-cyan)",
|
||||
Google: "var(--color-red)",
|
||||
...SEVERITY_COLORS,
|
||||
};
|
||||
|
||||
const CustomTooltip = ({ active, payload }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
|
||||
<p className="text-sm font-semibold text-white">{data.name}</p>
|
||||
{data.value && (
|
||||
<p className="text-xs text-slate-400">Value: {data.value}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const CustomNode = ({ x, y, width, height, payload, containerWidth }: any) => {
|
||||
const isOut = x + width + 6 > containerWidth;
|
||||
const nodeName = payload.name;
|
||||
const color = COLORS[nodeName] || CHART_COLORS.defaultColor;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<Rectangle
|
||||
x={x}
|
||||
y={y}
|
||||
width={width}
|
||||
height={height}
|
||||
fill={color}
|
||||
fillOpacity="1"
|
||||
/>
|
||||
<text
|
||||
textAnchor={isOut ? "end" : "start"}
|
||||
x={isOut ? x - 6 : x + width + 6}
|
||||
y={y + height / 2}
|
||||
fontSize="14"
|
||||
className="fill-white stroke-white"
|
||||
>
|
||||
{nodeName}
|
||||
</text>
|
||||
<text
|
||||
textAnchor={isOut ? "end" : "start"}
|
||||
x={isOut ? x - 6 : x + width + 6}
|
||||
y={y + height / 2 + 13}
|
||||
fontSize="12"
|
||||
className="fill-slate-400 stroke-slate-400"
|
||||
strokeOpacity="0.5"
|
||||
>
|
||||
{payload.value}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomLink = (props: any) => {
|
||||
const {
|
||||
sourceX,
|
||||
targetX,
|
||||
sourceY,
|
||||
targetY,
|
||||
sourceControlX,
|
||||
targetControlX,
|
||||
linkWidth,
|
||||
} = props;
|
||||
|
||||
const sourceName = props.payload.source?.name || "";
|
||||
const color = COLORS[sourceName] || CHART_COLORS.defaultColor;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<path
|
||||
d={`
|
||||
M${sourceX},${sourceY + linkWidth / 2}
|
||||
C${sourceControlX},${sourceY + linkWidth / 2}
|
||||
${targetControlX},${targetY + linkWidth / 2}
|
||||
${targetX},${targetY + linkWidth / 2}
|
||||
L${targetX},${targetY - linkWidth / 2}
|
||||
C${targetControlX},${targetY - linkWidth / 2}
|
||||
${sourceControlX},${sourceY - linkWidth / 2}
|
||||
${sourceX},${sourceY - linkWidth / 2}
|
||||
Z
|
||||
`}
|
||||
fill={color}
|
||||
fillOpacity="0.4"
|
||||
stroke="none"
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export function SankeyChart({ data, height = 400 }: SankeyChartProps) {
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<Sankey
|
||||
data={data}
|
||||
node={<CustomNode />}
|
||||
link={<CustomLink />}
|
||||
nodePadding={50}
|
||||
margin={{ top: 20, right: 160, bottom: 20, left: 160 }}
|
||||
>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
</Sankey>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { Cell, Label, Pie, PieChart, Tooltip } from "recharts";
|
||||
|
||||
import { ChartConfig, ChartContainer } from "@/components/ui/chart/Chart";
|
||||
|
||||
import { ChartLegend } from "./shared/ChartLegend";
|
||||
import { ChartLegend } from "./shared/chart-legend";
|
||||
import { DonutDataPoint } from "./types";
|
||||
|
||||
interface DonutChartProps {
|
||||
@@ -24,18 +24,30 @@ const CustomTooltip = ({ active, payload }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
|
||||
<div
|
||||
className="rounded-lg border p-3 shadow-lg"
|
||||
style={{
|
||||
backgroundColor: "var(--chart-background)",
|
||||
borderColor: "var(--chart-border-emphasis)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-3 w-3 rounded-sm"
|
||||
style={{ backgroundColor: data.color }}
|
||||
/>
|
||||
<span className="text-sm font-semibold text-white">
|
||||
<span
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: "var(--chart-text-primary)" }}
|
||||
>
|
||||
{data.percentage}% {data.name}
|
||||
</span>
|
||||
</div>
|
||||
{data.change !== undefined && (
|
||||
<p className="mt-2 text-xs text-slate-400">
|
||||
<p
|
||||
className="mt-2 text-xs"
|
||||
style={{ color: "var(--chart-text-secondary)" }}
|
||||
>
|
||||
<span className="font-bold">
|
||||
{data.change > 0 ? "+" : ""}
|
||||
{data.change}%
|
||||
@@ -145,14 +157,19 @@ export function DonutChart({
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
className="fill-white text-3xl font-bold"
|
||||
className="text-3xl font-bold"
|
||||
style={{
|
||||
fill: "var(--chart-text-primary)",
|
||||
}}
|
||||
>
|
||||
{formattedValue}
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 24}
|
||||
className="fill-slate-400"
|
||||
style={{
|
||||
fill: "var(--chart-text-secondary)",
|
||||
}}
|
||||
>
|
||||
{centerLabel.label}
|
||||
</tspan>
|
||||
@@ -26,7 +26,12 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
<div className="w-full">
|
||||
{title && (
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold text-white">{title}</h3>
|
||||
<h3
|
||||
className="text-lg font-semibold"
|
||||
style={{ color: "var(--chart-text-primary)" }}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -48,8 +53,9 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
>
|
||||
<div className="w-24 text-right">
|
||||
<span
|
||||
className="text-sm text-white"
|
||||
className="text-sm"
|
||||
style={{
|
||||
color: "var(--chart-text-primary)",
|
||||
opacity: isFaded ? 0.5 : 1,
|
||||
transition: "opacity 0.2s",
|
||||
}}
|
||||
@@ -70,26 +76,44 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
/>
|
||||
|
||||
{isHovered && (
|
||||
<div className="absolute top-10 left-0 z-10 min-w-[200px] rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
|
||||
<div
|
||||
className="absolute top-10 left-0 z-10 min-w-[200px] rounded-lg border p-3 shadow-lg"
|
||||
style={{
|
||||
backgroundColor: "var(--chart-background)",
|
||||
borderColor: "var(--chart-border-emphasis)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="h-3 w-3 rounded-sm"
|
||||
style={{ backgroundColor: barColor }}
|
||||
/>
|
||||
<span className="font-semibold text-white">
|
||||
<span
|
||||
className="font-semibold"
|
||||
style={{ color: "var(--chart-text-primary)" }}
|
||||
>
|
||||
{item.value.toLocaleString()} {item.name} Risk
|
||||
</span>
|
||||
</div>
|
||||
{item.newFindings !== undefined && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Bell size={14} className="text-slate-400" />
|
||||
<span className="text-sm text-slate-400">
|
||||
<Bell
|
||||
size={14}
|
||||
style={{ color: "var(--chart-fail)" }}
|
||||
/>
|
||||
<span
|
||||
className="text-sm"
|
||||
style={{ color: "var(--chart-text-secondary)" }}
|
||||
>
|
||||
{item.newFindings} New Findings
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{item.change !== undefined && (
|
||||
<p className="mt-1 text-sm text-slate-400">
|
||||
<p
|
||||
className="mt-1 text-sm"
|
||||
style={{ color: "var(--chart-text-secondary)" }}
|
||||
>
|
||||
<span className="font-bold">
|
||||
{item.change > 0 ? "+" : ""}
|
||||
{item.change}%
|
||||
@@ -102,15 +126,16 @@ export function HorizontalBarChart({ data, title }: HorizontalBarChartProps) {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex w-40 items-center gap-2 text-sm text-white"
|
||||
className="flex w-40 items-center gap-2 text-sm"
|
||||
style={{
|
||||
color: "var(--chart-text-primary)",
|
||||
opacity: isFaded ? 0.5 : 1,
|
||||
transition: "opacity 0.2s",
|
||||
}}
|
||||
>
|
||||
<span className="font-semibold">{item.percentage}%</span>
|
||||
<span className="text-slate-400">•</span>
|
||||
<span>{item.value.toLocaleString()}</span>
|
||||
<span style={{ color: "var(--chart-text-secondary)" }}>•</span>
|
||||
<span className="font-bold">{item.value.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,9 +1,9 @@
|
||||
export { BarChart } from "./BarChart";
|
||||
export { DonutChart } from "./DonutChart";
|
||||
export { HorizontalBarChart } from "./HorizontalBarChart";
|
||||
export { LineChart } from "./LineChart";
|
||||
export { RadarChart } from "./RadarChart";
|
||||
export { RadialChart } from "./RadialChart";
|
||||
export { SankeyChart } from "./SankeyChart";
|
||||
export { ScatterPlot } from "./ScatterPlot";
|
||||
export { ChartLegend, type ChartLegendItem } from "./shared/ChartLegend";
|
||||
export { DonutChart } from "./donut-chart";
|
||||
export { HorizontalBarChart } from "./horizontal-bar-chart";
|
||||
export { LineChart } from "./line-chart";
|
||||
export { MapChart, type MapChartData, type MapChartProps } from "./map-chart";
|
||||
export { RadarChart } from "./radar-chart";
|
||||
export { RadialChart } from "./radial-chart";
|
||||
export { SankeyChart } from "./sankey-chart";
|
||||
export { ScatterPlot } from "./scatter-plot";
|
||||
export { ChartLegend, type ChartLegendItem } from "./shared/chart-legend";
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
import { AlertPill } from "./shared/AlertPill";
|
||||
import { ChartLegend } from "./shared/ChartLegend";
|
||||
import { AlertPill } from "./shared/alert-pill";
|
||||
import { ChartLegend } from "./shared/chart-legend";
|
||||
import { CHART_COLORS } from "./shared/constants";
|
||||
import { LineConfig, LineDataPoint } from "./types";
|
||||
|
||||
@@ -48,8 +48,19 @@ const CustomLineTooltip = ({
|
||||
const totalValue = typedPayload.reduce((sum, item) => sum + item.value, 0);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
|
||||
<p className="mb-3 text-xs text-slate-400">{label}</p>
|
||||
<div
|
||||
className="rounded-lg border p-3 shadow-lg"
|
||||
style={{
|
||||
backgroundColor: "var(--chart-background)",
|
||||
borderColor: "var(--chart-border-emphasis)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="mb-3 text-xs"
|
||||
style={{ color: "var(--chart-text-secondary)" }}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
|
||||
<div className="mb-3">
|
||||
<AlertPill value={totalValue} textSize="sm" />
|
||||
@@ -67,18 +78,29 @@ const CustomLineTooltip = ({
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: item.stroke }}
|
||||
/>
|
||||
<span className="text-sm text-white">{item.value}</span>
|
||||
<span
|
||||
className="text-sm"
|
||||
style={{ color: "var(--chart-text-primary)" }}
|
||||
>
|
||||
{item.value}
|
||||
</span>
|
||||
</div>
|
||||
{newFindings !== undefined && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell size={14} className="text-slate-400" />
|
||||
<span className="text-xs text-slate-400">
|
||||
<Bell size={14} style={{ color: "var(--chart-fail)" }} />
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: "var(--chart-text-secondary)" }}
|
||||
>
|
||||
{newFindings} New Findings
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{change !== undefined && typeof change === "number" && (
|
||||
<p className="text-xs text-slate-400">
|
||||
<p
|
||||
className="text-xs"
|
||||
style={{ color: "var(--chart-text-secondary)" }}
|
||||
>
|
||||
<span className="font-bold">
|
||||
{change > 0 ? "+" : ""}
|
||||
{change}%
|
||||
479
ui/components/graphs/map-chart.tsx
Normal file
479
ui/components/graphs/map-chart.tsx
Normal file
@@ -0,0 +1,479 @@
|
||||
"use client";
|
||||
|
||||
import * as d3 from "d3";
|
||||
import type {
|
||||
Feature,
|
||||
FeatureCollection,
|
||||
GeoJsonProperties,
|
||||
Geometry,
|
||||
} from "geojson";
|
||||
import { AlertTriangle, Info, MapPin } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { feature } from "topojson-client";
|
||||
import type {
|
||||
GeometryCollection,
|
||||
Objects,
|
||||
Topology,
|
||||
} from "topojson-specification";
|
||||
|
||||
import { HorizontalBarChart } from "./horizontal-bar-chart";
|
||||
import { BarDataPoint } from "./types";
|
||||
|
||||
// Constants
|
||||
const MAP_CONFIG = {
|
||||
defaultWidth: 688,
|
||||
defaultHeight: 400,
|
||||
pointRadius: 6,
|
||||
selectedPointRadius: 8,
|
||||
transitionDuration: 300,
|
||||
} as const;
|
||||
|
||||
const MAP_COLORS = {
|
||||
landFill: "var(--chart-border-emphasis)",
|
||||
landStroke: "var(--chart-border)",
|
||||
pointDefault: "#DB2B49",
|
||||
pointSelected: "#86DA26",
|
||||
pointHover: "#DB2B49",
|
||||
} as const;
|
||||
|
||||
const RISK_LEVELS = {
|
||||
LOW_HIGH: "low-high",
|
||||
HIGH: "high",
|
||||
CRITICAL: "critical",
|
||||
} as const;
|
||||
|
||||
type RiskLevel = (typeof RISK_LEVELS)[keyof typeof RISK_LEVELS];
|
||||
|
||||
interface LocationPoint {
|
||||
id: string;
|
||||
name: string;
|
||||
region: string;
|
||||
coordinates: [number, number];
|
||||
totalFindings: number;
|
||||
riskLevel: RiskLevel;
|
||||
severityData: BarDataPoint[];
|
||||
change?: number;
|
||||
}
|
||||
|
||||
export interface MapChartData {
|
||||
locations: LocationPoint[];
|
||||
regions: string[];
|
||||
}
|
||||
|
||||
export interface MapChartProps {
|
||||
data: MapChartData;
|
||||
height?: number;
|
||||
onLocationSelect?: (location: LocationPoint | null) => void;
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function createProjection(width: number, height: number) {
|
||||
return d3
|
||||
.geoNaturalEarth1()
|
||||
.fitExtent(
|
||||
[
|
||||
[1, 1],
|
||||
[width - 1, height - 1],
|
||||
],
|
||||
{ type: "Sphere" },
|
||||
)
|
||||
.precision(0.2);
|
||||
}
|
||||
|
||||
async function fetchWorldData(): Promise<FeatureCollection | null> {
|
||||
try {
|
||||
const worldAtlasModule = await import("world-atlas/countries-110m.json");
|
||||
const worldData = worldAtlasModule.default || worldAtlasModule;
|
||||
const topology = worldData as unknown as Topology<Objects>;
|
||||
return feature(
|
||||
topology,
|
||||
topology.objects.countries as GeometryCollection,
|
||||
) as FeatureCollection;
|
||||
} catch (error) {
|
||||
console.error("Error loading world map data:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Create SVG element
|
||||
function createSVGElement<T extends SVGElement>(
|
||||
type: string,
|
||||
attributes: Record<string, string>,
|
||||
): T {
|
||||
const element = document.createElementNS(
|
||||
"http://www.w3.org/2000/svg",
|
||||
type,
|
||||
) as T;
|
||||
Object.entries(attributes).forEach(([key, value]) => {
|
||||
element.setAttribute(key, value);
|
||||
});
|
||||
return element;
|
||||
}
|
||||
|
||||
// Components
|
||||
function MapTooltip({
|
||||
location,
|
||||
position,
|
||||
}: {
|
||||
location: LocationPoint;
|
||||
position: { x: number; y: number };
|
||||
}) {
|
||||
const CHART_COLORS = {
|
||||
tooltipBorder: "var(--chart-border-emphasis)",
|
||||
tooltipBackground: "var(--chart-background)",
|
||||
textPrimary: "var(--chart-text-primary)",
|
||||
textSecondary: "var(--chart-text-secondary)",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute z-50 min-w-[200px] rounded-lg border p-3 shadow-lg"
|
||||
style={{
|
||||
left: `${position.x + 15}px`,
|
||||
top: `${position.y + 15}px`,
|
||||
transform: "translate(0, -50%)",
|
||||
borderColor: CHART_COLORS.tooltipBorder,
|
||||
backgroundColor: CHART_COLORS.tooltipBackground,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin size={14} style={{ color: CHART_COLORS.textSecondary }} />
|
||||
<span
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: CHART_COLORS.textPrimary }}
|
||||
>
|
||||
{location.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<AlertTriangle size={14} className="text-[#DB2B49]" />
|
||||
<span className="text-sm" style={{ color: CHART_COLORS.textPrimary }}>
|
||||
{location.totalFindings.toLocaleString()} Fail Findings
|
||||
</span>
|
||||
</div>
|
||||
{location.change !== undefined && (
|
||||
<p
|
||||
className="mt-1 text-xs"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
<span className="font-bold">
|
||||
{location.change > 0 ? "+" : ""}
|
||||
{location.change}%
|
||||
</span>{" "}
|
||||
since last scan
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EmptyState() {
|
||||
const CHART_COLORS = {
|
||||
tooltipBorder: "var(--chart-border-emphasis)",
|
||||
tooltipBackground: "var(--chart-background)",
|
||||
textSecondary: "var(--chart-text-secondary)",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex h-full min-h-[400px] items-center justify-center rounded-lg border p-6"
|
||||
style={{
|
||||
borderColor: CHART_COLORS.tooltipBorder,
|
||||
backgroundColor: CHART_COLORS.tooltipBackground,
|
||||
}}
|
||||
>
|
||||
<div className="text-center">
|
||||
<Info
|
||||
size={48}
|
||||
className="mx-auto mb-2"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
/>
|
||||
<p className="text-sm" style={{ color: CHART_COLORS.textSecondary }}>
|
||||
Select a location on the map to view details
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingState({ height }: { height: number }) {
|
||||
const CHART_COLORS = {
|
||||
textSecondary: "var(--chart-text-secondary)",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center" style={{ height }}>
|
||||
<div className="text-center">
|
||||
<div className="mb-2" style={{ color: CHART_COLORS.textSecondary }}>
|
||||
Loading map...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MapChart({
|
||||
data,
|
||||
height = MAP_CONFIG.defaultHeight,
|
||||
}: MapChartProps) {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [selectedLocation, setSelectedLocation] =
|
||||
useState<LocationPoint | null>(null);
|
||||
const [hoveredLocation, setHoveredLocation] = useState<LocationPoint | null>(
|
||||
null,
|
||||
);
|
||||
const [tooltipPosition, setTooltipPosition] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
const [worldData, setWorldData] = useState<FeatureCollection | null>(null);
|
||||
const [isLoadingMap, setIsLoadingMap] = useState(true);
|
||||
const [dimensions, setDimensions] = useState<{
|
||||
width: number;
|
||||
height: number;
|
||||
}>({
|
||||
width: MAP_CONFIG.defaultWidth,
|
||||
height,
|
||||
});
|
||||
|
||||
// Fetch world data once on mount
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
fetchWorldData()
|
||||
.then((data) => {
|
||||
if (isMounted && data) setWorldData(data);
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
if (isMounted) setIsLoadingMap(false);
|
||||
});
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update dimensions on resize
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
if (containerRef.current) {
|
||||
setDimensions({ width: containerRef.current.clientWidth, height });
|
||||
}
|
||||
};
|
||||
updateDimensions();
|
||||
window.addEventListener("resize", updateDimensions);
|
||||
return () => window.removeEventListener("resize", updateDimensions);
|
||||
}, [height]);
|
||||
|
||||
// Render the map
|
||||
useEffect(() => {
|
||||
if (!svgRef.current || !worldData || isLoadingMap) return;
|
||||
|
||||
const svg = svgRef.current;
|
||||
const { width, height } = dimensions;
|
||||
svg.innerHTML = "";
|
||||
|
||||
const projection = createProjection(width, height);
|
||||
const path = d3.geoPath().projection(projection);
|
||||
|
||||
// Render countries
|
||||
const mapGroup = createSVGElement<SVGGElement>("g", {
|
||||
class: "map-countries",
|
||||
});
|
||||
worldData.features?.forEach(
|
||||
(feature: Feature<Geometry, GeoJsonProperties>) => {
|
||||
const pathData = path(feature);
|
||||
if (pathData) {
|
||||
const pathElement = createSVGElement<SVGPathElement>("path", {
|
||||
d: pathData,
|
||||
fill: MAP_COLORS.landFill,
|
||||
stroke: MAP_COLORS.landStroke,
|
||||
"stroke-width": "0.5",
|
||||
});
|
||||
mapGroup.appendChild(pathElement);
|
||||
}
|
||||
},
|
||||
);
|
||||
svg.appendChild(mapGroup);
|
||||
|
||||
// Helper to update tooltip position
|
||||
const updateTooltip = (e: MouseEvent) => {
|
||||
const rect = svg.getBoundingClientRect();
|
||||
setTooltipPosition({
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
});
|
||||
};
|
||||
|
||||
// Helper to create circle
|
||||
const createCircle = (location: LocationPoint) => {
|
||||
const projected = projection(location.coordinates);
|
||||
if (!projected) return null;
|
||||
|
||||
const [x, y] = projected;
|
||||
if (x < 0 || x > width || y < 0 || y > height) return null;
|
||||
|
||||
const isSelected = selectedLocation?.id === location.id;
|
||||
const isHovered = hoveredLocation?.id === location.id;
|
||||
const classes = ["cursor-pointer"];
|
||||
|
||||
if (isSelected) classes.push("drop-shadow-[0_0_8px_#86da26]");
|
||||
if (isHovered && !isSelected) classes.push("opacity-70");
|
||||
|
||||
const circle = createSVGElement<SVGCircleElement>("circle", {
|
||||
cx: x.toString(),
|
||||
cy: y.toString(),
|
||||
r: (isSelected
|
||||
? MAP_CONFIG.selectedPointRadius
|
||||
: MAP_CONFIG.pointRadius
|
||||
).toString(),
|
||||
fill: isSelected ? MAP_COLORS.pointSelected : MAP_COLORS.pointDefault,
|
||||
class: classes.join(" "),
|
||||
});
|
||||
|
||||
circle.addEventListener("click", () =>
|
||||
setSelectedLocation(isSelected ? null : location),
|
||||
);
|
||||
circle.addEventListener("mouseenter", (e) => {
|
||||
setHoveredLocation(location);
|
||||
updateTooltip(e);
|
||||
});
|
||||
circle.addEventListener("mousemove", updateTooltip);
|
||||
circle.addEventListener("mouseleave", () => {
|
||||
setHoveredLocation(null);
|
||||
setTooltipPosition(null);
|
||||
});
|
||||
|
||||
return circle;
|
||||
};
|
||||
|
||||
// Render points
|
||||
const pointsGroup = createSVGElement<SVGGElement>("g", {
|
||||
class: "threat-points",
|
||||
});
|
||||
|
||||
// Unselected points first
|
||||
data.locations.forEach((location) => {
|
||||
if (selectedLocation?.id !== location.id) {
|
||||
const circle = createCircle(location);
|
||||
if (circle) pointsGroup.appendChild(circle);
|
||||
}
|
||||
});
|
||||
|
||||
// Selected point last (on top)
|
||||
if (selectedLocation) {
|
||||
const selectedData = data.locations.find(
|
||||
(loc) => loc.id === selectedLocation.id,
|
||||
);
|
||||
if (selectedData) {
|
||||
const circle = createCircle(selectedData);
|
||||
if (circle) pointsGroup.appendChild(circle);
|
||||
}
|
||||
}
|
||||
|
||||
svg.appendChild(pointsGroup);
|
||||
}, [
|
||||
data.locations,
|
||||
dimensions,
|
||||
selectedLocation,
|
||||
hoveredLocation,
|
||||
worldData,
|
||||
isLoadingMap,
|
||||
]);
|
||||
|
||||
const CHART_COLORS = {
|
||||
tooltipBorder: "var(--chart-border-emphasis)",
|
||||
tooltipBackground: "var(--chart-background)",
|
||||
textPrimary: "var(--chart-text-primary)",
|
||||
textSecondary: "var(--chart-text-secondary)",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-6 lg:flex-row lg:items-start">
|
||||
{/* Map Section */}
|
||||
<div className="flex-1">
|
||||
<h3
|
||||
className="mb-4 text-lg font-semibold"
|
||||
style={{ color: CHART_COLORS.textPrimary }}
|
||||
>
|
||||
Threat Map
|
||||
</h3>
|
||||
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="rounded-lg border p-4"
|
||||
style={{
|
||||
borderColor: CHART_COLORS.tooltipBorder,
|
||||
backgroundColor: CHART_COLORS.tooltipBackground,
|
||||
}}
|
||||
>
|
||||
{isLoadingMap ? (
|
||||
<LoadingState height={dimensions.height} />
|
||||
) : (
|
||||
<>
|
||||
<div className="relative">
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
className="w-full"
|
||||
style={{ maxWidth: "100%" }}
|
||||
/>
|
||||
{hoveredLocation && tooltipPosition && (
|
||||
<MapTooltip
|
||||
location={hoveredLocation}
|
||||
position={tooltipPosition}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<div className="h-3 w-3 rounded-full bg-[#DB2B49]" />
|
||||
<span
|
||||
className="text-sm"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
{data.locations.length} Locations
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Details Section */}
|
||||
<div className="w-full lg:w-[400px]">
|
||||
<div className="mb-4 h-10" />
|
||||
{selectedLocation ? (
|
||||
<div
|
||||
className="rounded-lg border p-6"
|
||||
style={{
|
||||
borderColor: CHART_COLORS.tooltipBorder,
|
||||
backgroundColor: CHART_COLORS.tooltipBackground,
|
||||
}}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-[#86DA26]" />
|
||||
<h4
|
||||
className="text-base font-semibold"
|
||||
style={{ color: CHART_COLORS.textPrimary }}
|
||||
>
|
||||
{selectedLocation.name}
|
||||
</h4>
|
||||
</div>
|
||||
<p
|
||||
className="text-sm"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
{selectedLocation.totalFindings.toLocaleString()} Total Findings
|
||||
</p>
|
||||
</div>
|
||||
<HorizontalBarChart data={selectedLocation.severityData} />
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
ui/components/graphs/map-region-filter.tsx
Normal file
50
ui/components/graphs/map-region-filter.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select/Select";
|
||||
|
||||
interface MapRegionFilterProps {
|
||||
regions: string[];
|
||||
selectedRegion: string;
|
||||
onRegionChange: (region: string) => void;
|
||||
chartColors: {
|
||||
tooltipBorder: string;
|
||||
tooltipBackground: string;
|
||||
textPrimary: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function MapRegionFilter({
|
||||
regions,
|
||||
selectedRegion,
|
||||
onRegionChange,
|
||||
chartColors,
|
||||
}: MapRegionFilterProps) {
|
||||
return (
|
||||
<Select value={selectedRegion} onValueChange={onRegionChange}>
|
||||
<SelectTrigger
|
||||
className="min-w-[200px] rounded-lg"
|
||||
style={{
|
||||
borderColor: chartColors.tooltipBorder,
|
||||
backgroundColor: chartColors.tooltipBackground,
|
||||
color: chartColors.textPrimary,
|
||||
}}
|
||||
>
|
||||
<SelectValue placeholder="All Regions" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="All Regions">All Regions</SelectItem>
|
||||
{regions.map((region) => (
|
||||
<SelectItem key={region} value={region}>
|
||||
{region}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
ChartTooltip,
|
||||
} from "@/components/ui/chart/Chart";
|
||||
|
||||
import { AlertPill } from "./shared/AlertPill";
|
||||
import { AlertPill } from "./shared/alert-pill";
|
||||
import { CHART_COLORS } from "./shared/constants";
|
||||
import { RadarDataPoint } from "./types";
|
||||
|
||||
@@ -28,7 +28,7 @@ interface RadarChartProps {
|
||||
const chartConfig = {
|
||||
value: {
|
||||
label: "Findings",
|
||||
color: "var(--color-magenta)",
|
||||
color: "var(--chart-radar-primary)",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
@@ -36,15 +36,27 @@ const CustomTooltip = ({ active, payload }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0];
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
|
||||
<p className="text-sm font-semibold text-white">
|
||||
<div
|
||||
className="rounded-lg border p-3 shadow-lg"
|
||||
style={{
|
||||
borderColor: CHART_COLORS.tooltipBorder,
|
||||
backgroundColor: CHART_COLORS.tooltipBackground,
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: CHART_COLORS.textPrimary }}
|
||||
>
|
||||
{data.payload.category}
|
||||
</p>
|
||||
<div className="mt-1">
|
||||
<AlertPill value={data.value} />
|
||||
</div>
|
||||
{data.payload.change !== undefined && (
|
||||
<p className="mt-1 text-xs text-slate-400">
|
||||
<p
|
||||
className="mt-1 text-xs"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
<span className="font-bold">
|
||||
{data.payload.change > 0 ? "+" : ""}
|
||||
{data.payload.change}%
|
||||
@@ -84,8 +96,11 @@ const CustomDot = (props: any) => {
|
||||
cx={cx}
|
||||
cy={cy}
|
||||
r={isSelected ? 9 : 6}
|
||||
fill={isSelected ? "var(--color-success)" : "var(--color-purple-dark)"}
|
||||
fill={
|
||||
isSelected ? "var(--chart-success-color)" : "var(--chart-radar-primary)"
|
||||
}
|
||||
fillOpacity={1}
|
||||
className={isSelected ? "drop-shadow-[0_0_8px_#86da26]" : ""}
|
||||
style={{
|
||||
cursor: onSelectPoint ? "pointer" : "default",
|
||||
pointerEvents: "all",
|
||||
@@ -117,7 +132,7 @@ export function RadarChart({
|
||||
<PolarGrid strokeOpacity={0.3} />
|
||||
<Radar
|
||||
dataKey={dataKey}
|
||||
fill="var(--color-magenta)"
|
||||
fill="var(--chart-radar-primary)"
|
||||
fillOpacity={0.2}
|
||||
activeDot={false}
|
||||
dot={
|
||||
@@ -135,7 +150,7 @@ export function RadarChart({
|
||||
}
|
||||
: {
|
||||
r: 6,
|
||||
fill: "var(--color-purple-dark)",
|
||||
fill: "var(--chart-radar-primary)",
|
||||
fillOpacity: 1,
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ interface RadialChartProps {
|
||||
export function RadialChart({
|
||||
percentage,
|
||||
label = "Score",
|
||||
color = "var(--color-success)",
|
||||
color = "var(--chart-success-color)",
|
||||
backgroundColor = CHART_COLORS.tooltipBackground,
|
||||
height = 250,
|
||||
innerRadius = 60,
|
||||
@@ -68,7 +68,10 @@ export function RadialChart({
|
||||
y="50%"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
className="fill-white text-4xl font-bold"
|
||||
className="text-4xl font-bold"
|
||||
style={{
|
||||
fill: "var(--chart-text-primary)",
|
||||
}}
|
||||
>
|
||||
{percentage}%
|
||||
</text>
|
||||
403
ui/components/graphs/sankey-chart.tsx
Normal file
403
ui/components/graphs/sankey-chart.tsx
Normal file
@@ -0,0 +1,403 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Rectangle, ResponsiveContainer, Sankey, Tooltip } from "recharts";
|
||||
|
||||
import { ChartTooltip } from "./shared/chart-tooltip";
|
||||
import { CHART_COLORS } from "./shared/constants";
|
||||
|
||||
interface SankeyNode {
|
||||
name: string;
|
||||
newFindings?: number;
|
||||
change?: number;
|
||||
}
|
||||
|
||||
interface SankeyLink {
|
||||
source: number;
|
||||
target: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface SankeyChartProps {
|
||||
data: {
|
||||
nodes: SankeyNode[];
|
||||
links: SankeyLink[];
|
||||
};
|
||||
height?: number;
|
||||
}
|
||||
|
||||
interface LinkTooltipState {
|
||||
show: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
sourceName: string;
|
||||
targetName: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface NodeTooltipState {
|
||||
show: boolean;
|
||||
x: number;
|
||||
y: number;
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
newFindings?: number;
|
||||
change?: number;
|
||||
}
|
||||
|
||||
// Note: Using hex colors directly because Recharts SVG fill doesn't resolve CSS variables
|
||||
const COLORS: Record<string, string> = {
|
||||
Success: "#86da26",
|
||||
Fail: "#db2b49",
|
||||
AWS: "#ff9900",
|
||||
Azure: "#00bcd4",
|
||||
Google: "#EA4335",
|
||||
Critical: "#971348",
|
||||
High: "#ff3077",
|
||||
Medium: "#ff7d19",
|
||||
Low: "#fdd34f",
|
||||
Info: "#2e51b2",
|
||||
Informational: "#2e51b2",
|
||||
};
|
||||
|
||||
const CustomTooltip = ({ active, payload }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg border p-3 shadow-lg"
|
||||
style={{
|
||||
borderColor: CHART_COLORS.tooltipBorder,
|
||||
backgroundColor: CHART_COLORS.tooltipBackground,
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: CHART_COLORS.textPrimary }}
|
||||
>
|
||||
{data.name}
|
||||
</p>
|
||||
{data.value && (
|
||||
<p className="text-xs" style={{ color: CHART_COLORS.textSecondary }}>
|
||||
Value: {data.value}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const CustomNode = (props: any) => {
|
||||
const { x, y, width, height, payload, containerWidth } = props;
|
||||
const isOut = x + width + 6 > containerWidth;
|
||||
const nodeName = payload.name;
|
||||
const color = COLORS[nodeName] || CHART_COLORS.defaultColor;
|
||||
const isHidden = nodeName === "";
|
||||
const hasTooltip = !isHidden && payload.newFindings;
|
||||
|
||||
const handleMouseEnter = (e: React.MouseEvent) => {
|
||||
if (!hasTooltip) return;
|
||||
|
||||
const rect = e.currentTarget.closest("svg") as SVGSVGElement;
|
||||
if (rect) {
|
||||
const bbox = rect.getBoundingClientRect();
|
||||
props.onNodeHover?.({
|
||||
x: e.clientX - bbox.left,
|
||||
y: e.clientY - bbox.top,
|
||||
name: nodeName,
|
||||
value: payload.value,
|
||||
color,
|
||||
newFindings: payload.newFindings,
|
||||
change: payload.change,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (!hasTooltip) return;
|
||||
|
||||
const rect = e.currentTarget.closest("svg") as SVGSVGElement;
|
||||
if (rect) {
|
||||
const bbox = rect.getBoundingClientRect();
|
||||
props.onNodeMove?.({
|
||||
x: e.clientX - bbox.left,
|
||||
y: e.clientY - bbox.top,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (!hasTooltip) return;
|
||||
props.onNodeLeave?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<g
|
||||
style={{ cursor: hasTooltip ? "pointer" : "default" }}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Rectangle
|
||||
x={x}
|
||||
y={y}
|
||||
width={width}
|
||||
height={height}
|
||||
fill={color}
|
||||
fillOpacity={isHidden ? "0" : "1"}
|
||||
/>
|
||||
{!isHidden && (
|
||||
<>
|
||||
<text
|
||||
textAnchor={isOut ? "end" : "start"}
|
||||
x={isOut ? x - 6 : x + width + 6}
|
||||
y={y + height / 2}
|
||||
fontSize="14"
|
||||
fill={CHART_COLORS.textPrimary}
|
||||
>
|
||||
{nodeName}
|
||||
</text>
|
||||
<text
|
||||
textAnchor={isOut ? "end" : "start"}
|
||||
x={isOut ? x - 6 : x + width + 6}
|
||||
y={y + height / 2 + 13}
|
||||
fontSize="12"
|
||||
fill={CHART_COLORS.textSecondary}
|
||||
>
|
||||
{payload.value}
|
||||
</text>
|
||||
</>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomLink = (props: any) => {
|
||||
const {
|
||||
sourceX,
|
||||
targetX,
|
||||
sourceY,
|
||||
targetY,
|
||||
sourceControlX,
|
||||
targetControlX,
|
||||
linkWidth,
|
||||
index,
|
||||
} = props;
|
||||
|
||||
const sourceName = props.payload.source?.name || "";
|
||||
const targetName = props.payload.target?.name || "";
|
||||
const value = props.payload.value || 0;
|
||||
const color = COLORS[sourceName] || CHART_COLORS.defaultColor;
|
||||
const isHidden = targetName === "";
|
||||
|
||||
const isHovered = props.hoveredLink !== null && props.hoveredLink === index;
|
||||
const hasHoveredLink = props.hoveredLink !== null;
|
||||
|
||||
const pathD = `
|
||||
M${sourceX},${sourceY + linkWidth / 2}
|
||||
C${sourceControlX},${sourceY + linkWidth / 2}
|
||||
${targetControlX},${targetY + linkWidth / 2}
|
||||
${targetX},${targetY + linkWidth / 2}
|
||||
L${targetX},${targetY - linkWidth / 2}
|
||||
C${targetControlX},${targetY - linkWidth / 2}
|
||||
${sourceControlX},${sourceY - linkWidth / 2}
|
||||
${sourceX},${sourceY - linkWidth / 2}
|
||||
Z
|
||||
`;
|
||||
|
||||
const getOpacity = () => {
|
||||
if (isHidden) return "0";
|
||||
if (!hasHoveredLink) return "0.4";
|
||||
return isHovered ? "0.8" : "0.1";
|
||||
};
|
||||
|
||||
const handleMouseEnter = (e: React.MouseEvent) => {
|
||||
const rect = e.currentTarget.parentElement?.parentElement
|
||||
?.parentElement as unknown as SVGSVGElement;
|
||||
if (rect) {
|
||||
const bbox = rect.getBoundingClientRect();
|
||||
props.onLinkHover?.(index, {
|
||||
x: e.clientX - bbox.left,
|
||||
y: e.clientY - bbox.top,
|
||||
sourceName,
|
||||
targetName,
|
||||
value,
|
||||
color,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
const rect = e.currentTarget.parentElement?.parentElement
|
||||
?.parentElement as unknown as SVGSVGElement;
|
||||
if (rect && isHovered) {
|
||||
const bbox = rect.getBoundingClientRect();
|
||||
props.onLinkMove?.({
|
||||
x: e.clientX - bbox.left,
|
||||
y: e.clientY - bbox.top,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
props.onLinkLeave?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<g>
|
||||
<path
|
||||
d={pathD}
|
||||
fill={color}
|
||||
fillOpacity={getOpacity()}
|
||||
stroke="none"
|
||||
style={{ cursor: "pointer", transition: "fill-opacity 0.2s" }}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export function SankeyChart({ data, height = 400 }: SankeyChartProps) {
|
||||
const [hoveredLink, setHoveredLink] = useState<number | null>(null);
|
||||
const [linkTooltip, setLinkTooltip] = useState<LinkTooltipState>({
|
||||
show: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
sourceName: "",
|
||||
targetName: "",
|
||||
value: 0,
|
||||
color: "",
|
||||
});
|
||||
|
||||
const [nodeTooltip, setNodeTooltip] = useState<NodeTooltipState>({
|
||||
show: false,
|
||||
x: 0,
|
||||
y: 0,
|
||||
name: "",
|
||||
value: 0,
|
||||
color: "",
|
||||
});
|
||||
|
||||
const handleLinkHover = (
|
||||
index: number,
|
||||
data: Omit<LinkTooltipState, "show">,
|
||||
) => {
|
||||
setHoveredLink(index);
|
||||
setLinkTooltip({ show: true, ...data });
|
||||
};
|
||||
|
||||
const handleLinkMove = (position: { x: number; y: number }) => {
|
||||
setLinkTooltip((prev) => ({
|
||||
...prev,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleLinkLeave = () => {
|
||||
setHoveredLink(null);
|
||||
setLinkTooltip((prev) => ({ ...prev, show: false }));
|
||||
};
|
||||
|
||||
const handleNodeHover = (data: Omit<NodeTooltipState, "show">) => {
|
||||
setNodeTooltip({ show: true, ...data });
|
||||
};
|
||||
|
||||
const handleNodeMove = (position: { x: number; y: number }) => {
|
||||
setNodeTooltip((prev) => ({
|
||||
...prev,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleNodeLeave = () => {
|
||||
setNodeTooltip((prev) => ({ ...prev, show: false }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<Sankey
|
||||
data={data}
|
||||
node={
|
||||
<CustomNode
|
||||
onNodeHover={handleNodeHover}
|
||||
onNodeMove={handleNodeMove}
|
||||
onNodeLeave={handleNodeLeave}
|
||||
/>
|
||||
}
|
||||
link={
|
||||
<CustomLink
|
||||
hoveredLink={hoveredLink}
|
||||
onLinkHover={handleLinkHover}
|
||||
onLinkMove={handleLinkMove}
|
||||
onLinkLeave={handleLinkLeave}
|
||||
/>
|
||||
}
|
||||
nodePadding={50}
|
||||
margin={{ top: 20, right: 160, bottom: 20, left: 160 }}
|
||||
sort={false}
|
||||
>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
</Sankey>
|
||||
</ResponsiveContainer>
|
||||
{linkTooltip.show && (
|
||||
<div
|
||||
className="pointer-events-none absolute z-50"
|
||||
style={{
|
||||
left: `${Math.max(125, Math.min(linkTooltip.x, window.innerWidth - 125))}px`,
|
||||
top: `${Math.max(linkTooltip.y - 80, 10)}px`,
|
||||
transform: "translate(-50%, -100%)",
|
||||
}}
|
||||
>
|
||||
<ChartTooltip
|
||||
active={true}
|
||||
payload={[
|
||||
{
|
||||
payload: {
|
||||
name: linkTooltip.targetName,
|
||||
value: linkTooltip.value,
|
||||
color: linkTooltip.color,
|
||||
},
|
||||
color: linkTooltip.color,
|
||||
},
|
||||
]}
|
||||
label={`${linkTooltip.sourceName} → ${linkTooltip.targetName}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{nodeTooltip.show && (
|
||||
<div
|
||||
className="pointer-events-none absolute z-50"
|
||||
style={{
|
||||
left: `${Math.max(125, Math.min(nodeTooltip.x, window.innerWidth - 125))}px`,
|
||||
top: `${Math.max(nodeTooltip.y - 80, 10)}px`,
|
||||
transform: "translate(-50%, -100%)",
|
||||
}}
|
||||
>
|
||||
<ChartTooltip
|
||||
active={true}
|
||||
payload={[
|
||||
{
|
||||
payload: {
|
||||
name: nodeTooltip.name,
|
||||
value: nodeTooltip.value,
|
||||
color: nodeTooltip.color,
|
||||
newFindings: nodeTooltip.newFindings,
|
||||
change: nodeTooltip.change,
|
||||
},
|
||||
color: nodeTooltip.color,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,18 +11,11 @@ import {
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
import { AlertPill } from "./shared/AlertPill";
|
||||
import { ChartLegend } from "./shared/ChartLegend";
|
||||
import { AlertPill } from "./shared/alert-pill";
|
||||
import { ChartLegend } from "./shared/chart-legend";
|
||||
import { CHART_COLORS } from "./shared/constants";
|
||||
import { getSeverityColorByRiskScore } from "./shared/utils";
|
||||
|
||||
interface ScatterDataPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
provider: string;
|
||||
name: string;
|
||||
size?: number;
|
||||
}
|
||||
import type { ScatterDataPoint } from "./types";
|
||||
|
||||
interface ScatterPlotProps {
|
||||
data: ScatterDataPoint[];
|
||||
@@ -34,9 +27,9 @@ interface ScatterPlotProps {
|
||||
}
|
||||
|
||||
const PROVIDER_COLORS = {
|
||||
AWS: "var(--color-orange)",
|
||||
Azure: "var(--color-cyan)",
|
||||
Google: "var(--color-red)",
|
||||
AWS: "var(--chart-provider-aws)",
|
||||
Azure: "var(--chart-provider-azure)",
|
||||
Google: "var(--chart-provider-google)",
|
||||
};
|
||||
|
||||
const CustomTooltip = ({ active, payload }: any) => {
|
||||
@@ -45,9 +38,23 @@ const CustomTooltip = ({ active, payload }: any) => {
|
||||
const severityColor = getSeverityColorByRiskScore(data.x);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
|
||||
<p className="text-sm font-semibold text-white">{data.name}</p>
|
||||
<p className="mt-1 text-xs text-slate-400">
|
||||
<div
|
||||
className="rounded-lg border p-3 shadow-lg"
|
||||
style={{
|
||||
borderColor: CHART_COLORS.tooltipBorder,
|
||||
backgroundColor: CHART_COLORS.tooltipBackground,
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: CHART_COLORS.textPrimary }}
|
||||
>
|
||||
{data.name}
|
||||
</p>
|
||||
<p
|
||||
className="mt-1 text-xs"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
<span style={{ color: severityColor }}>{data.x}</span> Risk Score
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
@@ -69,7 +76,7 @@ const CustomScatterDot = ({
|
||||
const isSelected = selectedPoint?.name === payload.name;
|
||||
const size = isSelected ? 18 : 8;
|
||||
const fill = isSelected
|
||||
? "var(--color-success)"
|
||||
? "#86DA26"
|
||||
: PROVIDER_COLORS[payload.provider as keyof typeof PROVIDER_COLORS] ||
|
||||
CHART_COLORS.defaultColor;
|
||||
|
||||
@@ -79,8 +86,9 @@ const CustomScatterDot = ({
|
||||
cy={cy}
|
||||
r={size / 2}
|
||||
fill={fill}
|
||||
stroke={isSelected ? "var(--color-success)" : "transparent"}
|
||||
stroke={isSelected ? "#86DA26" : "transparent"}
|
||||
strokeWidth={2}
|
||||
className={isSelected ? "drop-shadow-[0_0_8px_#86da26]" : ""}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => onSelectPoint?.(payload)}
|
||||
/>
|
||||
@@ -17,13 +17,17 @@ export function AlertPill({
|
||||
}: AlertPillProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="bg-alert-pill-bg flex items-center gap-1 rounded-full px-2 py-1">
|
||||
<AlertTriangle size={iconSize} className="text-alert-pill-text" />
|
||||
<div
|
||||
className="flex items-center gap-1 rounded-full px-2 py-1"
|
||||
style={{ backgroundColor: "var(--chart-alert-bg)" }}
|
||||
>
|
||||
<AlertTriangle
|
||||
size={iconSize}
|
||||
style={{ color: "var(--chart-alert-text)" }}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
`text-${textSize}`,
|
||||
"text-alert-pill-text font-semibold",
|
||||
)}
|
||||
className={cn(`text-${textSize}`, "font-semibold")}
|
||||
style={{ color: "var(--chart-alert-text)" }}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
@@ -9,14 +9,22 @@ interface ChartLegendProps {
|
||||
|
||||
export function ChartLegend({ items }: ChartLegendProps) {
|
||||
return (
|
||||
<div className="bg-card-border mt-4 inline-flex gap-[46px] rounded-full border-2 px-[19px] py-[9px]">
|
||||
<div
|
||||
className="mt-4 inline-flex gap-[46px] rounded-full border-2 px-[19px] py-[9px]"
|
||||
style={{ borderColor: "var(--chart-border)" }}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<div key={`legend-${index}`} className="flex items-center gap-1">
|
||||
<div
|
||||
className="h-3 w-3 rounded"
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
<span className="text-xs text-gray-300">{item.label}</span>
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: "var(--chart-text-secondary)" }}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -3,6 +3,7 @@ import { Bell, VolumeX } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { TooltipData } from "../types";
|
||||
import { CHART_COLORS } from "./constants";
|
||||
|
||||
interface ChartTooltipProps {
|
||||
active?: boolean;
|
||||
@@ -27,7 +28,13 @@ export function ChartTooltip({
|
||||
const color = payload[0].color || data.color;
|
||||
|
||||
return (
|
||||
<div className="min-w-[200px] rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
|
||||
<div
|
||||
className="min-w-[200px] rounded-lg border p-3 shadow-lg"
|
||||
style={{
|
||||
borderColor: CHART_COLORS.tooltipBorder,
|
||||
backgroundColor: CHART_COLORS.tooltipBackground,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{showColorIndicator && color && (
|
||||
<div
|
||||
@@ -38,10 +45,15 @@ export function ChartTooltip({
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
)}
|
||||
<p className="text-sm font-semibold text-white">{label || data.name}</p>
|
||||
<p
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: CHART_COLORS.textPrimary }}
|
||||
>
|
||||
{label || data.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p className="mt-1 text-xs text-white">
|
||||
<p className="mt-1 text-xs" style={{ color: CHART_COLORS.textPrimary }}>
|
||||
{typeof data.value === "number"
|
||||
? data.value.toLocaleString()
|
||||
: data.value}
|
||||
@@ -50,8 +62,11 @@ export function ChartTooltip({
|
||||
|
||||
{data.newFindings !== undefined && data.newFindings > 0 && (
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<Bell size={14} className="text-slate-400" />
|
||||
<span className="text-xs text-slate-400">
|
||||
<Bell size={14} style={{ color: "var(--chart-fail)" }} />
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
{data.newFindings} New Findings
|
||||
</span>
|
||||
</div>
|
||||
@@ -59,20 +74,33 @@ export function ChartTooltip({
|
||||
|
||||
{data.new !== undefined && data.new > 0 && (
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<Bell size={14} className="text-slate-400" />
|
||||
<span className="text-xs text-slate-400">{data.new} New</span>
|
||||
<Bell size={14} style={{ color: "var(--chart-fail)" }} />
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
{data.new} New
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.muted !== undefined && data.muted > 0 && (
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<VolumeX size={14} className="text-slate-400" />
|
||||
<span className="text-xs text-slate-400">{data.muted} Muted</span>
|
||||
<VolumeX size={14} style={{ color: CHART_COLORS.textSecondary }} />
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
{data.muted} Muted
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.change !== undefined && (
|
||||
<p className="mt-1 text-xs text-slate-400">
|
||||
<p
|
||||
className="mt-1 text-xs"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
<span className="font-bold">
|
||||
{data.change > 0 ? "+" : ""}
|
||||
{data.change}%
|
||||
@@ -97,8 +125,19 @@ export function MultiSeriesChartTooltip({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-w-[200px] rounded-lg border border-slate-700 bg-slate-800 p-3 shadow-lg">
|
||||
<p className="mb-2 text-sm font-semibold text-white">{label}</p>
|
||||
<div
|
||||
className="min-w-[200px] rounded-lg border p-3 shadow-lg"
|
||||
style={{
|
||||
borderColor: CHART_COLORS.tooltipBorder,
|
||||
backgroundColor: CHART_COLORS.tooltipBackground,
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="mb-2 text-sm font-semibold"
|
||||
style={{ color: CHART_COLORS.textPrimary }}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
|
||||
{payload.map((entry: any, index: number) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
@@ -106,12 +145,20 @@ export function MultiSeriesChartTooltip({
|
||||
className="h-2 w-2 rounded-full"
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
<span className="text-xs text-white">{entry.name}:</span>
|
||||
<span className="text-xs font-semibold text-white">
|
||||
<span className="text-xs" style={{ color: CHART_COLORS.textPrimary }}>
|
||||
{entry.name}:
|
||||
</span>
|
||||
<span
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: CHART_COLORS.textPrimary }}
|
||||
>
|
||||
{entry.value}
|
||||
</span>
|
||||
{entry.payload[`${entry.dataKey}_change`] && (
|
||||
<span className="text-xs text-slate-400">
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{ color: CHART_COLORS.textSecondary }}
|
||||
>
|
||||
({entry.payload[`${entry.dataKey}_change`] > 0 ? "+" : ""}
|
||||
{entry.payload[`${entry.dataKey}_change`]}%)
|
||||
</span>
|
||||
@@ -1,21 +1,33 @@
|
||||
export const SEVERITY_COLORS = {
|
||||
Informational: "var(--color-info)",
|
||||
Low: "var(--color-warning)",
|
||||
Medium: "var(--color-warning-emphasis)",
|
||||
High: "var(--color-danger)",
|
||||
Critical: "var(--color-danger-emphasis)",
|
||||
Informational: "var(--chart-info)",
|
||||
Info: "var(--chart-info)",
|
||||
Low: "var(--chart-warning)",
|
||||
Medium: "var(--chart-warning-emphasis)",
|
||||
High: "var(--chart-danger)",
|
||||
Critical: "var(--chart-danger-emphasis)",
|
||||
} as const;
|
||||
|
||||
export const PROVIDER_COLORS = {
|
||||
AWS: "var(--chart-provider-aws)",
|
||||
Azure: "var(--chart-provider-azure)",
|
||||
Google: "var(--chart-provider-google)",
|
||||
} as const;
|
||||
|
||||
export const STATUS_COLORS = {
|
||||
Success: "var(--chart-success-color)",
|
||||
Fail: "var(--chart-fail)",
|
||||
} as const;
|
||||
|
||||
export const CHART_COLORS = {
|
||||
tooltipBorder: "var(--color-slate-700)",
|
||||
tooltipBackground: "var(--color-slate-800)",
|
||||
textPrimary: "var(--color-white)",
|
||||
textSecondary: "var(--color-slate-400)",
|
||||
gridLine: "var(--color-slate-700)",
|
||||
tooltipBorder: "var(--chart-border-emphasis)",
|
||||
tooltipBackground: "var(--chart-background)",
|
||||
textPrimary: "var(--chart-text-primary)",
|
||||
textSecondary: "var(--chart-text-secondary)",
|
||||
gridLine: "var(--chart-border-emphasis)",
|
||||
backgroundTrack: "rgba(51, 65, 85, 0.5)", // slate-700 with 50% opacity
|
||||
alertPillBg: "var(--color-alert-pill-bg)",
|
||||
alertPillText: "var(--color-alert-pill-text)",
|
||||
defaultColor: "var(--color-slate-500)", // Default fallback color for charts
|
||||
alertPillBg: "var(--chart-alert-bg)",
|
||||
alertPillText: "var(--chart-alert-text)",
|
||||
defaultColor: "#64748b", // slate-500
|
||||
} as const;
|
||||
|
||||
export const CHART_DIMENSIONS = {
|
||||
|
||||
@@ -36,6 +36,14 @@ export interface RadarDataPoint {
|
||||
change?: number;
|
||||
}
|
||||
|
||||
export interface ScatterDataPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
provider: string;
|
||||
name: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface LineConfig {
|
||||
dataKey: string;
|
||||
color: string;
|
||||
|
||||
@@ -12,6 +12,6 @@ export * from "./feedback-banner/feedback-banner";
|
||||
export * from "./headers/navigation-header";
|
||||
export * from "./label/Label";
|
||||
export * from "./main-layout/main-layout";
|
||||
export * from "./select/Select";
|
||||
export * from "./select";
|
||||
export * from "./sidebar";
|
||||
export * from "./toast";
|
||||
|
||||
12
ui/components/ui/select/index.ts
Normal file
12
ui/components/ui/select/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "./Select";
|
||||
@@ -5,7 +5,7 @@
|
||||
"from": "1.0.59",
|
||||
"to": "1.0.59",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-01T11:13:12.025Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -13,7 +13,7 @@
|
||||
"from": "2.0.59",
|
||||
"to": "2.0.59",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-01T11:13:12.025Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -21,15 +21,15 @@
|
||||
"from": "2.8.4",
|
||||
"to": "2.8.4",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-29T14:26:25.838Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@hookform/resolvers",
|
||||
"from": "3.10.0",
|
||||
"from": "5.2.2",
|
||||
"to": "5.2.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-01T15:09:44.056Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -37,7 +37,7 @@
|
||||
"from": "0.3.77",
|
||||
"to": "0.3.77",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -45,23 +45,23 @@
|
||||
"from": "0.4.9",
|
||||
"to": "0.4.9",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@langchain/langgraph-supervisor",
|
||||
"from": "0.0.12",
|
||||
"from": "0.0.20",
|
||||
"to": "0.0.20",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "@langchain/openai",
|
||||
"from": "0.6.9",
|
||||
"from": "0.5.18",
|
||||
"to": "0.5.18",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -69,7 +69,7 @@
|
||||
"from": "15.3.5",
|
||||
"to": "15.3.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -77,7 +77,7 @@
|
||||
"from": "1.1.14",
|
||||
"to": "1.1.14",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -85,7 +85,7 @@
|
||||
"from": "1.1.14",
|
||||
"to": "1.1.14",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -93,7 +93,7 @@
|
||||
"from": "2.1.15",
|
||||
"to": "2.1.15",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -101,7 +101,7 @@
|
||||
"from": "1.3.2",
|
||||
"to": "1.3.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -109,7 +109,7 @@
|
||||
"from": "2.1.7",
|
||||
"to": "2.1.7",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -117,7 +117,7 @@
|
||||
"from": "2.2.5",
|
||||
"to": "2.2.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -125,7 +125,7 @@
|
||||
"from": "1.2.3",
|
||||
"to": "1.2.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -133,7 +133,7 @@
|
||||
"from": "1.2.14",
|
||||
"to": "1.2.14",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -141,7 +141,7 @@
|
||||
"from": "3.9.4",
|
||||
"to": "3.9.4",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -149,7 +149,7 @@
|
||||
"from": "3.8.12",
|
||||
"to": "3.8.12",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -157,7 +157,7 @@
|
||||
"from": "4.1.13",
|
||||
"to": "4.1.13",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-24T15:04:48.761Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -165,7 +165,7 @@
|
||||
"from": "0.5.16",
|
||||
"to": "0.5.16",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -173,7 +173,7 @@
|
||||
"from": "8.21.3",
|
||||
"to": "8.21.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -181,15 +181,15 @@
|
||||
"from": "4.0.9",
|
||||
"to": "4.0.9",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "ai",
|
||||
"from": "4.3.16",
|
||||
"from": "5.0.59",
|
||||
"to": "5.0.59",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-01T10:03:22.788Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -197,7 +197,7 @@
|
||||
"from": "6.0.2",
|
||||
"to": "6.0.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -205,7 +205,7 @@
|
||||
"from": "0.7.1",
|
||||
"to": "0.7.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -213,7 +213,15 @@
|
||||
"from": "2.1.1",
|
||||
"to": "2.1.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "d3",
|
||||
"from": "7.9.0",
|
||||
"to": "7.9.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -221,7 +229,7 @@
|
||||
"from": "4.1.0",
|
||||
"to": "4.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -229,7 +237,7 @@
|
||||
"from": "11.18.2",
|
||||
"to": "11.18.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -237,7 +245,7 @@
|
||||
"from": "10.7.16",
|
||||
"to": "10.7.16",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -245,7 +253,7 @@
|
||||
"from": "5.10.0",
|
||||
"to": "5.10.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -253,7 +261,7 @@
|
||||
"from": "4.1.0",
|
||||
"to": "4.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -261,7 +269,7 @@
|
||||
"from": "4.0.0",
|
||||
"to": "4.0.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -269,7 +277,7 @@
|
||||
"from": "0.543.0",
|
||||
"to": "0.543.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -277,15 +285,15 @@
|
||||
"from": "15.0.12",
|
||||
"to": "15.0.12",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "next",
|
||||
"from": "14.2.32",
|
||||
"from": "15.5.3",
|
||||
"to": "15.5.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -293,7 +301,7 @@
|
||||
"from": "5.0.0-beta.29",
|
||||
"to": "5.0.0-beta.29",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -301,7 +309,7 @@
|
||||
"from": "0.2.1",
|
||||
"to": "0.2.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -309,23 +317,23 @@
|
||||
"from": "1.4.2",
|
||||
"to": "1.4.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "react",
|
||||
"from": "18.3.1",
|
||||
"from": "19.1.1",
|
||||
"to": "19.1.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "react-dom",
|
||||
"from": "18.3.1",
|
||||
"from": "19.1.1",
|
||||
"to": "19.1.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -333,7 +341,7 @@
|
||||
"from": "7.62.0",
|
||||
"to": "7.62.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -341,7 +349,7 @@
|
||||
"from": "10.1.0",
|
||||
"to": "10.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -349,7 +357,7 @@
|
||||
"from": "2.15.4",
|
||||
"to": "2.15.4",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -357,7 +365,7 @@
|
||||
"from": "3.13.0",
|
||||
"to": "3.13.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -365,15 +373,7 @@
|
||||
"from": "0.0.1",
|
||||
"to": "0.0.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "shadcn",
|
||||
"from": "3.2.1",
|
||||
"to": "3.2.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -381,7 +381,7 @@
|
||||
"from": "0.33.5",
|
||||
"to": "0.33.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -389,7 +389,7 @@
|
||||
"from": "3.3.1",
|
||||
"to": "3.3.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -397,7 +397,15 @@
|
||||
"from": "1.0.7",
|
||||
"to": "1.0.7",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "topojson-client",
|
||||
"from": "3.1.0",
|
||||
"to": "3.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -405,7 +413,7 @@
|
||||
"from": "1.4.0",
|
||||
"to": "1.4.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-15T07:57:13.225Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
@@ -413,23 +421,31 @@
|
||||
"from": "11.1.0",
|
||||
"to": "11.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.548Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "world-atlas",
|
||||
"from": "2.0.2",
|
||||
"to": "2.0.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "zod",
|
||||
"from": "3.25.73",
|
||||
"from": "4.1.11",
|
||||
"to": "4.1.11",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-01T09:40:25.207Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "dependencies",
|
||||
"name": "zustand",
|
||||
"from": "4.5.7",
|
||||
"from": "5.0.8",
|
||||
"to": "5.0.8",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-01T09:40:25.207Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -437,15 +453,23 @@
|
||||
"from": "5.2.1",
|
||||
"to": "5.2.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@playwright/test",
|
||||
"from": "1.53.2",
|
||||
"to": "1.53.2",
|
||||
"from": "1.56.1",
|
||||
"to": "1.56.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@types/d3",
|
||||
"from": "7.4.3",
|
||||
"to": "7.4.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -453,23 +477,31 @@
|
||||
"from": "20.5.7",
|
||||
"to": "20.5.7",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@types/react",
|
||||
"from": "18.3.3",
|
||||
"from": "19.1.13",
|
||||
"to": "19.1.13",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@types/react-dom",
|
||||
"from": "18.3.0",
|
||||
"from": "19.1.9",
|
||||
"to": "19.1.9",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "@types/topojson-client",
|
||||
"from": "3.1.5",
|
||||
"to": "3.1.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -477,7 +509,7 @@
|
||||
"from": "10.0.0",
|
||||
"to": "10.0.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -485,7 +517,7 @@
|
||||
"from": "7.18.0",
|
||||
"to": "7.18.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -493,7 +525,7 @@
|
||||
"from": "7.18.0",
|
||||
"to": "7.18.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -501,7 +533,7 @@
|
||||
"from": "10.4.19",
|
||||
"to": "10.4.19",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -509,7 +541,7 @@
|
||||
"from": "19.1.0-rc.3",
|
||||
"to": "19.1.0-rc.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -517,15 +549,15 @@
|
||||
"from": "8.57.1",
|
||||
"to": "8.57.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "eslint-config-next",
|
||||
"from": "14.2.32",
|
||||
"from": "15.5.3",
|
||||
"to": "15.5.3",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-23T10:22:08.630Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -533,7 +565,7 @@
|
||||
"from": "10.1.5",
|
||||
"to": "10.1.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -541,7 +573,7 @@
|
||||
"from": "2.32.0",
|
||||
"to": "2.32.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -549,7 +581,7 @@
|
||||
"from": "6.10.2",
|
||||
"to": "6.10.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -557,7 +589,7 @@
|
||||
"from": "11.1.0",
|
||||
"to": "11.1.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -565,7 +597,7 @@
|
||||
"from": "5.5.1",
|
||||
"to": "5.5.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -573,7 +605,7 @@
|
||||
"from": "7.37.5",
|
||||
"to": "7.37.5",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -581,7 +613,7 @@
|
||||
"from": "4.6.2",
|
||||
"to": "4.6.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -589,7 +621,7 @@
|
||||
"from": "3.0.1",
|
||||
"to": "3.0.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -597,7 +629,7 @@
|
||||
"from": "12.1.1",
|
||||
"to": "12.1.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -605,7 +637,7 @@
|
||||
"from": "3.2.0",
|
||||
"to": "3.2.0",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -613,7 +645,7 @@
|
||||
"from": "9.1.7",
|
||||
"to": "9.1.7",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -621,7 +653,7 @@
|
||||
"from": "15.5.2",
|
||||
"to": "15.5.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -629,7 +661,7 @@
|
||||
"from": "8.4.38",
|
||||
"to": "8.4.38",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -637,15 +669,23 @@
|
||||
"from": "3.6.2",
|
||||
"to": "3.6.2",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "prettier-plugin-tailwindcss",
|
||||
"from": "0.6.13",
|
||||
"from": "0.6.14",
|
||||
"to": "0.6.14",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-24T13:59:11.231Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "shadcn",
|
||||
"from": "3.4.1",
|
||||
"to": "3.4.1",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -653,15 +693,15 @@
|
||||
"from": "0.1.20",
|
||||
"to": "0.1.20",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
"name": "tailwindcss",
|
||||
"from": "3.4.3",
|
||||
"from": "4.1.13",
|
||||
"to": "4.1.13",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-24T13:59:11.231Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
},
|
||||
{
|
||||
"section": "devDependencies",
|
||||
@@ -669,6 +709,6 @@
|
||||
"from": "5.5.4",
|
||||
"to": "5.5.4",
|
||||
"strategy": "installed",
|
||||
"generatedAt": "2025-09-10T11:50:17.554Z"
|
||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||
}
|
||||
]
|
||||
|
||||
1740
ui/package-lock.json
generated
1740
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,7 @@
|
||||
"alert": "6.0.2",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"d3": "7.9.0",
|
||||
"date-fns": "4.1.0",
|
||||
"framer-motion": "11.18.2",
|
||||
"intl-messageformat": "10.7.16",
|
||||
@@ -72,17 +73,21 @@
|
||||
"sharp": "0.33.5",
|
||||
"tailwind-merge": "3.3.1",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"topojson-client": "3.1.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"uuid": "11.1.0",
|
||||
"world-atlas": "2.0.2",
|
||||
"zod": "4.1.11",
|
||||
"zustand": "5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "5.2.1",
|
||||
"@types/d3": "7.4.3",
|
||||
"@playwright/test": "1.56.1",
|
||||
"@types/node": "20.5.7",
|
||||
"@types/react": "19.1.13",
|
||||
"@types/react-dom": "19.1.9",
|
||||
"@types/topojson-client": "3.1.5",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.18.0",
|
||||
"@typescript-eslint/parser": "7.18.0",
|
||||
|
||||
@@ -1,8 +1,59 @@
|
||||
@import "tailwindcss";
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
@theme {
|
||||
/* Chart Severity Colors - Dark Theme */
|
||||
--chart-info: #2e51b2;
|
||||
--chart-warning: #fdd34f;
|
||||
--chart-warning-emphasis: #ff7d19;
|
||||
--chart-danger: #ff3077;
|
||||
--chart-danger-emphasis: #971348;
|
||||
|
||||
/* Chart Status Colors */
|
||||
--chart-success-color: #86da26;
|
||||
--chart-fail: #db2b49;
|
||||
|
||||
/* Chart Radar Colors */
|
||||
--chart-radar-primary: #b51c80;
|
||||
--chart-radar-primary-rgb: 181 28 128;
|
||||
|
||||
/* Chart Provider Colors */
|
||||
--chart-provider-aws: #ff9900;
|
||||
--chart-provider-azure: #00bcd4;
|
||||
--chart-provider-google: #EA4335;
|
||||
|
||||
/* Chart UI Colors - Dark Theme (defaults) */
|
||||
--chart-text-primary: #ffffff;
|
||||
--chart-text-secondary: #94a3b8;
|
||||
--chart-border: #475569;
|
||||
--chart-border-emphasis: #334155;
|
||||
--chart-background: #1e293b;
|
||||
|
||||
/* Chart Alert Colors */
|
||||
--chart-alert-bg: #432232;
|
||||
--chart-alert-text: #f54280;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
/* Light Theme Chart Colors */
|
||||
--chart-info: #1e40af;
|
||||
--chart-warning: #d97706;
|
||||
--chart-warning-emphasis: #dc2626;
|
||||
--chart-danger: #dc2626;
|
||||
--chart-danger-emphasis: #991b1b;
|
||||
--chart-success-color: #16a34a;
|
||||
--chart-fail: #dc2626;
|
||||
--chart-radar-primary: #9d174d;
|
||||
--chart-text-primary: #1f2937;
|
||||
--chart-text-secondary: #6b7280;
|
||||
--chart-border: #d1d5db;
|
||||
--chart-border-emphasis: #9ca3af;
|
||||
--chart-background: #f9fafb;
|
||||
--chart-alert-bg: #fecdd3;
|
||||
--chart-alert-text: #be123c;
|
||||
|
||||
/* Chart HSL values */
|
||||
--chart-success: 146 80% 35%;
|
||||
--chart-fail: 339 90% 51%;
|
||||
--chart-muted: 45 93% 47%;
|
||||
@@ -17,6 +68,24 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Dark Theme Chart Colors */
|
||||
--chart-info: #2e51b2;
|
||||
--chart-warning: #fdd34f;
|
||||
--chart-warning-emphasis: #ff7d19;
|
||||
--chart-danger: #ff3077;
|
||||
--chart-danger-emphasis: #971348;
|
||||
--chart-success-color: #86da26;
|
||||
--chart-fail: #db2b49;
|
||||
--chart-radar-primary: #b51c80;
|
||||
--chart-text-primary: #ffffff;
|
||||
--chart-text-secondary: #94a3b8;
|
||||
--chart-border: #475569;
|
||||
--chart-border-emphasis: #334155;
|
||||
--chart-background: #1e293b;
|
||||
--chart-alert-bg: #432232;
|
||||
--chart-alert-text: #f54280;
|
||||
|
||||
/* Chart HSL values */
|
||||
--chart-success: 146 80% 35%;
|
||||
--chart-fail: 339 90% 51%;
|
||||
--chart-muted: 45 93% 47%;
|
||||
@@ -56,6 +125,7 @@
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
Reference in New Issue
Block a user