Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/api/services/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,15 @@ export const deleteView = async (id: string) => {
export const getViewData = async (
namespace: string,
name: string,
headers?: Record<string, string>
headers?: Record<string, string>,
filter?: string
): Promise<ViewResult> => {
const response = await fetch(`/api/view/${namespace}/${name}`, {
let url = `/api/view/${namespace}/${name}`;
if (filter && filter.trim()) {
url += `?filter=${encodeURIComponent(filter)}`;
}

const response = await fetch(url, {
credentials: "include",
headers
});
Expand Down
56 changes: 56 additions & 0 deletions src/pages/audit-report/components/BadgeCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from "react";

interface BadgeCellProps {
value: string;
}

// Medium saturation color palette suitable for badge backgrounds
const BADGE_COLOR_BANK = [
"#BBDEFB",
"#C8E6C9",
"#FFE0B2",
"#F8BBD9",
"#E1BEE7",
"#B2DFDB",
"#FFF9C4",
"#FFCDD2",
"#B3E5FC",
"#DCEDC8",
"#FFE082",
"#F0F4C3",
"#C5CAE9",
"#D7CCC8",
"#E0E0E0",
"#B3E5FC",
"#C8E6C9",
"#FFCC80",
"#F48FB1",
"#CE93D8"
];

// Global map to track assigned colors and ensure uniqueness
const colorAssignments = new Map<string, string>();
let nextColorIndex = 0;

const BadgeCell: React.FC<BadgeCellProps> = ({ value }) => {
if (!value) return <span>-</span>;

let backgroundColor = colorAssignments.get(value);
if (!backgroundColor) {
backgroundColor =
BADGE_COLOR_BANK[nextColorIndex % BADGE_COLOR_BANK.length];
colorAssignments.set(value, backgroundColor);
nextColorIndex++;
}

return (
<span
className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium text-gray-800"
style={{ backgroundColor }}
>
{value}
</span>
);
};

export default BadgeCell;
12 changes: 6 additions & 6 deletions src/pages/audit-report/components/DynamicDataTable.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ const gaugeColumns: ViewColumnDef[] = [
max: 100,
unit: "%",
thresholds: [
{ value: 70, color: "orange" },
{ value: 90, color: "red" }
{ percent: 70, color: "orange" },
{ percent: 90, color: "red" }
]
}
},
Expand All @@ -80,8 +80,8 @@ const gaugeColumns: ViewColumnDef[] = [
max: 16,
unit: "GB",
thresholds: [
{ value: 12, color: "orange" },
{ value: 15, color: "red" }
{ percent: 75, color: "orange" },
{ percent: 94, color: "red" }
]
}
},
Expand All @@ -93,8 +93,8 @@ const gaugeColumns: ViewColumnDef[] = [
max: 1000,
unit: "GB",
thresholds: [
{ value: 800, color: "orange" },
{ value: 950, color: "red" }
{ percent: 80, color: "orange" },
{ percent: 95, color: "red" }
]
}
}
Expand Down
57 changes: 56 additions & 1 deletion src/pages/audit-report/components/DynamicDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import DataTable from "./DataTable";
import HealthBadge, { HealthType } from "./HealthBadge";
import StatusBadge from "./StatusBadge";
import GaugeCell from "./GaugeCell";
import BadgeCell from "./BadgeCell";
import { Link } from "react-router-dom";
import { formatBytes } from "../../../utils/common";
import { formatDuration as formatDurationMs } from "../../../utils/date";
import { FilterByCellValue } from "@flanksource-ui/ui/DataTable/FilterByCellValue";

interface DynamicDataTableProps {
columns: ViewColumnDef[];
Expand Down Expand Up @@ -120,6 +122,37 @@ const DynamicDataTable: React.FC<DynamicDataTableProps> = ({
return multiplier ? num * multiplier : null;
};

// Format values based on unit type for gauge display
const formatValueWithUnit = (value: any, unit?: string): any => {
if (!unit || value == null) return value;

switch (unit) {
case "bytes":
if (typeof value === "number") {
return value; // Keep numeric value for gauge calculation, unit display handled by GaugeCell
} else if (typeof value === "string") {
const parsedBytes = parseMemoryUnit(value);
return parsedBytes !== null ? parsedBytes : value;
}
return value;

case "millicores":
case "millicore":
// Use existing formatMillicore function logic but return numeric value for gauge
if (typeof value === "string") {
const numericValue = value.replace(/m$/, "");
const millicoreValue = parseInt(numericValue, 10);
return !isNaN(millicoreValue) ? millicoreValue : value;
} else if (typeof value === "number") {
return value;
}
return value;

default:
return value;
}
};

const renderCellValue = (value: any, column: ViewColumnDef, row: any) => {
if (value == null) return "-";

Expand Down Expand Up @@ -190,7 +223,25 @@ const DynamicDataTable: React.FC<DynamicDataTableProps> = ({
if (!column.gauge) {
cellContent = String(value);
} else {
cellContent = <GaugeCell value={value} gauge={column.gauge} />;
// Check if row attributes contain a max value for this column
const rowAttributes = row.__rowAttributes as Record<string, any>;
const maxFromAttributes = rowAttributes?.[column.name]?.max;

const gaugeConfig =
maxFromAttributes !== undefined
? { ...column.gauge, max: Number(maxFromAttributes) }
: column.gauge;

// Apply unit formatting based on column.unit
const formattedValue = formatValueWithUnit(value, column.unit);
const finalGaugeConfig = {
...gaugeConfig,
unit: column.unit || gaugeConfig.unit
};

cellContent = (
<GaugeCell value={formattedValue} gauge={finalGaugeConfig} />
);
}
break;

Expand All @@ -202,6 +253,10 @@ const DynamicDataTable: React.FC<DynamicDataTableProps> = ({
);
break;

case "badge":
cellContent = <BadgeCell value={String(value)} />;
break;

default:
cellContent = String(value);
break;
Expand Down
24 changes: 12 additions & 12 deletions src/pages/audit-report/components/GaugeCell.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ const basicGaugeConfig: GaugeConfig = {
max: 100,
unit: "%",
thresholds: [
{ value: 80, color: "#ef4444" },
{ value: 60, color: "#f59e0b" },
{ value: 0, color: "#10b981" }
{ percent: 80, color: "#ef4444" },
{ percent: 60, color: "#f59e0b" },
{ percent: 0, color: "#10b981" }
]
};

Expand Down Expand Up @@ -53,9 +53,9 @@ export const WithObjectValue: Story = {
max: 200,
unit: "MB",
thresholds: [
{ value: 160, color: "#ef4444" },
{ value: 120, color: "#f59e0b" },
{ value: 0, color: "#10b981" }
{ percent: 80, color: "#ef4444" },
{ percent: 60, color: "#f59e0b" },
{ percent: 0, color: "#10b981" }
]
}
}
Expand All @@ -69,9 +69,9 @@ export const MemoryUsage: Story = {
max: 2,
unit: "GB",
thresholds: [
{ value: 1.8, color: "#ef4444" },
{ value: 1.5, color: "#f59e0b" },
{ value: 0, color: "#10b981" }
{ percent: 90, color: "#ef4444" },
{ percent: 75, color: "#f59e0b" },
{ percent: 0, color: "#10b981" }
]
}
}
Expand All @@ -85,9 +85,9 @@ export const CPUUsage: Story = {
max: 100,
unit: "%",
thresholds: [
{ value: 90, color: "#ef4444" },
{ value: 70, color: "#f59e0b" },
{ value: 0, color: "#10b981" }
{ percent: 90, color: "#ef4444" },
{ percent: 70, color: "#f59e0b" },
{ percent: 0, color: "#10b981" }
]
}
}
Expand Down
24 changes: 23 additions & 1 deletion src/pages/audit-report/components/GaugeCell.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { GaugeConfig } from "../types";
import { generateGaugeData } from "./View/panels/utils";
import { formatBytes } from "../../../utils/common";

interface GaugeCellProps {
value: number | { min: number; max: number; value: number };
Expand All @@ -18,6 +19,27 @@ const GaugeCell: React.FC<GaugeCellProps> = ({ value, gauge }) => {
const percentage = gaugeData.value;
const color = gaugeData.color;

// Format display value based on unit
const formatDisplayValue = (value: number, unit?: string): string => {
if (!unit) return value.toString();

switch (unit) {
case "bytes":
return formatBytes(value);
case "millicores":
case "millicore":
// Use the same logic as formatMillicore function in DynamicDataTable
if (value >= 1000) {
return `${(value / 1000).toFixed(2)} cores`;
}
return `${value}m`;
default:
return `${value} ${unit}`;
}
};

const displayValue = formatDisplayValue(gaugeValue, gaugeConfig?.unit);

return (
<div className="flex items-center gap-2">
<div className="flex w-20 items-center gap-2">
Expand All @@ -33,7 +55,7 @@ const GaugeCell: React.FC<GaugeCellProps> = ({ value, gauge }) => {
</div>
{/* Value text */}
<span className="min-w-fit text-xs font-medium text-gray-700">
{gaugeValue} {gaugeConfig?.unit}
{displayValue}
</span>
</div>
</div>
Expand Down
69 changes: 65 additions & 4 deletions src/pages/audit-report/components/View/View.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from "react";
import React, { useMemo } from "react";
import { Box } from "lucide-react";
import DynamicDataTable from "../DynamicDataTable";
import { ViewResult } from "../../types";
import FormikSearchInputClearable from "@flanksource-ui/components/Forms/Formik/FormikSearchInputClearable";
import { ViewColumnDropdown } from "../ViewColumnDropdown";
import {
NumberPanel,
TablePanel,
Expand All @@ -14,6 +16,8 @@ interface ViewProps {
title: string;
icon?: string;
view: ViewResult;
showSearch?: boolean;
dropdownOptionsData?: ViewResult; // Unfiltered data for dropdown options
}

const renderPanel = (panel: any, index: number) => {
Expand All @@ -33,7 +37,43 @@ const renderPanel = (panel: any, index: number) => {
}
};

const View: React.FC<ViewProps> = ({ title, icon, view }) => {
const View: React.FC<ViewProps> = ({
title,
icon,
view,
showSearch = false,
dropdownOptionsData
}) => {
const hasDataTable = view.columns && view.columns.length > 0;

// Extract filterable columns and their unique values from unfiltered data
const filterableColumns = useMemo(() => {
const sourceData = dropdownOptionsData || view;

if (!sourceData.columns || !sourceData.rows) return [];

return sourceData.columns
.map((column, index) => {
if (column.filter?.type !== "multiselect") return null;

const uniqueValues = [
...new Set(
sourceData
.rows!.map((row) => row[index])
.filter((value) => value != null && value !== "")
.map(String)
)
].sort();

return { column, columnIndex: index, uniqueValues };
})
.filter(Boolean) as Array<{
column: any;
columnIndex: number;
uniqueValues: string[];
}>;
}, [view, dropdownOptionsData]);

return (
<div>
{title !== "" && (
Expand All @@ -50,8 +90,29 @@ const View: React.FC<ViewProps> = ({ title, icon, view }) => {
</div>
)}

{view.rows && view.columns && (
<DynamicDataTable columns={view.columns} rows={view.rows} />
{hasDataTable && (
<div className="space-y-4">
{showSearch && (
<div className="flex flex-wrap items-center gap-2">
<FormikSearchInputClearable
name="filter"
placeholder="Filter results..."
className="w-80"
/>

{filterableColumns.map(({ column, uniqueValues }) => (
<ViewColumnDropdown
key={column.name}
label={column.name}
paramsKey={column.name.toLowerCase()}
options={uniqueValues}
/>
))}
</div>
)}

<DynamicDataTable columns={view.columns!} rows={view.rows || []} />
</div>
)}
</div>
</div>
Expand Down
Loading
Loading