diff --git a/ui/dashboard/package.json b/ui/dashboard/package.json index 4f405c46..e4fdeb34 100644 --- a/ui/dashboard/package.json +++ b/ui/dashboard/package.json @@ -46,6 +46,7 @@ "react-router-dom": "6.28.0", "react-router-hash-link": "2.4.3", "react-select": "5.8.3", + "react-split-pane": "0.1.92", "react-syntax-highlighter": "15.6.1", "react-time-picker": "7.0.0", "react-use-websocket": "4.11.1", diff --git a/ui/dashboard/src/components/CodeBlock/index.tsx b/ui/dashboard/src/components/CodeBlock/index.tsx index dfc22325..b6c84dc4 100644 --- a/ui/dashboard/src/components/CodeBlock/index.tsx +++ b/ui/dashboard/src/components/CodeBlock/index.tsx @@ -2,6 +2,7 @@ import CopyToClipboard, { CopyToClipboardProvider } from "../CopyToClipboard"; import hcl from "react-syntax-highlighter/dist/esm/languages/prism/hcl"; import json from "react-syntax-highlighter/dist/esm/languages/prism/json"; import sql from "react-syntax-highlighter/dist/esm/languages/prism/sql"; +import yaml from "react-syntax-highlighter/dist/esm/languages/prism/yaml"; import { classNames } from "@powerpipe/utils/styles"; import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; import { ThemeNames } from "@powerpipe/hooks/useTheme"; @@ -15,11 +16,12 @@ import { SyntaxHighlighter.registerLanguage("hcl", hcl); SyntaxHighlighter.registerLanguage("json", json); SyntaxHighlighter.registerLanguage("sql", sql); +SyntaxHighlighter.registerLanguage("yaml", yaml); type CodeBlockProps = { children: string; copyToClipboard?: boolean; - language?: "hcl" | "json" | "sql"; + language?: "hcl" | "json" | "sql" | "yaml"; style?: any; }; diff --git a/ui/dashboard/src/components/CopyToClipboard/index.tsx b/ui/dashboard/src/components/CopyToClipboard/index.tsx index 309f6d38..1ba4904c 100644 --- a/ui/dashboard/src/components/CopyToClipboard/index.tsx +++ b/ui/dashboard/src/components/CopyToClipboard/index.tsx @@ -1,9 +1,5 @@ import copy from "copy-to-clipboard"; import { classNames } from "@powerpipe/utils/styles"; -import { - CopyToClipboardIcon, - CopyToClipboardSuccessIcon, -} from "@powerpipe/constants/icons"; import { createContext, useCallback, @@ -11,6 +7,7 @@ import { useEffect, useState, } from "react"; +import Icon from "@powerpipe/components/Icon"; type ICopyToClipboardContext = { doCopy: boolean; @@ -78,13 +75,17 @@ const CopyToClipboard = ({ return ( <> {!copySuccess && ( - handleCopy(e)} /> )} {copySuccess && ( - + )} ); diff --git a/ui/dashboard/src/components/Icon/index.tsx b/ui/dashboard/src/components/Icon/index.tsx index 3237ea9d..536e84a4 100644 --- a/ui/dashboard/src/components/Icon/index.tsx +++ b/ui/dashboard/src/components/Icon/index.tsx @@ -4,7 +4,7 @@ import useDashboardIcons from "@powerpipe/hooks/useDashboardIcons"; type IconProps = { className?: string; icon: string; - onClick?: () => void; + onClick?: (e: any) => void; style?: any; title?: string; }; diff --git a/ui/dashboard/src/components/dashboards/Table/index.tsx b/ui/dashboard/src/components/dashboards/Table/index.tsx index ca916c5f..5d379a9b 100644 --- a/ui/dashboard/src/components/dashboards/Table/index.tsx +++ b/ui/dashboard/src/components/dashboards/Table/index.tsx @@ -40,11 +40,14 @@ import { KeyValuePairs, RowRenderResult } from "../common/types"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { PanelDefinition } from "@powerpipe/types"; import { ThemeProvider, ThemeWrapper } from "@powerpipe/hooks/useTheme"; +import { useDashboardPanelDetail } from "@powerpipe/hooks/useDashboardPanelDetail"; import { useDashboardSearchPath } from "@powerpipe/hooks/useDashboardSearchPath"; 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"); @@ -137,6 +140,7 @@ const getData = (columns: TableColumnInfo[], rows: LeafNodeDataRow[]) => { }; type CellValueProps = { + panel: PanelDefinition; column: TableColumnInfo; rowIndex: number; rowTemplateData: RowRenderResult[]; @@ -154,6 +158,7 @@ type CellValueProps = { }; const CellValue = ({ + panel, column, rowIndex, rowTemplateData, @@ -459,6 +464,8 @@ const CellValue = ({ { + const { selectSidePanel } = useDashboardPanelDetail(); + const { setShowPanelControls } = usePanelControls(); const [popperElement, setPopperElement] = useState(null); - // Need to define memoized / stable modifiers else the usePopper hook will infinitely re-render - // const noFlip = useMemo(() => ({ name: "flip", enabled: false }), []); - const offset = useMemo(() => { - return { - name: "offset", - options: { - offset: [4, 1], - }, - }; - }, []); const { styles, attributes } = usePopper(referenceElement, popperElement, { - modifiers: [offset], - placement: "right-end", + placement: "bottom-start", }); + const { copy, copySuccess } = useCopyToClipboard(); return ( <> @@ -502,21 +503,46 @@ const CellControls = ({ style={{ ...styles.popper }} {...attributes.popper} > -
+
copy(value) : undefined} + /> + addFilter("equal", column.name, value, context) } /> addFilter("not_equal", column.name, value, context) } /> + { + selectSidePanel({ + panel, + context: { + requestedColumnName: column.name, + rowIndex, + }, + }); + setShowPanelControls(false); + }} + />
@@ -528,14 +554,27 @@ const CellControls = ({ ); }; -const CellControl = ({ icon, title, onClick }) => { +const CellControl = ({ + iconClassName, + icon, + title, + onClick, +}: { + iconClassName?: string; + icon: string; + title: string; + onClick: AsyncNoop | undefined; +}) => { return (
- +
); }; @@ -617,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 && @@ -626,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" && @@ -656,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); @@ -669,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); @@ -738,7 +774,7 @@ const useDisableHoverOnScroll = (scrollElement: HTMLDivElement | null) => { }; const TableViewVirtualizedRows = ({ - panelName, + panel, data, columns, columnVisibility, @@ -747,7 +783,7 @@ const TableViewVirtualizedRows = ({ context = "", }) => { const { filters, addFilter, removeFilter } = useTableFilters( - panelName, + panel.name, context, ); const { ready: templateRenderReady, renderTemplates } = useTemplateRender(); @@ -829,7 +865,7 @@ const TableViewVirtualizedRows = ({ <>
{filterEnabled && !!filters.length && ( -
+
{filters.map((filter) => { return (
{`${filter.key}: ${filter.value}`} @@ -879,7 +915,7 @@ const TableViewVirtualizedRows = ({ colSpan={header.colSpan} scope="col" className={classNames( - "py-3 text-left text-sm font-normal tracking-wider whitespace-nowrap pl-4", + "py-3 text-left font-normal tracking-wider whitespace-nowrap pl-4", )} //style={{ width: header.getSize() }} > @@ -921,7 +957,7 @@ const TableViewVirtualizedRows = ({ {rows.length === 0 && ( No results @@ -945,7 +981,7 @@ const TableViewVirtualizedRows = ({
setShowColumnSettingsModal(false)} @@ -1010,7 +1047,7 @@ const TableViewWrapper = (props: TableProps) => { return props.data ? ( { } return (
- + {col.title} { )} > { setPanelData, setShowPanelControls, } = usePanelControls(); - const { selectFilterAndGroupPanel } = useDashboardPanelDetail(); + const { selectSidePanel } = useDashboardPanelDetail(); useEffect(() => { setCustomControls([ @@ -84,7 +84,10 @@ const Benchmark = (props: InnerCheckProps) => { key: "filter-and-group", title: "Filter & Group", component: , - action: async () => selectFilterAndGroupPanel(props.definition.name), + action: async () => + selectSidePanel({ + panel: props.definition, + }), }, ]); }, [props.definition.name, setCustomControls]); @@ -367,15 +370,23 @@ const Inner = ({ withTitle }) => { } }; -type BenchmarkProps = PanelDefinition & { +type BenchmarkProps = { + definition: PanelDefinition; + benchmarkChildren?: PanelDefinition[] | undefined; showControls: boolean; withTitle: boolean; }; const BenchmarkWrapper = (props: BenchmarkProps) => { return ( - - + + diff --git a/ui/dashboard/src/components/dashboards/grouping/DetectionBenchmark/index.tsx b/ui/dashboard/src/components/dashboards/grouping/DetectionBenchmark/index.tsx index 984d6230..533cf989 100644 --- a/ui/dashboard/src/components/dashboards/grouping/DetectionBenchmark/index.tsx +++ b/ui/dashboard/src/components/dashboards/grouping/DetectionBenchmark/index.tsx @@ -62,8 +62,7 @@ const DetectionBenchmark = (props: InnerCheckProps) => { const { download, processing } = useDownloadDetectionBenchmarkData( props.benchmark, ); - const { selectFilterAndGroupPanel, selectedPanel } = - useDashboardPanelDetail(); + const { selectSidePanel, selectedPanel } = useDashboardPanelDetail(); useEffect(() => { setCustomControls([ @@ -71,7 +70,10 @@ const DetectionBenchmark = (props: InnerCheckProps) => { key: "filter-and-group", title: "Filter & Group", component: , - action: async () => selectFilterAndGroupPanel(props.definition.name), + action: async () => + selectSidePanel({ + panel: props.definition, + }), }, { key: "download-data", @@ -395,15 +397,23 @@ const Inner = ({ showControls, withTitle }) => { } }; -type DetectionBenchmarkWrapperProps = PanelDefinition & { +type DetectionBenchmarkWrapperProps = { + definition: PanelDefinition; + benchmarkChildren?: PanelDefinition[] | undefined; showControls: boolean; withTitle: boolean; }; const DetectionBenchmarkWrapper = (props: DetectionBenchmarkWrapperProps) => { return ( - - + + diff --git a/ui/dashboard/src/components/dashboards/layout/Child/index.tsx b/ui/dashboard/src/components/dashboards/layout/Child/index.tsx index 081e6738..ec47365d 100644 --- a/ui/dashboard/src/components/dashboards/layout/Child/index.tsx +++ b/ui/dashboard/src/components/dashboards/layout/Child/index.tsx @@ -29,7 +29,8 @@ const Child = ({ const DetectionBenchmark = getComponent("detection_benchmark"); return ( ); @@ -37,7 +38,8 @@ const Child = ({ const Benchmark = getComponent("benchmark"); return ( ); @@ -46,7 +48,7 @@ const Child = ({ const Benchmark = getComponent("benchmark"); return ( ); @@ -54,7 +56,7 @@ const Child = ({ const DetectionBenchmark = getComponent("detection_benchmark"); return ( ); diff --git a/ui/dashboard/src/components/dashboards/layout/Dashboard/index.tsx b/ui/dashboard/src/components/dashboards/layout/Dashboard/index.tsx index 27272965..0b9781c5 100644 --- a/ui/dashboard/src/components/dashboards/layout/Dashboard/index.tsx +++ b/ui/dashboard/src/components/dashboards/layout/Dashboard/index.tsx @@ -1,6 +1,6 @@ import Children from "../Children"; -import DashboardControls from "./DashboardControls"; import DashboardProgress from "./DashboardProgress"; +import DashboardSidePanel from "../DashboardSidePanel"; import DashboardTitle from "@powerpipe/components/dashboards/titles/DashboardTitle"; import Grid from "../Grid"; import PanelDetail from "../PanelDetail"; @@ -37,7 +37,7 @@ const Dashboard = ({ components: { SnapshotHeader }, dataMode, } = useDashboardState(); - const { selectedFilterAndGroupPanel } = useDashboardPanelDetail(); + const { selectedSidePanel } = useDashboardPanelDetail(); const grid = ( {isRoot && !definition.artificial && ( @@ -66,7 +66,7 @@ const Dashboard = ({ ) : (
{grid}
)} - +
); diff --git a/ui/dashboard/src/components/dashboards/layout/Dashboard/DashboardControls.tsx b/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/FilterAndGroupSidePanel.tsx similarity index 63% rename from ui/dashboard/src/components/dashboards/layout/Dashboard/DashboardControls.tsx rename to ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/FilterAndGroupSidePanel.tsx index 9be74440..e7ba2eae 100644 --- a/ui/dashboard/src/components/dashboards/layout/Dashboard/DashboardControls.tsx +++ b/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/FilterAndGroupSidePanel.tsx @@ -3,22 +3,17 @@ import GroupingConfig from "@powerpipe/components/dashboards/grouping/GroupingCo import Icon from "@powerpipe/components/Icon"; import { useDashboardPanelDetail } from "@powerpipe/hooks/useDashboardPanelDetail"; -const DashboardControls = ({ panelName }: { panelName: string | null }) => { - const { closeFilterAndGroupPanel } = useDashboardPanelDetail(); - - if (!panelName) { - return null; - } - +const FilterAndGroupSidePanel = ({ panelName }: { panelName: string }) => { + const { closeSidePanel } = useDashboardPanelDetail(); return ( -
-
+ <> +

Filter & Group

@@ -29,8 +24,8 @@ const DashboardControls = ({ panelName }: { panelName: string | null }) => { Group
-
+ ); }; -export default DashboardControls; +export default FilterAndGroupSidePanel; diff --git a/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/TableRowSidePanel.tsx b/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/TableRowSidePanel.tsx new file mode 100644 index 00000000..b5a1b326 --- /dev/null +++ b/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/TableRowSidePanel.tsx @@ -0,0 +1,246 @@ +import CodeBlock from "@powerpipe/components/CodeBlock"; +import Icon from "@powerpipe/components/Icon"; +import SearchInput from "@powerpipe/components/SearchInput"; +import useCopyToClipboard from "@powerpipe/hooks/useCopyToClipboard"; +import { classNames } from "@powerpipe/utils/styles"; +import { + LeafNodeData, + LeafNodeDataColumn, +} from "@powerpipe/components/dashboards/common"; +import { parseDate } from "@powerpipe/utils/date"; +import { useDashboardPanelDetail } from "@powerpipe/hooks/useDashboardPanelDetail"; +import { useEffect, useMemo, useState } from "react"; + +const getNumericValue = (value) => { + if ( + !value || + value.NaN || + value.Exp === null || + value.Status === null || + value.InfinityModifier === null + ) { + return Number.NaN.toString(); + } + if (value.InfinityModifier === 1) { + return Number.POSITIVE_INFINITY.toString(); + } + if (value.InfinityModifier === -1) { + return Number.NEGATIVE_INFINITY.toString(); + } + + const parts: string[] = []; + if (value.Int === null) { + parts.push("0"); + } else if (value.Int !== undefined) { + parts.push(value.Int.toString()); + } else { + parts.push(value.toString()); + } + if (value.Exp !== undefined && value.Exp !== null) { + parts.push("e"); + parts.push(parseInt(value.Exp, 10).toString()); + } + return parseFloat(parts.join("")).toString(); +}; + +const renderValue = (name: string, dataType: string, value: any) => { + switch (dataType.toLowerCase()) { + case "text": + case "varchar": + return ( + + {value} + + ); + case "timestamptz": + return ( + + {parseDate(value)?.format() || ""} + + ); + case "jsonb": + case "varchar[]": + return ( + + {JSON.stringify(value, null, 2)} + + ); + case "numeric": + case "bigint": { + if (name === "timestamp") { + return ( + + {parseDate(value)?.format() || ""} + + ); + } + return ( + + {getNumericValue(value)} + + ); + } + default: + return ( + + {JSON.stringify(value, null, 2)} + + ); + } +}; + +const TableRowItem = ({ dataType, name, value }) => { + const [showCopy, setShowCopy] = useState(false); + const { copy, copySuccess } = useCopyToClipboard(); + return ( +
copy(JSON.stringify(value, null, 2)) : undefined + } + onMouseEnter={() => setShowCopy(true)} + onMouseLeave={() => setShowCopy(false)} + > +
+ + {name} + + {showCopy && ( + + )} +
+
+ {value === null && ( + null + )} + {value !== null && renderValue(name, dataType, value)} +
+
+ ); +}; + +const TableRowSidePanel = ({ + data, + requestedColumnName, + rowIndex, +}: { + data: LeafNodeData | undefined; + requestedColumnName?: string; + rowIndex: number | undefined; +}) => { + const { closeSidePanel } = useDashboardPanelDetail(); + const [search, setSearch] = useState(""); + + // if (!data || !data.columns || !data.rows || rowIndex === undefined) { + // return null; + // } + + const { columns } = data; + const row = data.rows[rowIndex]; + + const orderedRow: { column: LeafNodeDataColumn; value: any }[] = []; + for (const column of columns) { + orderedRow.push({ column, value: row[column.name] }); + } + + const filteredObj = useMemo(() => { + if (!search) { + return orderedRow; + } + + const searchParts = search.trim().toLowerCase().split(" "); + const filtered: { column: LeafNodeDataColumn; value: any }[] = []; + for (const item of orderedRow) { + const dataType = item.column.data_type.toLowerCase(); + if ( + searchParts.every((searchPart) => { + if (item.column.name.toLowerCase().indexOf(searchPart) >= 0) { + return true; + } else if (search === "null" && item.value === null) { + return true; + } else if ( + (dataType === "jsonb" || + dataType === "varchar[]" || + dataType.startsWith("struct")) && + item.value && + JSON.stringify(item.value).toLowerCase().indexOf(searchPart) >= 0 + ) { + return true; + } else if ( + item.value && + item.value.toString().toLowerCase().indexOf(searchPart) >= 0 + ) { + return true; + } else { + return false; + } + }) + ) { + filtered.push(item); + } + } + return filtered; + }, [orderedRow, search]); + + useEffect(() => { + if (!requestedColumnName) { + return; + } + + const element = document.getElementById(requestedColumnName); + if (element) { + element.scrollIntoView(); + } + }, [requestedColumnName]); + + return ( +
+
+

Row

+ +
+
+
+ +
+
+ {filteredObj.map((item) => ( + + ))} +
+
+
+ ); +}; + +export default TableRowSidePanel; diff --git a/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/index.tsx b/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/index.tsx new file mode 100644 index 00000000..9f5f3578 --- /dev/null +++ b/ui/dashboard/src/components/dashboards/layout/DashboardSidePanel/index.tsx @@ -0,0 +1,32 @@ +import FilterAndGroupSidePanel from "@powerpipe/components/dashboards/layout/DashboardSidePanel/FilterAndGroupSidePanel"; +import TableRowSidePanel from "@powerpipe/components/dashboards/layout/DashboardSidePanel/TableRowSidePanel"; +import { SidePanelInfo } from "@powerpipe/hooks/useDashboardPanelDetail"; + +const DashboardSidePanel = ({ + sidePanel, +}: { + sidePanel: SidePanelInfo | null; +}) => { + if (!sidePanel) { + return null; + } + + return ( +
+ {(sidePanel.panel.panel_type === "benchmark" || + sidePanel.panel.panel_type === "control" || + sidePanel.panel.panel_type === "detection") && ( + + )} + {sidePanel.panel.panel_type === "table" && ( + + )} +
+ ); +}; + +export default DashboardSidePanel; diff --git a/ui/dashboard/src/constants/icons.ts b/ui/dashboard/src/constants/icons.ts index 7d62d4c2..bee922d2 100644 --- a/ui/dashboard/src/constants/icons.ts +++ b/ui/dashboard/src/constants/icons.ts @@ -4,7 +4,6 @@ import { BackwardIcon as BackwardIconOutline, ChevronDownIcon as ChevronDownIconOutline, ChevronUpIcon as ChevronUpIconOutline, - ClipboardDocumentListIcon as ClipboardDocumentListIconOutline, MagnifyingGlassIcon as MagnifyingGlassIconOutline, MinusIcon as MinusIconOutline, PlusIcon as PlusIconOutline, @@ -16,7 +15,6 @@ import { CheckCircleIcon as CheckCircleIconSolid, ChevronDownIcon as ChevronDownIconSolid, ChevronUpIcon as ChevronUpIconSolid, - ClipboardDocumentCheckIcon as ClipboardDocumentCheckIconSolid, ExclamationCircleIcon as ExclamationCircleIconSolid, InformationCircleIcon as InformationCircleIconSolid, NoSymbolIcon as NoSymbolIconSolid, @@ -26,8 +24,6 @@ import { // General export const ClearIcon = XMarkIconOutline; export const CloseIcon = XMarkIconOutline; -export const CopyToClipboardIcon = ClipboardDocumentListIconOutline; -export const CopyToClipboardSuccessIcon = ClipboardDocumentCheckIconSolid; export const ErrorIcon = ExclamationCircleIconSolid; export const SearchIcon = MagnifyingGlassIconOutline; export const SubmitIcon = ArrowDownOnSquareIconOutline; diff --git a/ui/dashboard/src/hooks/useBenchmarkGrouping.tsx b/ui/dashboard/src/hooks/useBenchmarkGrouping.tsx index fb3567e5..98f4aa41 100644 --- a/ui/dashboard/src/hooks/useBenchmarkGrouping.tsx +++ b/ui/dashboard/src/hooks/useBenchmarkGrouping.tsx @@ -76,7 +76,6 @@ type ICheckGroupingContext = { }; const GroupingActions: IActions = { - COLLAPSE_ALL_NODES: "collapse_all_nodes", COLLAPSE_NODE: "collapse_node", EXPAND_ALL_NODES: "expand_all_nodes", EXPAND_NODE: "expand_node", @@ -520,19 +519,6 @@ const getCheckResultNode = (checkResult: CheckResult) => { const reducer = (state: CheckGroupNodeStates, action) => { switch (action.type) { - case GroupingActions.COLLAPSE_ALL_NODES: { - const newNodes = {}; - for (const [name, node] of Object.entries(state)) { - newNodes[name] = { - ...node, - expanded: false, - }; - } - return { - ...state, - nodes: newNodes, - }; - } case GroupingActions.COLLAPSE_NODE: return { ...state, @@ -570,6 +556,7 @@ const reducer = (state: CheckGroupNodeStates, action) => { type CheckGroupingProviderProps = { children: null | JSX.Element | JSX.Element[]; definition: PanelDefinition; + benchmarkChildren?: PanelDefinition[] | undefined; }; function recordFilterValues( @@ -763,6 +750,7 @@ const includeResult = (result: CheckResult, filterConfig: Filter): boolean => { const useGroupingInternal = ( definition: PanelDefinition | null, + benchmarkChildren: PanelDefinition[] | undefined, panelsMap: PanelsMap | undefined, groupingConfig: CheckDisplayGroup[], skip = false, @@ -786,24 +774,21 @@ const useGroupingInternal = ( } // @ts-ignore - const nestedBenchmarks = definition.children?.filter( + const nestedBenchmarks = benchmarkChildren?.filter( (child) => child.panel_type === "benchmark", ); const nestedControls = definition.panel_type === "control" ? [definition] : // @ts-ignore - definition.children?.filter( - (child) => child.panel_type === "control", - ); + benchmarkChildren?.filter((child) => child.panel_type === "control"); - const rootBenchmarkPanel = panelsMap[definition.name]; const b = new BenchmarkType( "0", - rootBenchmarkPanel.name, - rootBenchmarkPanel.title, - rootBenchmarkPanel.description, - rootBenchmarkPanel.documentation, + definition.name, + definition.title, + definition.description, + definition.documentation, nestedBenchmarks, nestedControls, panelsMap, @@ -850,7 +835,6 @@ const useGroupingInternal = ( return [ b, - { ...rootBenchmarkPanel, children: definition.children }, results, firstChildSummaries, checkNodeStates, @@ -862,6 +846,7 @@ const useGroupingInternal = ( const GroupingProvider = ({ children, definition, + benchmarkChildren, }: CheckGroupingProviderProps) => { const { panelsMap } = useDashboardState(); const { setContext: setDashboardControlsContext } = useDashboardControls(); @@ -870,12 +855,16 @@ const GroupingProvider = ({ const [ benchmark, - panelDefinition, grouping, firstChildSummaries, tempNodeStates, filterValues, - ] = useGroupingInternal(definition, panelsMap, groupingConfig); + ] = useGroupingInternal( + definition, + benchmarkChildren, + panelsMap, + groupingConfig, + ); const previousGroupings = usePrevious({ groupingConfig }); @@ -902,8 +891,7 @@ const GroupingProvider = ({ { + const [copySuccess, setCopySuccess] = useState(false); + + const handleCopy = useCallback( + (data) => { + // @ts-ignore + const copyOutput = copy(data); + if (copyOutput) { + setCopySuccess(true); + } + }, + [setCopySuccess], + ); + + useEffect(() => { + let timeoutId; + if (copySuccess) { + timeoutId = setTimeout(() => { + setCopySuccess(false); + }, 1000); + } + return () => clearTimeout(timeoutId); + }, [copySuccess]); + + return { copy: handleCopy, copySuccess }; +}; + +export default useCopyToClipboard; diff --git a/ui/dashboard/src/hooks/useDashboardPanelDetail.tsx b/ui/dashboard/src/hooks/useDashboardPanelDetail.tsx index 54264357..bd9bcf69 100644 --- a/ui/dashboard/src/hooks/useDashboardPanelDetail.tsx +++ b/ui/dashboard/src/hooks/useDashboardPanelDetail.tsx @@ -11,14 +11,19 @@ import { LeafNodeData } from "@powerpipe/components/dashboards/common"; import { noop } from "@powerpipe/utils/func"; import { PanelDefinition } from "@powerpipe/types"; +export interface SidePanelInfo { + panel: PanelDefinition; + context?: any; +} + interface IDashboardPanelDetailContext { selectedPanel: PanelDefinition | null; selectPanel: (panelName: PanelDefinition | null, data?: LeafNodeData) => void; closePanel: () => void; panelOverrideData: LeafNodeData | null; - selectedFilterAndGroupPanel: string | null; - selectFilterAndGroupPanel: (panelName: string | null) => void; - closeFilterAndGroupPanel: () => void; + selectedSidePanel: SidePanelInfo | null; + selectSidePanel: (SidePanelInfo: SidePanelInfo | null) => void; + closeSidePanel: () => void; } interface DashboardPanelDetailProviderProps { @@ -34,8 +39,8 @@ export const DashboardPanelDetailProvider = ({ const [selectedPanel, setSelectedPanel] = useState( null, ); - const [selectedFilterAndGroupPanel, setSelectedFilterAndGroupPanel] = - useState(null); + const [selectedSidePanel, setSelectedSidePanel] = + useState(null); const [panelOverrideData, setPanelOverrideData] = useState(null); @@ -74,10 +79,10 @@ export const DashboardPanelDetailProvider = ({ selectPanel, closePanel, panelOverrideData, - selectedFilterAndGroupPanel, - selectFilterAndGroupPanel: (panelName: string | null) => - setSelectedFilterAndGroupPanel(panelName), - closeFilterAndGroupPanel: () => setSelectedFilterAndGroupPanel(null), + selectedSidePanel, + selectSidePanel: (sidePanelInfo: SidePanelInfo | null) => + setSelectedSidePanel(sidePanelInfo), + closeSidePanel: () => setSelectedSidePanel(null), }} > { const reducer = (state: CheckGroupNodeStates, action) => { switch (action.type) { - case GroupingActions.COLLAPSE_ALL_NODES: { - const newNodes = {}; - for (const [name, node] of Object.entries(state)) { - newNodes[name] = { - ...node, - expanded: false, - }; - } - return { - ...state, - nodes: newNodes, - }; - } case GroupingActions.COLLAPSE_NODE: return { ...state, @@ -488,6 +475,7 @@ const reducer = (state: CheckGroupNodeStates, action) => { type DetectionGroupingProviderProps = { children: null | JSX.Element | JSX.Element[]; definition: PanelDefinition; + benchmarkChildren: PanelDefinition[] | undefined; }; function recordFilterValues( @@ -586,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); @@ -661,11 +668,12 @@ const includeResult = ( const useGroupingInternal = ( definition: PanelDefinition | null, + benchmarkChildren: PanelDefinition[] | undefined, panelsMap: PanelsMap | undefined, groupingConfig: DetectionDisplayGroup[], skip = false, ) => { - const { filter: checkFilterConfig } = useFilterConfig(definition?.name); + const { allFilters } = useFilterConfig(definition?.name); return useMemo(() => { const filterValues = { @@ -681,24 +689,23 @@ const useGroupingInternal = ( } // @ts-ignore - const nestedDetectionBenchmarks = definition.children?.filter( + const nestedDetectionBenchmarks = benchmarkChildren?.filter( (child) => child.panel_type === "benchmark", ); const nestedDetections = definition.panel_type === "detection" ? [definition] : // @ts-ignore - definition.children?.filter( + benchmarkChildren?.filter( (child) => child.panel_type === "detection", ); - const rootBenchmarkPanel = panelsMap[definition.name]; const b = new DetectionBenchmarkType( "0", - rootBenchmarkPanel.name, - rootBenchmarkPanel.title, - rootBenchmarkPanel.description, - rootBenchmarkPanel.documentation, + definition.name, + definition.title, + definition.description, + definition.documentation, nestedDetectionBenchmarks, nestedDetections, panelsMap, @@ -716,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; } @@ -754,19 +761,19 @@ const useGroupingInternal = ( return [ b, - { ...rootBenchmarkPanel, children: definition.children }, results, firstChildSummaries, hasSeverityResults, detectionNodeStates, filterValues, ] as const; - }, [checkFilterConfig, definition, groupingConfig, panelsMap, skip]); + }, [allFilters, definition, groupingConfig, panelsMap, skip]); }; const GroupingProvider = ({ children, definition, + benchmarkChildren, }: DetectionGroupingProviderProps) => { const { panelsMap } = useDashboardState(); const { setContext: setDashboardControlsContext } = useDashboardControls(); @@ -775,21 +782,26 @@ const GroupingProvider = ({ const [ benchmark, - panelDefinition, grouping, firstChildSummaries, hasSeverityResults, tempNodeStates, filterValues, - ] = useGroupingInternal(definition, panelsMap, groupingConfig); + ] = useGroupingInternal( + definition, + benchmarkChildren, + panelsMap, + groupingConfig, + ); const previousGroupings = usePrevious({ groupingConfig }); useEffect(() => { if ( - previousGroupings && - JSON.stringify(previousGroupings.groupingConfig) === - JSON.stringify(groupingConfig) + !previousGroupings || + (!!previousGroupings && + JSON.stringify(previousGroupings.groupingConfig) === + JSON.stringify(groupingConfig)) ) { return; } @@ -807,8 +819,7 @@ const GroupingProvider = ({