From 3ae5ae8b2c6ecf9299afe394c8c41a181082b047 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Thu, 11 Dec 2025 17:17:16 +0545 Subject: [PATCH 1/4] feat: Playbooks Panel --- .../audit-report/components/View/View.tsx | 7 +- .../components/View/panels/PlaybooksPanel.tsx | 288 ++++++++++++++++++ .../components/View/panels/index.ts | 1 + src/pages/audit-report/types/index.ts | 3 +- src/utils/mixins.module.css | 12 + 5 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx 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/PlaybooksPanel.tsx b/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx new file mode 100644 index 000000000..462816e84 --- /dev/null +++ b/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx @@ -0,0 +1,288 @@ +import { ChangeEvent, useMemo, useState, useRef, useEffect } from "react"; +import { Search, X, Play, 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 { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle +} from "@flanksource-ui/components/ui/card"; +import { Input } from "@flanksource-ui/components/ui/input"; +import { Button } from "@flanksource-ui/components/ui/button"; +import { Badge } from "@flanksource-ui/components/ui/badge"; +import { Separator } from "@flanksource-ui/components/ui/separator"; + +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 as PlaybookRunRow[]) || [], + [summary.rows] + ); + const [selected, setSelected] = useState(null); + const [search, setSearch] = useState(""); + const [searchExpanded, setSearchExpanded] = useState(false); + const [showScrollIndicator, setShowScrollIndicator] = useState(false); + const scrollContainerRef = useRef(null); + const searchInputRef = useRef(null); + + const filteredRows = useMemo(() => { + const term = search.trim().toLowerCase(); + if (!term) return rows; + return rows.filter((row) => { + const title = row.title || row.name || ""; + return ( + title.toLowerCase().includes(term) || + (row.name ?? "").toLowerCase().includes(term) || + (row.description ?? "").toLowerCase().includes(term) + ); + }); + }, [rows, search]); + + const { data: playbookSpec, isLoading } = useGetPlaybookSpecsDetails( + selected?.id ?? "", + { + enabled: !!selected?.id + } + ); + + const handleCloseModal = () => { + setSelected(null); + }; + + const handleClearSearch = () => { + setSearch(""); + setSearchExpanded(false); + }; + + const handleToggleSearch = () => { + setSearchExpanded(!searchExpanded); + }; + + // Focus input when search is expanded + useEffect(() => { + if (searchExpanded && searchInputRef.current) { + searchInputRef.current.focus(); + } + }, [searchExpanded]); + + // 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); + }, [filteredRows]); // Re-check when filtered rows change + + return ( + + +
+ {/* Title and Badge - always visible */} +
+ {summary.name} + + {filteredRows?.length ?? 0} + {rows && filteredRows && rows.length !== filteredRows.length + ? `/${rows.length}` + : ""} + +
+ + {/* Spacer to push search to the right */} +
+ + {/* Search Input - expands to the left when active */} +
+ ) => + setSearch(e.target.value) + } + placeholder="Search playbooks..." + className="h-9 pr-9 focus-visible:ring-0 focus-visible:ring-offset-0" + /> + {search && ( + + )} +
+ + {/* Search Toggle Button - always on far right */} + +
+ {summary.description && !searchExpanded && ( + {summary.description} + )} +
+ + + + + {/* Playbook List */} +
+
+ {(!rows || rows.length === 0) && ( + + )} + + {rows && rows.length > 0 && filteredRows.length === 0 && ( +
+ +

+ No playbooks found +

+

+ Try adjusting your search term +

+
+ )} + + {filteredRows && filteredRows.length > 0 && ( +
+ {filteredRows.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 && ( + + )} +
+ ); +} 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; +} From 4baa4efe365f4eedf37dcdc1abe58a50163082c6 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Thu, 11 Dec 2025 17:18:37 +0545 Subject: [PATCH 2/4] PanelWrapper --- .../components/View/panels/BarGaugePanel.tsx | 10 ++--- .../components/View/panels/DurationPanel.tsx | 17 +++----- .../components/View/panels/NumberPanel.tsx | 17 +++----- .../components/View/panels/PanelWrapper.tsx | 39 +++++++++++++++++++ .../components/View/panels/PieChartPanel.tsx | 13 ++++--- .../components/View/panels/PlaybooksPanel.tsx | 30 ++++++-------- .../components/View/panels/TablePanel.tsx | 15 ++++--- .../components/View/panels/TextPanel.tsx | 17 +++----- 8 files changed, 86 insertions(+), 72 deletions(-) create mode 100644 src/pages/audit-report/components/View/panels/PanelWrapper.tsx 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 index 462816e84..9d2e23d8d 100644 --- a/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx +++ b/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx @@ -7,17 +7,9 @@ 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 { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle -} from "@flanksource-ui/components/ui/card"; import { Input } from "@flanksource-ui/components/ui/input"; import { Button } from "@flanksource-ui/components/ui/button"; import { Badge } from "@flanksource-ui/components/ui/badge"; -import { Separator } from "@flanksource-ui/components/ui/separator"; type PlaybookRunRow = { id: string; @@ -115,12 +107,15 @@ export default function PlaybooksPanel({ summary }: PlaybookRunPanelProps) { }, [filteredRows]); // Re-check when filtered rows change return ( - - +
+ {/* Header */} +
{/* Title and Badge - always visible */}
- {summary.name} +

+ {summary.name} +

{filteredRows?.length ?? 0} {rows && filteredRows && rows.length !== filteredRows.length @@ -171,13 +166,12 @@ export default function PlaybooksPanel({ summary }: PlaybookRunPanelProps) {
{summary.description && !searchExpanded && ( - {summary.description} +

{summary.description}

)} - +
- - - + {/* Content */} +
{/* Playbook List */}
)}
- +
{playbookSpec && selected && ( )} - +
); } 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}
-
+ ); })}
From a0334055186d66676baf7abaf8b59d398150c22f Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Mon, 8 Dec 2025 22:12:15 +0545 Subject: [PATCH 3/4] Remove search --- .../components/View/panels/PlaybooksPanel.tsx | 126 ++---------------- 1 file changed, 9 insertions(+), 117 deletions(-) diff --git a/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx b/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx index 9d2e23d8d..6379a5e23 100644 --- a/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx +++ b/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx @@ -1,5 +1,5 @@ -import { ChangeEvent, useMemo, useState, useRef, useEffect } from "react"; -import { Search, X, Play, Loader2, ChevronDown, Workflow } from "lucide-react"; +import { useMemo, useState, useRef, useEffect } from "react"; +import { Play, 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"; @@ -7,9 +7,8 @@ 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 { Input } from "@flanksource-ui/components/ui/input"; import { Button } from "@flanksource-ui/components/ui/button"; -import { Badge } from "@flanksource-ui/components/ui/badge"; +import PanelWrapper from "./PanelWrapper"; type PlaybookRunRow = { id: string; @@ -37,24 +36,8 @@ export default function PlaybooksPanel({ summary }: PlaybookRunPanelProps) { [summary.rows] ); const [selected, setSelected] = useState(null); - const [search, setSearch] = useState(""); - const [searchExpanded, setSearchExpanded] = useState(false); const [showScrollIndicator, setShowScrollIndicator] = useState(false); const scrollContainerRef = useRef(null); - const searchInputRef = useRef(null); - - const filteredRows = useMemo(() => { - const term = search.trim().toLowerCase(); - if (!term) return rows; - return rows.filter((row) => { - const title = row.title || row.name || ""; - return ( - title.toLowerCase().includes(term) || - (row.name ?? "").toLowerCase().includes(term) || - (row.description ?? "").toLowerCase().includes(term) - ); - }); - }, [rows, search]); const { data: playbookSpec, isLoading } = useGetPlaybookSpecsDetails( selected?.id ?? "", @@ -67,22 +50,6 @@ export default function PlaybooksPanel({ summary }: PlaybookRunPanelProps) { setSelected(null); }; - const handleClearSearch = () => { - setSearch(""); - setSearchExpanded(false); - }; - - const handleToggleSearch = () => { - setSearchExpanded(!searchExpanded); - }; - - // Focus input when search is expanded - useEffect(() => { - if (searchExpanded && searchInputRef.current) { - searchInputRef.current.focus(); - } - }, [searchExpanded]); - // Check scroll position to show/hide indicator const checkScrollPosition = () => { const container = scrollContainerRef.current; @@ -104,74 +71,11 @@ export default function PlaybooksPanel({ summary }: PlaybookRunPanelProps) { container.addEventListener("scroll", checkScrollPosition); return () => container.removeEventListener("scroll", checkScrollPosition); - }, [filteredRows]); // Re-check when filtered rows change + }, [rows]); // Re-check when rows change return ( -
- {/* Header */} -
-
- {/* Title and Badge - always visible */} -
-

- {summary.name} -

- - {filteredRows?.length ?? 0} - {rows && filteredRows && rows.length !== filteredRows.length - ? `/${rows.length}` - : ""} - -
- - {/* Spacer to push search to the right */} -
- - {/* Search Input - expands to the left when active */} -
- ) => - setSearch(e.target.value) - } - placeholder="Search playbooks..." - className="h-9 pr-9 focus-visible:ring-0 focus-visible:ring-offset-0" - /> - {search && ( - - )} -
- - {/* Search Toggle Button - always on far right */} - -
- {summary.description && !searchExpanded && ( -

{summary.description}

- )} -
- - {/* Content */} -
+ +
{/* Playbook List */}
)} - {rows && rows.length > 0 && filteredRows.length === 0 && ( -
- -

- No playbooks found -

-

- Try adjusting your search term -

-
- )} - - {filteredRows && filteredRows.length > 0 && ( + {rows && rows.length > 0 && (
- {filteredRows.map((row) => { + {rows.map((row) => { const title = row.title || row.name || "Playbook"; const isCurrentlyLoading = isLoading && selected?.id === row.id; @@ -277,6 +169,6 @@ export default function PlaybooksPanel({ summary }: PlaybookRunPanelProps) { params={selected.params} /> )} -
+ ); } From 11104fd2074b37808671dcaa8daef1e85cd7c5a8 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Mon, 8 Dec 2025 22:19:46 +0545 Subject: [PATCH 4/4] update btn --- .../components/View/panels/PlaybooksPanel.tsx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx b/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx index 6379a5e23..482047481 100644 --- a/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx +++ b/src/pages/audit-report/components/View/panels/PlaybooksPanel.tsx @@ -1,5 +1,5 @@ import { useMemo, useState, useRef, useEffect } from "react"; -import { Play, Loader2, ChevronDown, Workflow } from "lucide-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"; @@ -32,7 +32,14 @@ type PlaybookRunPanelProps = { */ export default function PlaybooksPanel({ summary }: PlaybookRunPanelProps) { const rows = useMemo( - () => (summary.rows as PlaybookRunRow[]) || [], + () => + (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); @@ -123,20 +130,18 @@ export default function PlaybooksPanel({ summary }: PlaybookRunPanelProps) {
@@ -158,7 +163,7 @@ export default function PlaybooksPanel({ summary }: PlaybookRunPanelProps) {
- {playbookSpec && selected && ( + {playbookSpec && selected && playbookSpec.id === selected.id && (