diff --git a/src/api/services/playbooks.ts b/src/api/services/playbooks.ts index c4ff791e2..aa815cb49 100644 --- a/src/api/services/playbooks.ts +++ b/src/api/services/playbooks.ts @@ -92,13 +92,32 @@ export async function getPlaybookToRunForResource( } export async function getPlaybookRun(id: string) { + const select = [ + "*", + `created_by(${AVATAR_INFO})`, + "playbooks(id,name,title,spec)", + "component:components(id,name,icon)", + "config:config_items(id,name,type,config_class)", + "check:checks(id,name,icon)", + `playbook_approvals(*, person_id(${AVATAR_INFO}), team_id(*))` + ].join(","); + const res = await IncidentCommander.get( // todo: use playbook names instead - `/playbook_runs?id=eq.${id}&select=*,created_by(${AVATAR_INFO}),playbooks(id,name,title,spec),component:components(id,name,icon),config:config_items(id,name,type,config_class),check:checks(id,name,icon)` + `/playbook_runs?id=eq.${id}&select=${select}` ); + const actionsSelect = [ + "id", + "name", + "status", + "start_time", + "end_time", + "scheduled_time" + ].join(","); + const resActions = await IncidentCommander.get( - `/playbook_run_actions?select=id,name,status,start_time,end_time,scheduled_time&order=start_time.asc&playbook_run_id=eq.${id}` + `/playbook_run_actions?select=${actionsSelect}&order=start_time.asc&playbook_run_id=eq.${id}` ); const actions = resActions.data ?? []; diff --git a/src/api/types/playbooks.ts b/src/api/types/playbooks.ts index 98c3aa763..4340fd640 100644 --- a/src/api/types/playbooks.ts +++ b/src/api/types/playbooks.ts @@ -3,7 +3,7 @@ import { Agent, Avatar, CreatedAt } from "../traits"; import { ConfigItem } from "./configs"; import { HealthCheckSummary } from "./health"; import { Topology } from "./topology"; -import { User } from "./users"; +import { Team, User } from "./users"; export type PlaybookRunStatus = | "scheduled" @@ -40,8 +40,17 @@ export type PlaybookRunAction = { error?: string; }; +export type PlaybookApproval = { + id: string; + run_id: string; + person_id?: User; + team_id?: Team; + created_at: string; +}; + export interface PlaybookRunWithActions extends PlaybookRun { actions: PlaybookRunAction[]; + playbook_approvals?: PlaybookApproval[]; } export interface PlaybookRun extends CreatedAt, Avatar, Agent { diff --git a/src/components/Playbooks/Runs/Actions/PlaybookRunsActions.tsx b/src/components/Playbooks/Runs/Actions/PlaybookRunsActions.tsx index 01f4e7c40..d16fe9ec5 100644 --- a/src/components/Playbooks/Runs/Actions/PlaybookRunsActions.tsx +++ b/src/components/Playbooks/Runs/Actions/PlaybookRunsActions.tsx @@ -1,4 +1,5 @@ import { + PlaybookApproval, PlaybookRunAction, PlaybookRunWithActions } from "@flanksource-ui/api/types/playbooks"; @@ -16,6 +17,8 @@ import ReRunPlaybookWithParamsButton from "../Submit/ReRunPlaybookWithParamsButt import { PlaybookStatusDescription } from "./../PlaybookRunsStatus"; import PlaybookRunActionFetch from "./PlaybookRunActionFetch"; import PlaybookRunsActionItem from "./PlaybookRunsActionItem"; +import PlaybookRunsApprovalActionItem from "./PlaybookRunsApprovalActionItem"; +import PlaybookRunsApprovalActionsResults from "./PlaybookRunsApprovalActionsResults"; import PlaybooksRunActionsResults from "./PlaybooksActionsResults"; import ShowPlaybookRunsParams from "./ShowParamaters/ShowPlaybookRunsParams"; @@ -29,10 +32,21 @@ export default function PlaybookRunsActions({ refetch = () => {} }: PlaybookRunActionsProps) { const [selectedAction, setSelectedAction] = useState< - PlaybookRunAction | undefined + | { + type: "Action"; + data?: PlaybookRunAction; + } + | { + type: "Approval"; + data: PlaybookApproval; + } + | undefined >(() => { // show the last action by default - return data.actions.at(-1); + return { + type: "Action", + data: data.actions.at(-1) + }; }); const resource = getResourceForRun(data); @@ -151,41 +165,85 @@ export default function PlaybookRunsActions({
{initializationAction && ( setSelectedAction(initializationAction)} + onClick={() => + setSelectedAction({ + type: "Action", + data: initializationAction + }) + } stepNumber={0} /> )} + {data.playbook_approvals && + data.playbook_approvals.length === 1 && ( + + setSelectedAction({ + type: "Approval", + data: data.playbook_approvals?.[0]! + }) + } + /> + )} {data.actions.map((action, index) => ( setSelectedAction(action)} - stepNumber={index + 1} + onClick={() => + setSelectedAction({ + data: action, + type: "Action" + }) + } + stepNumber={index + (data.playbook_approvals ? 2 : 1)} /> ))}
{selectedAction && - (selectedAction.id === "initialization" ? ( + selectedAction.type === "Action" && + selectedAction.data?.id === "initialization" && (
- ) : ( + )} + + {selectedAction && + selectedAction.type === "Action" && + selectedAction.data?.id !== "initialization" && (
- ))} + )} + + {selectedAction && selectedAction.type === "Approval" && ( +
+ +
+ )}
diff --git a/src/components/Playbooks/Runs/Actions/PlaybookRunsApprovalActionItem.tsx b/src/components/Playbooks/Runs/Actions/PlaybookRunsApprovalActionItem.tsx new file mode 100644 index 000000000..6f611a926 --- /dev/null +++ b/src/components/Playbooks/Runs/Actions/PlaybookRunsApprovalActionItem.tsx @@ -0,0 +1,42 @@ +import { PlaybookApproval } from "@flanksource-ui/api/types/playbooks"; +import { Avatar } from "@flanksource-ui/ui/Avatar"; +import clsx from "clsx"; +import { BsCheck2Circle } from "react-icons/bs"; + +type PlaybookRunsActionItemProps = { + onClick?: () => void; + isSelected?: boolean; + approval: PlaybookApproval; +}; + +export default function PlaybookRunsApprovalActionItem({ + onClick = () => {}, + isSelected = false, + approval +}: PlaybookRunsActionItemProps) { + return ( +
+
+
+ + Approved by{" "} + {approval?.person_id ? ( + + ) : ( + + {approval.team_id?.name} + + )} +
+
+
+
+ ); +} diff --git a/src/components/Playbooks/Runs/Actions/PlaybookRunsApprovalActionsResults.tsx b/src/components/Playbooks/Runs/Actions/PlaybookRunsApprovalActionsResults.tsx new file mode 100644 index 000000000..845b7185f --- /dev/null +++ b/src/components/Playbooks/Runs/Actions/PlaybookRunsApprovalActionsResults.tsx @@ -0,0 +1,34 @@ +import { + PlaybookApproval, + PlaybookSpec +} from "@flanksource-ui/api/types/playbooks"; +import { Avatar } from "@flanksource-ui/ui/Avatar"; + +type Props = { + approval?: PlaybookApproval; + className?: string; + playbook: Pick; +}; + +export default function PlaybookRunsApprovalActionsResults({ + approval, + className = "whitespace-pre-wrap break-all", + playbook +}: Props) { + if (!approval) { + return null; + } + + return ( +
+

+ Approved by{" "} + {approval.person_id ? ( + + ) : ( + {approval.team_id?.name} + )} +

+
+ ); +} diff --git a/src/ui/Avatar/index.tsx b/src/ui/Avatar/index.tsx index b8d46127d..20bd5299d 100644 --- a/src/ui/Avatar/index.tsx +++ b/src/ui/Avatar/index.tsx @@ -6,7 +6,7 @@ import { Tooltip } from "react-tooltip"; import { User } from "../../api/types/users"; interface IProps { - size?: "sm" | "lg" | "md"; + size?: "sm" | "lg" | "md" | "xs"; circular?: boolean; inline?: boolean; alt?: string; @@ -14,6 +14,7 @@ interface IProps { imageProps?: React.ComponentPropsWithoutRef<"img">; containerProps?: React.ComponentPropsWithoutRef<"div">; unload?: boolean; + showName?: boolean; } export function Avatar({ @@ -24,9 +25,14 @@ export function Avatar({ containerProps, imageProps, inline = false, - circular = true + circular = true, + showName = false }: IProps) { const [textSize, setTextSize] = useState(() => { + if (size === "xs") { + return "12px"; + } + if (size !== "sm") { return "16px"; } @@ -41,6 +47,8 @@ export function Avatar({ }); const sizeClass = useMemo(() => { switch (size) { + case "xs": + return "w-5 h-5 text-xs"; case "sm": return "w-6 h-6 text-xs"; case "lg": @@ -123,6 +131,7 @@ export function Avatar({ )} + {showName && {user?.name}} );