diff --git a/src/pages/audit-report/components/View/View.tsx b/src/pages/audit-report/components/View/View.tsx index 3576e5084..e34927411 100644 --- a/src/pages/audit-report/components/View/View.tsx +++ b/src/pages/audit-report/components/View/View.tsx @@ -25,7 +25,8 @@ import { TextPanel, DurationPanel, BarGaugePanel, - PropertiesPanel + PropertiesPanel, + PlaybooksPanel } from "./panels"; import GlobalFilters from "./GlobalFilters"; import GlobalFiltersForm from "./GlobalFiltersForm"; @@ -232,7 +233,7 @@ const View: React.FC = ({
{title !== "" && (

- + {title}

)} @@ -427,6 +428,8 @@ const renderPanel = (panel: PanelResult, index: number) => { return ; case "properties": return ; + case "playbooks": + return ; default: return null; } diff --git a/src/pages/audit-report/components/View/panels/BarGaugePanel.tsx b/src/pages/audit-report/components/View/panels/BarGaugePanel.tsx index 9779e0a29..669db343b 100644 --- a/src/pages/audit-report/components/View/panels/BarGaugePanel.tsx +++ b/src/pages/audit-report/components/View/panels/BarGaugePanel.tsx @@ -1,6 +1,7 @@ import React from "react"; import { PanelResult, BarGaugeConfig } from "../../../types"; import { getGaugeColor, formatDisplayValue } from "./utils"; +import PanelWrapper from "./PanelWrapper"; interface BarGaugePanelProps { summary: PanelResult; @@ -60,12 +61,7 @@ const BarGaugePanel: React.FC = ({ summary }) => { }; return ( -
-

{summary.name}

- {summary.description && ( -

{summary.description}

- )} - +
{summary.rows.map((row, rowIndex) => { const labelValue = @@ -127,7 +123,7 @@ const BarGaugePanel: React.FC = ({ summary }) => { ); })}
-
+ ); }; diff --git a/src/pages/audit-report/components/View/panels/DurationPanel.tsx b/src/pages/audit-report/components/View/panels/DurationPanel.tsx index 4f84225ef..0bf51f052 100644 --- a/src/pages/audit-report/components/View/panels/DurationPanel.tsx +++ b/src/pages/audit-report/components/View/panels/DurationPanel.tsx @@ -1,6 +1,7 @@ import React from "react"; import { formatDuration } from "@flanksource-ui/utils/date"; import { PanelResult } from "../../../types"; +import PanelWrapper from "./PanelWrapper"; interface DurationPanelProps { summary: PanelResult; @@ -22,24 +23,18 @@ const DurationPanel: React.FC = ({ summary }) => { const formattedDuration = formatDuration(milliseconds); return ( -
-

- {label || summary.name} -

- {summary.description && ( -

- {summary.description} -

- )}

{formattedDuration}

-
+ ); })} diff --git a/src/pages/audit-report/components/View/panels/NumberPanel.tsx b/src/pages/audit-report/components/View/panels/NumberPanel.tsx index eaaecba87..c900b810b 100644 --- a/src/pages/audit-report/components/View/panels/NumberPanel.tsx +++ b/src/pages/audit-report/components/View/panels/NumberPanel.tsx @@ -1,6 +1,7 @@ import React from "react"; import { PanelResult } from "../../../types"; import { formatDisplayValue } from "./utils"; +import PanelWrapper from "./PanelWrapper"; interface NumberPanelProps { summary: PanelResult; @@ -19,18 +20,12 @@ const NumberPanel: React.FC = ({ summary }) => { const numericValue = Number(displayValue); return ( -
-

- {label || summary.name} -

- {summary.description && ( -

- {summary.description} -

- )}

{summary.number @@ -42,7 +37,7 @@ const NumberPanel: React.FC = ({ summary }) => { : displayValue}

-
+ ); })} diff --git a/src/pages/audit-report/components/View/panels/PanelWrapper.tsx b/src/pages/audit-report/components/View/panels/PanelWrapper.tsx new file mode 100644 index 000000000..d670d58fe --- /dev/null +++ b/src/pages/audit-report/components/View/panels/PanelWrapper.tsx @@ -0,0 +1,39 @@ +import React, { ReactNode } from "react"; + +interface PanelWrapperProps { + title: string; + description?: string; + className?: string; + titleClassName?: string; + children: ReactNode; +} + +/** + * Common wrapper component for all panel types. + * Provides consistent styling for the panel container, header, and description. + */ +const PanelWrapper: React.FC = ({ + title, + description, + className = "", + titleClassName = "", + children +}) => { + return ( +
+

+ {title} +

+ {description && ( +

{description}

+ )} + {children} +
+ ); +}; + +export default PanelWrapper; diff --git a/src/pages/audit-report/components/View/panels/PieChartPanel.tsx b/src/pages/audit-report/components/View/panels/PieChartPanel.tsx index f1f4caedf..52c27121e 100644 --- a/src/pages/audit-report/components/View/panels/PieChartPanel.tsx +++ b/src/pages/audit-report/components/View/panels/PieChartPanel.tsx @@ -9,6 +9,7 @@ import { } from "recharts"; import { PanelResult } from "../../../types"; import { COLOR_PALETTE, getSeverityOfText, severityToHex } from "./utils"; +import PanelWrapper from "./PanelWrapper"; interface PieChartPanelProps { summary: PanelResult; @@ -98,11 +99,11 @@ const PieChartPanel: React.FC = ({ summary }) => { ); return ( -
-

{summary.name}

- {summary.description && ( -

{summary.description}

- )} +
@@ -126,7 +127,7 @@ const PieChartPanel: React.FC = ({ summary }) => {
-
+ ); }; diff --git a/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx b/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx new file mode 100644 index 000000000..482047481 --- /dev/null +++ b/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx @@ -0,0 +1,179 @@ +import { useMemo, useState, useRef, useEffect } from "react"; +import { Loader2, ChevronDown, Workflow } from "lucide-react"; +import { useGetPlaybookSpecsDetails } from "@flanksource-ui/api/query-hooks/playbooks"; +import { SubmitPlaybookRunFormValues } from "@flanksource-ui/components/Playbooks/Runs/Submit/SubmitPlaybookRunForm"; +import SubmitPlaybookRunForm from "@flanksource-ui/components/Playbooks/Runs/Submit/SubmitPlaybookRunForm"; +import { Icon } from "@flanksource-ui/ui/Icons/Icon"; +import EmptyState from "@flanksource-ui/components/EmptyState"; +import { PanelResult } from "../../../types"; +import mixins from "@flanksource-ui/utils/mixins.module.css"; +import { Button } from "@flanksource-ui/components/ui/button"; +import PanelWrapper from "./PanelWrapper"; + +type PlaybookRunRow = { + id: string; + title?: string; + name?: string; + description?: string; + icon?: string; + component_id?: string; + config_id?: string; + check_id?: string; + params?: SubmitPlaybookRunFormValues["params"]; +}; + +type PlaybookRunPanelProps = { + summary: PanelResult; +}; + +/** + * A panel that lists playbooks with inline Run buttons and opens the existing + * SubmitPlaybookRunForm modal when a playbook is selected. + */ +export default function PlaybooksPanel({ summary }: PlaybookRunPanelProps) { + const rows = useMemo( + () => + (summary.rows || []).filter( + (row): row is PlaybookRunRow => + typeof row === "object" && + row !== null && + typeof row.id === "string" && + row.id.length > 0 + ), + [summary.rows] + ); + const [selected, setSelected] = useState(null); + const [showScrollIndicator, setShowScrollIndicator] = useState(false); + const scrollContainerRef = useRef(null); + + const { data: playbookSpec, isLoading } = useGetPlaybookSpecsDetails( + selected?.id ?? "", + { + enabled: !!selected?.id + } + ); + + const handleCloseModal = () => { + setSelected(null); + }; + + // Check scroll position to show/hide indicator + const checkScrollPosition = () => { + const container = scrollContainerRef.current; + if (!container) return; + + const { scrollTop, scrollHeight, clientHeight } = container; + const isScrollable = scrollHeight > clientHeight; + const isAtBottom = scrollHeight - scrollTop - clientHeight < 10; // 10px threshold + + setShowScrollIndicator(isScrollable && !isAtBottom); + }; + + // Set up scroll listener and check initial state + useEffect(() => { + const container = scrollContainerRef.current; + if (!container) return; + + checkScrollPosition(); + + container.addEventListener("scroll", checkScrollPosition); + return () => container.removeEventListener("scroll", checkScrollPosition); + }, [rows]); // Re-check when rows change + + return ( + +
+ {/* Playbook List */} +
+
+ {(!rows || rows.length === 0) && ( + + )} + + {rows && rows.length > 0 && ( +
+ {rows.map((row) => { + const title = row.title || row.name || "Playbook"; + const isCurrentlyLoading = + isLoading && selected?.id === row.id; + + return ( +
+
+
+
+ {row.icon ? ( + + ) : ( + + )} +
+
+

+ {title} +

+ {row.description && ( +

+ {row.description} +

+ )} +
+
+ +
+
+ ); + })} +
+ )} +
+ + {/* Scroll Indicator */} + {showScrollIndicator && ( +
+
+ +
+
+ )} +
+
+ + {playbookSpec && selected && playbookSpec.id === selected.id && ( + + )} +
+ ); +} diff --git a/src/pages/audit-report/components/View/panels/TablePanel.tsx b/src/pages/audit-report/components/View/panels/TablePanel.tsx index 257a04322..5f751563d 100644 --- a/src/pages/audit-report/components/View/panels/TablePanel.tsx +++ b/src/pages/audit-report/components/View/panels/TablePanel.tsx @@ -1,5 +1,6 @@ import React from "react"; import { PanelResult } from "../../../types"; +import PanelWrapper from "./PanelWrapper"; interface TablePanelProps { summary: PanelResult; @@ -7,13 +8,11 @@ interface TablePanelProps { const TablePanel: React.FC = ({ summary }) => { return ( -
-

- {summary.name} -

- {summary.description && ( -

{summary.description}

- )} +
{summary.rows?.map((row, rowIndex) => { return ( @@ -32,7 +31,7 @@ const TablePanel: React.FC = ({ summary }) => { ); })}
-
+ ); }; diff --git a/src/pages/audit-report/components/View/panels/TextPanel.tsx b/src/pages/audit-report/components/View/panels/TextPanel.tsx index 02661aa4e..33e465d32 100644 --- a/src/pages/audit-report/components/View/panels/TextPanel.tsx +++ b/src/pages/audit-report/components/View/panels/TextPanel.tsx @@ -1,5 +1,6 @@ import React from "react"; import { PanelResult } from "../../../types"; +import PanelWrapper from "./PanelWrapper"; interface TextPanelProps { summary: PanelResult; @@ -12,22 +13,16 @@ const TextPanel: React.FC = ({ summary }) => { const { value } = row; return ( -
-

- {summary.name} -

- {summary.description && ( -

- {summary.description} -

- )}
{value}
-
+ ); })}
diff --git a/src/pages/audit-report/components/View/panels/index.ts b/src/pages/audit-report/components/View/panels/index.ts index 0ea064551..1eecc5b2f 100644 --- a/src/pages/audit-report/components/View/panels/index.ts +++ b/src/pages/audit-report/components/View/panels/index.ts @@ -6,3 +6,4 @@ export { default as BarGaugePanel } from "./BarGaugePanel"; export { default as TextPanel } from "./TextPanel"; export { default as DurationPanel } from "./DurationPanel"; export { default as PropertiesPanel } from "./PropertiesPanel"; +export { default as PlaybooksPanel } from "./PlaybooksPanel"; diff --git a/src/pages/audit-report/types/index.ts b/src/pages/audit-report/types/index.ts index 24720f23a..bdb817f9d 100644 --- a/src/pages/audit-report/types/index.ts +++ b/src/pages/audit-report/types/index.ts @@ -485,7 +485,8 @@ export type PanelResult = { | "gauge" | "duration" | "bargauge" - | "properties"; + | "properties" + | "playbooks"; description?: string; rows?: Record[]; gauge?: GaugeConfig; diff --git a/src/utils/mixins.module.css b/src/utils/mixins.module.css index eb2582f74..007899f2e 100644 --- a/src/utils/mixins.module.css +++ b/src/utils/mixins.module.css @@ -18,3 +18,15 @@ .appleScrollbar::-webkit-scrollbar-thumb:hover { background-color: rgba(0, 0, 0, 0.4); } + +.hoverScrollbar { + overflow-y: auto; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE/Edge */ +} + +.hoverScrollbar::-webkit-scrollbar { + display: none; /* Safari/Chrome */ + width: 0; + height: 0; +}