diff --git a/src/api/services/playbooks.ts b/src/api/services/playbooks.ts index f7eebb2ca3..f43a210133 100644 --- a/src/api/services/playbooks.ts +++ b/src/api/services/playbooks.ts @@ -1,3 +1,4 @@ +import { PlaybookRunAction } from "../../components/Playbooks/Runs/PlaybookRunsSidePanel"; import { SubmitPlaybookRunFormValues } from "../../components/Playbooks/Runs/SubmitPlaybookRunForm"; import { NewPlaybookSpec, @@ -5,8 +6,9 @@ import { UpdatePlaybookSpec } from "../../components/Playbooks/Settings/PlaybookSpecsTable"; import { AVATAR_INFO } from "../../constants"; -import { IncidentCommander, PlaybookAPI } from "../axios"; +import { ConfigDB, IncidentCommander, PlaybookAPI } from "../axios"; import { GetPlaybooksToRunParams } from "../query-hooks/playbooks"; +import { resolve } from "../resolve"; export async function getAllPlaybooksSpecs() { const res = await IncidentCommander.get( @@ -62,3 +64,22 @@ export async function getPlaybookRun(params: GetPlaybooksToRunParams) { ); return res.data ?? []; } + +export async function getPlaybookRuns(componentId?: string, configId?: string) { + const componentParamString = componentId + ? `&component_id=eq.${componentId}` + : ""; + const configParamString = configId ? `&config_id=eq.${configId}` : ""; + + const res = await resolve( + ConfigDB.get( + `/playbook_runs?select=*,playbooks(id,name)&order=created_at.desc${componentParamString}&${configParamString}`, + { + headers: { + Prefer: "count=exact" + } + } + ) + ); + return res; +} diff --git a/src/components/ConfigSidebar/index.tsx b/src/components/ConfigSidebar/index.tsx index b3bb055e8c..05e9f15e82 100644 --- a/src/components/ConfigSidebar/index.tsx +++ b/src/components/ConfigSidebar/index.tsx @@ -8,6 +8,7 @@ import SlidingSideBar from "../SlidingSideBar"; import { ConfigDetails } from "./ConfigDetails"; import ConfigActionBar from "./ConfigActionBar"; import { useCallback, useState } from "react"; +import { PlaybookRunsSidePanel } from "../Playbooks/Runs/PlaybookRunsSidePanel"; type SidePanels = | "ConfigDetails" @@ -15,7 +16,8 @@ type SidePanels = | "Incidents" | "Costs" | "ConfigChanges" - | "Insights"; + | "Insights" + | "PlaybookRuns"; export default function ConfigSidebar() { const [openedPanel, setOpenedPanel] = useState( @@ -70,6 +72,14 @@ export default function ConfigSidebar() { panelCollapsedStatusChange(status, "Costs") } /> + + panelCollapsedStatusChange(status, "PlaybookRuns") + } + /> void; +}; + +export type PlaybookRunStatus = + | "scheduled" + | "running" + | "cancelled" + | "completed" + | "failed" + | "pending"; + +export type PlaybookRunAction = { + id: string; + name: string; + status: PlaybookRunStatus; + playbook_run_id: string; + start_time?: string; + end_time?: string; + result?: { + stdout?: string; + }; + error?: string; + playbooks?: PlaybookSpec; +}; + +const runsColumns: ColumnDef[] = [ + { + header: "Name", + id: "name", + accessorKey: "name", + size: 60, + cell: ({ row }) => { + const name = row.original.name ?? row.original.playbooks?.name; + return {name}; + } + }, + { + header: "Status", + id: "status", + accessorKey: "status", + size: 60 + }, + { + header: "Duration", + id: "duration", + cell: ({ row }) => { + const { start_time, end_time } = row.original; + const value = relativeDateTime(end_time!, start_time); + return {value}; + }, + size: 20 + } +]; + +export function PlaybookRunsSidePanel({ + isCollapsed, + onCollapsedStateChange, + ...props +}: ConfigSidePanelProps | TopologySidePanelProps) { + const { data, isLoading, refetch, isFetching } = useQuery( + ["componentTeams", props], + () => + getPlaybookRuns( + props.panelType === "topology" ? props.componentId : undefined, + props.panelType === "config" ? props.configId : undefined + ) + ); + + const totalEntries = data?.totalEntries ?? 0; + + const playbookRuns = data?.data ?? []; + + const [{ pageIndex, pageSize }, setPageState] = useState({ + pageIndex: 0, + pageSize: 50 + }); + + const canGoNext = () => { + const pageCount = Math.ceil(totalEntries / pageSize); + return pageCount >= pageIndex + 1; + }; + + const [triggerRefresh] = useAtom(refreshButtonClickedTrigger); + + const columns = useMemo(() => runsColumns, []); + + useEffect(() => { + if (!isLoading && !isFetching) { + refetch(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [triggerRefresh]); + + return ( + + } + /> + <PillBadge>{playbookRuns?.length ?? 0}</PillBadge> + </div> + } + dataCount={playbookRuns?.length} + > + <div className="flex flex-col space-y-4 py-2 w-full"> + {isLoading ? ( + <TextSkeletonLoader /> + ) : playbookRuns && playbookRuns.length > 0 ? ( + <div className="flex flex-col overflow-y-hidden"> + <InfiniteTable + isLoading={isLoading} + allRows={playbookRuns} + columns={columns} + isFetching={isFetching} + loaderView={<TextSkeletonLoader className="w-full my-2" />} + totalEntries={totalEntries} + fetchNextPage={() => { + if (canGoNext()) { + setPageState({ + pageIndex: pageIndex + 1, + pageSize + }); + } + }} + stickyHead + virtualizedRowEstimatedHeight={40} + columnsClassName={{ + name: "", + status: "fit-content", + duration: "fit-content" + }} + /> + </div> + ) : ( + <EmptyState /> + )} + </div> + </CollapsiblePanel> + ); +} diff --git a/src/components/TopologySidebar/TopologySidebar.tsx b/src/components/TopologySidebar/TopologySidebar.tsx index c6cbd8ab70..473539d326 100644 --- a/src/components/TopologySidebar/TopologySidebar.tsx +++ b/src/components/TopologySidebar/TopologySidebar.tsx @@ -10,6 +10,7 @@ import TopologyActionBar from "./TopologyActionBar"; import TopologyCost from "./TopologyCost"; import TopologyInsights from "./TopologyInsights"; import { useCallback, useState } from "react"; +import { PlaybookRunsSidePanel } from "../Playbooks/Runs/PlaybookRunsSidePanel"; type Props = { topology?: Topology; @@ -24,7 +25,8 @@ type SidePanels = | "Costs" | "ConfigChanges" | "Teams" - | "Insights"; + | "Insights" + | "PlaybookRuns"; export default function TopologySidebar({ topology, @@ -77,6 +79,7 @@ export default function TopologySidebar({ panelCollapsedStatusChange(status, "Incidents") } /> + <TopologyCost topology={topology} isCollapsed={openedPanel !== "Costs"} @@ -91,6 +94,16 @@ export default function TopologySidebar({ panelCollapsedStatusChange(status, "ConfigChanges") } /> + + <PlaybookRunsSidePanel + panelType="topology" + componentId={id} + isCollapsed={openedPanel !== "PlaybookRuns"} + onCollapsedStateChange={(status) => + panelCollapsedStatusChange(status, "PlaybookRuns") + } + /> + <ComponentTeams componentId={id} isCollapsed={openedPanel !== "Teams"}