diff --git a/src/api/services/views.ts b/src/api/services/views.ts index c9faea389..2a4a29cdd 100644 --- a/src/api/services/views.ts +++ b/src/api/services/views.ts @@ -65,9 +65,15 @@ export const deleteView = async (id: string) => { export const getViewData = async ( namespace: string, name: string, - headers?: Record + headers?: Record, + filter?: string ): Promise => { - 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 }); diff --git a/src/pages/audit-report/components/BadgeCell.tsx b/src/pages/audit-report/components/BadgeCell.tsx new file mode 100644 index 000000000..47b495d18 --- /dev/null +++ b/src/pages/audit-report/components/BadgeCell.tsx @@ -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(); +let nextColorIndex = 0; + +const BadgeCell: React.FC = ({ value }) => { + if (!value) return -; + + let backgroundColor = colorAssignments.get(value); + if (!backgroundColor) { + backgroundColor = + BADGE_COLOR_BANK[nextColorIndex % BADGE_COLOR_BANK.length]; + colorAssignments.set(value, backgroundColor); + nextColorIndex++; + } + + return ( + + {value} + + ); +}; + +export default BadgeCell; diff --git a/src/pages/audit-report/components/DynamicDataTable.stories.tsx b/src/pages/audit-report/components/DynamicDataTable.stories.tsx index 3375ea882..1ddf8a82f 100644 --- a/src/pages/audit-report/components/DynamicDataTable.stories.tsx +++ b/src/pages/audit-report/components/DynamicDataTable.stories.tsx @@ -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" } ] } }, @@ -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" } ] } }, @@ -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" } ] } } diff --git a/src/pages/audit-report/components/DynamicDataTable.tsx b/src/pages/audit-report/components/DynamicDataTable.tsx index 1ceb69589..9e1ed22ef 100644 --- a/src/pages/audit-report/components/DynamicDataTable.tsx +++ b/src/pages/audit-report/components/DynamicDataTable.tsx @@ -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[]; @@ -120,6 +122,37 @@ const DynamicDataTable: React.FC = ({ 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 "-"; @@ -190,7 +223,25 @@ const DynamicDataTable: React.FC = ({ if (!column.gauge) { cellContent = String(value); } else { - cellContent = ; + // Check if row attributes contain a max value for this column + const rowAttributes = row.__rowAttributes as Record; + 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 = ( + + ); } break; @@ -202,6 +253,10 @@ const DynamicDataTable: React.FC = ({ ); break; + case "badge": + cellContent = ; + break; + default: cellContent = String(value); break; diff --git a/src/pages/audit-report/components/GaugeCell.stories.tsx b/src/pages/audit-report/components/GaugeCell.stories.tsx index ae5363869..8fc854643 100644 --- a/src/pages/audit-report/components/GaugeCell.stories.tsx +++ b/src/pages/audit-report/components/GaugeCell.stories.tsx @@ -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" } ] }; @@ -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" } ] } } @@ -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" } ] } } @@ -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" } ] } } diff --git a/src/pages/audit-report/components/GaugeCell.tsx b/src/pages/audit-report/components/GaugeCell.tsx index ce42f0ef4..2f5a5f96e 100644 --- a/src/pages/audit-report/components/GaugeCell.tsx +++ b/src/pages/audit-report/components/GaugeCell.tsx @@ -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 }; @@ -18,6 +19,27 @@ const GaugeCell: React.FC = ({ 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 (
@@ -33,7 +55,7 @@ const GaugeCell: React.FC = ({ value, gauge }) => {
{/* Value text */} - {gaugeValue} {gaugeConfig?.unit} + {displayValue}
diff --git a/src/pages/audit-report/components/View/View.tsx b/src/pages/audit-report/components/View/View.tsx index 37eb9ffb2..7597c6ddb 100644 --- a/src/pages/audit-report/components/View/View.tsx +++ b/src/pages/audit-report/components/View/View.tsx @@ -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, @@ -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) => { @@ -33,7 +37,43 @@ const renderPanel = (panel: any, index: number) => { } }; -const View: React.FC = ({ title, icon, view }) => { +const View: React.FC = ({ + 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 (
{title !== "" && ( @@ -50,8 +90,29 @@ const View: React.FC = ({ title, icon, view }) => {
)} - {view.rows && view.columns && ( - + {hasDataTable && ( +
+ {showSearch && ( +
+ + + {filterableColumns.map(({ column, uniqueValues }) => ( + + ))} +
+ )} + + +
)} diff --git a/src/pages/audit-report/components/View/panels/GaugePanel.stories.tsx b/src/pages/audit-report/components/View/panels/GaugePanel.stories.tsx index 270297e26..f9fd37fe9 100644 --- a/src/pages/audit-report/components/View/panels/GaugePanel.stories.tsx +++ b/src/pages/audit-report/components/View/panels/GaugePanel.stories.tsx @@ -22,9 +22,9 @@ const basicGaugeSummary: PanelResult = { max: 100, unit: "%", thresholds: [ - { value: 0, color: "#10b981" }, - { value: 60, color: "#f59e0b" }, - { value: 80, color: "#ef4444" } + { percent: 0, color: "#10b981" }, + { percent: 60, color: "#f59e0b" }, + { percent: 80, color: "#ef4444" } ] }, rows: [{ value: 75 }] @@ -65,9 +65,9 @@ export const CPUUsage: Story = { max: 100, unit: "%", thresholds: [ - { value: 0, color: "#10b981" }, - { value: 70, color: "#f59e0b" }, - { value: 90, color: "#ef4444" } + { percent: 0, color: "#10b981" }, + { percent: 70, color: "#f59e0b" }, + { percent: 90, color: "#ef4444" } ] }, rows: [{ value: 45 }] @@ -86,9 +86,9 @@ export const DiskUsage: Story = { max: 500, unit: "GB", thresholds: [ - { value: 0, color: "#ef4444" }, - { value: 100, color: "#f59e0b" }, - { value: 200, color: "#10b981" } + { percent: 0, color: "#10b981" }, + { percent: 60, color: "#f59e0b" }, + { percent: 80, color: "#ef4444" } ] }, rows: [{ value: 150 }] @@ -106,9 +106,9 @@ export const WithoutDescription: Story = { max: 100, unit: "ms", thresholds: [ - { value: 0, color: "#10b981" }, - { value: 50, color: "#f59e0b" }, - { value: 80, color: "#ef4444" } + { percent: 0, color: "#10b981" }, + { percent: 50, color: "#f59e0b" }, + { percent: 80, color: "#ef4444" } ] }, rows: [{ value: 35 }] diff --git a/src/pages/audit-report/components/View/panels/GaugePanel.tsx b/src/pages/audit-report/components/View/panels/GaugePanel.tsx index edefa6bd9..a116e9a70 100644 --- a/src/pages/audit-report/components/View/panels/GaugePanel.tsx +++ b/src/pages/audit-report/components/View/panels/GaugePanel.tsx @@ -16,7 +16,9 @@ const GaugePanel: React.FC = ({ summary }) => { const gaugeData = generateGaugeData(row, summary.gauge); const outerArcLength = 204; // π * 65 for outer threshold arc const sortedThresholds = summary.gauge.thresholds - ? [...summary.gauge.thresholds].sort((a, b) => a.value - b.value) + ? [...summary.gauge.thresholds].sort( + (a, b) => a.percent - b.percent + ) : []; return ( @@ -56,20 +58,9 @@ const GaugePanel: React.FC = ({ summary }) => { const nextThreshold = sortedThresholds[thresholdIndex + 1]; - const startPercentage = - gaugeData.min !== undefined && - gaugeData.max !== undefined - ? ((currentThreshold.value - gaugeData.min) / - (gaugeData.max - gaugeData.min)) * - 100 - : 0; + const startPercentage = currentThreshold.percent; const endPercentage = nextThreshold - ? gaugeData.min !== undefined && - gaugeData.max !== undefined - ? ((nextThreshold.value - gaugeData.min) / - (gaugeData.max - gaugeData.min)) * - 100 - : 100 + ? nextThreshold.percent : 100; const startLength = diff --git a/src/pages/audit-report/components/View/panels/utils.ts b/src/pages/audit-report/components/View/panels/utils.ts index f4d6ecb9d..b337661f7 100644 --- a/src/pages/audit-report/components/View/panels/utils.ts +++ b/src/pages/audit-report/components/View/panels/utils.ts @@ -37,28 +37,26 @@ export const generatePieChartData = (rows: Record[]) => { }; export const getGaugeColor = ( - value: number, - thresholds: Array<{ value: number; color: string }> + percentage: number, + thresholds: Array<{ percent: number; color: string }> ) => { - // Sort thresholds by value in ascending order - const sortedThresholds = [...thresholds].sort((a, b) => a.value - b.value); + // Sort thresholds by percent in ascending order + const sortedThresholds = [...thresholds].sort( + (a, b) => a.percent - b.percent + ); - // Find the appropriate range and return its color - for (let i = 0; i < sortedThresholds.length; i++) { - const currentThreshold = sortedThresholds[i]; - const nextThreshold = sortedThresholds[i + 1]; + // Find the highest threshold that the percentage is >= to + let selectedThreshold = sortedThresholds[0]; // default to first threshold - // If this is the last threshold or value is less than next threshold - if (!nextThreshold || value < nextThreshold.value) { - // Value falls in this range if it's >= current threshold - if (value >= currentThreshold.value) { - return currentThreshold.color; - } + for (const threshold of sortedThresholds) { + if (percentage >= threshold.percent) { + selectedThreshold = threshold; + } else { + break; } } - // Default to the first threshold color if value is below all thresholds - return sortedThresholds[0]?.color || COLOR_BANK[0]; + return selectedThreshold?.color || COLOR_BANK[0]; }; export const generateGaugeData = ( @@ -66,14 +64,15 @@ export const generateGaugeData = ( gauge?: GaugeConfig ) => { const value = row.value || 0; + const min = gauge?.min || 0; let percentage = 0; - if (gauge && gauge.max !== gauge.min) { - percentage = ((value - gauge.min) / (gauge.max - gauge.min)) * 100; + if (gauge && gauge.max !== min) { + percentage = ((value - min) / (gauge.max - min)) * 100; } const clampedPercentage = Math.max(0, Math.min(100, percentage)); const color = gauge?.thresholds - ? getGaugeColor(value, gauge.thresholds) + ? getGaugeColor(clampedPercentage, gauge.thresholds) : COLOR_BANK[0]; return { diff --git a/src/pages/audit-report/components/ViewColumnDropdown.tsx b/src/pages/audit-report/components/ViewColumnDropdown.tsx new file mode 100644 index 000000000..3b5776df2 --- /dev/null +++ b/src/pages/audit-report/components/ViewColumnDropdown.tsx @@ -0,0 +1,67 @@ +import TristateReactSelect, { + TriStateOptions +} from "@flanksource-ui/ui/Dropdowns/TristateReactSelect"; +import { useField } from "formik"; +import { useMemo } from "react"; + +type ViewColumnDropdownProps = { + label: string; + paramsKey: string; + options: string[]; +}; + +export function ViewColumnDropdown({ + label, + paramsKey, + options +}: ViewColumnDropdownProps) { + const [field] = useField({ + name: paramsKey + }); + + const dropdownOptions = useMemo(() => { + return options.map( + (option) => + ({ + value: option, + label: option, + id: option + }) satisfies TriStateOptions + ); + }, [options]); + + const sortedOptions = useMemo( + () => + dropdownOptions.sort((a, b) => { + if (a.label === "All") { + return -1; + } + if (b.label === "All") { + return 1; + } + return a.label?.localeCompare(b.label); + }), + [dropdownOptions] + ); + + return ( + { + if (value && value !== "all") { + field.onChange({ + target: { name: paramsKey, value: value } + }); + } else { + field.onChange({ + target: { name: paramsKey, value: undefined } + }); + } + }} + value={field.value} + className="w-auto max-w-[400px]" + label={label} + /> + ); +} diff --git a/src/pages/audit-report/types/index.ts b/src/pages/audit-report/types/index.ts index 0a1b5d3cc..e7617a8f9 100644 --- a/src/pages/audit-report/types/index.ts +++ b/src/pages/audit-report/types/index.ts @@ -189,8 +189,12 @@ export interface Restore { completedDate?: string; } +interface ViewColumnDefFilter { + type: "multiselect"; +} export interface ViewColumnDef { name: string; + filter?: ViewColumnDefFilter; type: | "string" | "number" @@ -204,10 +208,12 @@ export interface ViewColumnDef { | "decimal" | "millicore" | "url" + | "badge" | "row_attributes"; description?: string; gauge?: GaugeConfig; hidden?: boolean; + unit?: string; } type ViewRow = any[]; @@ -224,7 +230,7 @@ export interface GaugeConfig { max: number; unit?: string; thresholds?: { - value: number; + percent: number; color: string; }[]; } diff --git a/src/pages/views/components/SingleView.tsx b/src/pages/views/components/SingleView.tsx index 33293d1b4..9c786ce22 100644 --- a/src/pages/views/components/SingleView.tsx +++ b/src/pages/views/components/SingleView.tsx @@ -1,7 +1,10 @@ -import React, { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useSearchParams } from "react-router-dom"; +import { tristateOutputToQueryParamValue } from "@flanksource-ui/ui/Dropdowns/TristateReactSelect"; import { getViewById, getViewData } from "../../../api/services/views"; import View from "../../audit-report/components/View/View"; +import FormikFilterForm from "@flanksource-ui/components/Forms/FormikFilterForm"; import { Head } from "../../../ui/Head"; import { Icon } from "../../../ui/Icons/Icon"; @@ -11,8 +14,30 @@ interface SingleViewProps { const SingleView: React.FC = ({ id }) => { const [error, setError] = useState(); + const [searchParams] = useSearchParams(); const queryClient = useQueryClient(); + // Collect and combine all filter parameters from URL + const filters = useMemo(() => { + const filterParts: string[] = []; + + const generalFilter = searchParams.get("filter"); + if (generalFilter) { + filterParts.push(generalFilter); + } + + for (const [key, value] of searchParams.entries()) { + if (key !== "filter" && value && value.trim()) { + const filterValue = tristateOutputToQueryParamValue(value); + if (filterValue) { + filterParts.push(`${key}=${filterValue}`); + } + } + } + + return filterParts.join(","); + }, [searchParams]); + const { data: viewData, isLoading: isLoadingView, @@ -26,18 +51,48 @@ const SingleView: React.FC = ({ id }) => { const view = viewData?.data?.[0]; + // Fetch unfiltered data to get complete dropdown options + const { data: unfilteredViewData, isLoading: isLoadingUnfilteredData } = + useQuery({ + queryKey: ["view-data-unfiltered", view?.namespace, view?.name], + queryFn: () => + getViewData(view?.namespace ?? "", view?.name ?? "", undefined, ""), + enabled: !!(view?.namespace && view?.name), + staleTime: 5 * 60 * 1000 // 5 minutes + }); + + // Fetch filtered data for display const { data: actualViewData, isLoading: isLoadingData, isFetching: isFetchingData, error: dataError } = useQuery({ - queryKey: ["view-data", view?.namespace, view?.name], - queryFn: () => getViewData(view?.namespace ?? "", view?.name ?? ""), + queryKey: ["view-data", view?.namespace, view?.name, filters], + queryFn: () => + getViewData(view?.namespace ?? "", view?.name ?? "", undefined, filters), enabled: !!(view?.namespace && view?.name), staleTime: 5 * 60 * 1000 // 5 minutes }); + // Calculate dynamic filter fields based on filterable columns + // This must be called before any conditional rendering to maintain hook order + const dynamicFilterFields = useMemo(() => { + const baseFields = ["filter"]; + + // Use unfiltered data to determine available columns, fall back to filtered data + const sourceData = unfilteredViewData || actualViewData; + if (sourceData?.columns) { + const filterableFields = sourceData.columns + .filter((column) => column.filter?.type === "multiselect") + .map((column) => column.name.toLowerCase()); + + return [...baseFields, ...filterableFields]; + } + + return baseFields; + }, [unfilteredViewData, actualViewData]); + useEffect(() => { if (!id) { setError("No view ID provided"); @@ -62,7 +117,7 @@ const SingleView: React.FC = ({ id }) => { setError(undefined); }, [id, viewError, dataError]); - const isLoading = isLoadingView || isLoadingData; + const isLoading = isLoadingView || isLoadingData || isLoadingUnfilteredData; // isRefreshing is true when data exists but is being refetched const isRefreshing = actualViewData && isFetchingData && !isLoadingData; @@ -124,11 +179,16 @@ const SingleView: React.FC = ({ id }) => { const handleForceRefresh = async () => { if (view?.namespace && view?.name) { - const freshData = await getViewData(view.namespace, view.name, { - "cache-control": "max-age=1" // To force a refresh - }); + const freshData = await getViewData( + view.namespace, + view.name, + { + "cache-control": "max-age=1" // To force a refresh + }, + filters + ); queryClient.setQueryData( - ["view-data", view.namespace, view.name], + ["view-data", view.namespace, view.name, filters], freshData ); } @@ -178,7 +238,18 @@ const SingleView: React.FC = ({ id }) => {
- + + +
diff --git a/src/ui/DataTable/FilterByCellValue.tsx b/src/ui/DataTable/FilterByCellValue.tsx index 3046c3999..94c42f92b 100644 --- a/src/ui/DataTable/FilterByCellValue.tsx +++ b/src/ui/DataTable/FilterByCellValue.tsx @@ -53,8 +53,10 @@ export function FilterByCellValue({ return (
-
{children}
-
+
+ {children} +
+
onClick(e, "include")} icon={}