diff --git a/ui/dashboard/src/components/dashboards/Table/index.tsx b/ui/dashboard/src/components/dashboards/Table/index.tsx index d59d92b3..5d379a9b 100644 --- a/ui/dashboard/src/components/dashboards/Table/index.tsx +++ b/ui/dashboard/src/components/dashboards/Table/index.tsx @@ -46,6 +46,8 @@ import { usePanelControls } from "@powerpipe/hooks/usePanelControls"; import { usePopper } from "react-popper"; import { useSearchParams } from "react-router-dom"; import { useVirtualizer } from "@tanstack/react-virtual"; +import useCopyToClipboard from "@powerpipe/hooks/useCopyToClipboard"; +import { AsyncNoop } from "@powerpipe/types/func"; const ExternalLink = getComponent("external_link"); @@ -488,6 +490,7 @@ const CellControls = ({ const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: "bottom-start", }); + const { copy, copySuccess } = useCopyToClipboard(); return ( <> @@ -502,11 +505,14 @@ const CellControls = ({ >
- addFilter("equal", column.name, value, context) + iconClassName={copySuccess ? "text-ok" : undefined} + icon={ + copySuccess + ? "materialsymbols-solid:content_copy" + : "content_copy" } + title="Copy value" + onClick={!copySuccess ? async () => copy(value) : undefined} /> { +const CellControl = ({ + iconClassName, + icon, + title, + onClick, +}: { + iconClassName?: string; + icon: string; + title: string; + onClick: AsyncNoop | undefined; +}) => { return (
- +
); }; @@ -637,7 +656,9 @@ const useTableFilters = (panelName: string, context?: string) => { value: any, context?: string, ) => { - const index = urlFilters.expressions?.findIndex( + const newUrlFilters = { ...urlFilters }; + const expressions = [...(newUrlFilters.expressions || [])]; + const index = expressions.findIndex( (e) => e.type === "dimension" && e.key === key && @@ -646,11 +667,8 @@ const useTableFilters = (panelName: string, context?: string) => { ); let newFilters = index !== undefined && index > -1 - ? [ - ...urlFilters.expressions?.slice(0, index), - ...urlFilters.expressions?.slice(index + 1), - ] - : urlFilters.expressions || []; + ? [...expressions.slice(0, index), ...expressions.slice(index + 1)] + : expressions || []; if ( newFilters.length === 1 && newFilters[0].operator === "equal" && @@ -676,10 +694,10 @@ const useTableFilters = (panelName: string, context?: string) => { context, }); } - urlFilters.expressions = newFilters; + newUrlFilters.expressions = newFilters; const newPanelFilters = { ...allFilters, - [panelName]: urlFilters, + [panelName]: newUrlFilters, }; searchParams.set("where", JSON.stringify(newPanelFilters)); setSearchParams(searchParams); @@ -689,28 +707,26 @@ const useTableFilters = (panelName: string, context?: string) => { const removeFilter = useCallback( (key: string, value: any, context: string) => { - const index = urlFilters.expressions?.findIndex( + const newUrlFilters = { ...urlFilters }; + let expressions = [...(newUrlFilters.expressions || [])]; + const index = expressions.findIndex( (e) => e.type === "dimension" && e.key === key && e.value === value && e.context === context, ); - const newFilters = + let newFilters = index !== undefined - ? [ - ...urlFilters.expressions?.slice(0, index), - ...urlFilters.expressions?.slice(index + 1), - ] - : urlFilters.expressions || []; + ? [...expressions.slice(0, index), ...expressions.slice(index + 1)] + : expressions; if (newFilters.length === 0) { - urlFilters.expressions = [{ operator: "equal" }]; - } else { - urlFilters.expressions = newFilters; + newFilters = [{ operator: "equal" }]; } + newUrlFilters.expressions = newFilters; const newPanelFilters = { ...allFilters, - [panelName]: urlFilters, + [panelName]: newUrlFilters, }; searchParams.set("where", JSON.stringify(newPanelFilters)); setSearchParams(searchParams); @@ -849,7 +865,7 @@ const TableViewVirtualizedRows = ({ <>
{filterEnabled && !!filters.length && ( -
+
{filters.map((filter) => { return (
{`${filter.key}: ${filter.value}`} diff --git a/ui/dashboard/src/hooks/useDetectionGrouping.tsx b/ui/dashboard/src/hooks/useDetectionGrouping.tsx index bc36ae40..b83a1498 100644 --- a/ui/dashboard/src/hooks/useDetectionGrouping.tsx +++ b/ui/dashboard/src/hooks/useDetectionGrouping.tsx @@ -35,6 +35,7 @@ import { PanelDefinition, PanelsMap, } from "@powerpipe/types"; +import { KeyValuePairs } from "@powerpipe/components/dashboards/common/types"; import { LeafNodeDataRow } from "@powerpipe/components/dashboards/common"; import { useDashboardState } from "./useDashboardState"; import { useDashboardControls } from "@powerpipe/components/dashboards/layout/Dashboard/DashboardControlsProvider"; @@ -573,74 +574,93 @@ function recordFilterValues( const includeResult = ( result: DetectionResult, - filterConfig: Filter, + panel: PanelDefinition, + allFilters: KeyValuePairs, ): boolean => { - if ( - !filterConfig || - !filterConfig.expressions || - filterConfig.expressions.length === 0 - ) { + // If no filters, include this + if (Object.keys(allFilters).length === 0) { + return true; + } + + const filterForRootPanel = allFilters[panel.name]; + const filterForDetection = allFilters[result.detection.name]; + + // If no filters for the parent panel, or this panel, include + if (!filterForRootPanel && !filterForDetection) { return true; } + let matches: boolean[] = []; - for (const filter of filterConfig.expressions) { - if (!filter.type) { - continue; - } + for (const filter of [filterForRootPanel, filterForDetection].filter( + (f) => !!f && !!f.expressions?.length, + )) { + for (const expression of filter.expressions || []) { + if (!expression.type) { + continue; + } - switch (filter.type) { - case "benchmark": { - let matchesTrunk = false; - for (const benchmark of result.benchmark_trunk || []) { - const match = applyFilter(filter, benchmark.name); - if (match) { - matchesTrunk = true; - break; + switch (expression.type) { + case "benchmark": { + let matchesTrunk = false; + for (const benchmark of result.benchmark_trunk || []) { + const match = applyFilter(expression, benchmark.name); + if (match) { + matchesTrunk = true; + break; + } } + matches.push(matchesTrunk); + break; } - matches.push(matchesTrunk); - break; - } - case "detection": { - matches.push(applyFilter(filter, result.detection.name)); - break; - } - case "dimension": { - let newRows: LeafNodeDataRow[] = []; - if (filter.context && result.detection.name !== filter.context) { - newRows = result.rows || []; - } else { - let includeRow = false; - for (const row of result.rows || []) { - includeRow = - filter.key in row && applyFilter(filter, row[filter.key]); - if (includeRow) { - newRows.push(row); - } else { + case "detection": { + matches.push(applyFilter(expression, result.detection.name)); + break; + } + case "dimension": { + let newRows: LeafNodeDataRow[] = []; + if ( + expression.context && + result.detection.name !== expression.context + ) { + newRows = result.rows || []; + } else { + let includeRow = false; + for (const row of result.rows || []) { + includeRow = + !!expression.key && + expression.key in row && + applyFilter(expression, row[expression.key]); + if (includeRow) { + newRows.push(row); + } else { + } } } + result.rows = newRows; + matches.push(true); + break; } - result.rows = newRows; - matches.push(true); - break; - } - case "detection_tag": { - let matchesTags = false; - for (const [tagKey, tagValue] of Object.entries(result.tags || {})) { - if (filter.key === tagKey && applyFilter(filter, tagValue)) { - matchesTags = true; - break; + case "detection_tag": { + let matchesTags = false; + for (const [tagKey, tagValue] of Object.entries(result.tags || {})) { + if ( + expression.key === tagKey && + applyFilter(expression, tagValue) + ) { + matchesTags = true; + break; + } } + matches.push(matchesTags); + break; } - matches.push(matchesTags); - break; - } - case "severity": { - matches.push(applyFilter(filter, result.severity || "")); - break; + case "severity": { + matches.push(applyFilter(expression, result.severity || "")); + break; + } + default: + matches.push(true); } - default: - matches.push(true); } } return matches.every((m) => m); @@ -653,7 +673,7 @@ const useGroupingInternal = ( groupingConfig: DetectionDisplayGroup[], skip = false, ) => { - const { filter: checkFilterConfig } = useFilterConfig(definition?.name); + const { allFilters } = useFilterConfig(definition?.name); return useMemo(() => { const filterValues = { @@ -703,7 +723,7 @@ const useGroupingInternal = ( recordFilterValues(filterValues, detectionResult); // See if the result needs to be filtered - if (!includeResult(detectionResult, checkFilterConfig)) { + if (!includeResult(detectionResult, definition, allFilters)) { return; } @@ -747,7 +767,7 @@ const useGroupingInternal = ( detectionNodeStates, filterValues, ] as const; - }, [checkFilterConfig, definition, groupingConfig, panelsMap, skip]); + }, [allFilters, definition, groupingConfig, panelsMap, skip]); }; const GroupingProvider = ({