Skip to content

Commit

Permalink
feat: add playbook approval feature on the UI
Browse files Browse the repository at this point in the history
Fixes #2304
  • Loading branch information
mainawycliffe committed Oct 17, 2024
1 parent 26fc66b commit da59c6a
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 17 deletions.
7 changes: 7 additions & 0 deletions src/api/services/playbooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,10 @@ export async function getPlaybookRuns({
);
return res;
}

export async function approvePlaybookRun(id: string) {
const res = await PlaybookAPI.post<{
message: string;
}>(`/run/approve/${id}`);
return res.data;
}
3 changes: 2 additions & 1 deletion src/api/types/playbooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export type PlaybookRunStatus =
| "completed"
| "failed"
| "pending"
| "waiting";
| "waiting"
| "pending_approval";

export type PlaybookRunActionStatus =
| "completed"
Expand Down
16 changes: 14 additions & 2 deletions src/components/Playbooks/Runs/Actions/PlaybookRunsActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ import ShowPlaybookRunsParams from "./ShowParamaters/ShowPlaybookRunsParams";

type PlaybookRunActionsProps = {
data: PlaybookRunWithActions;
refetch?: () => void;
};

export default function PlaybookRunsActions({ data }: PlaybookRunActionsProps) {
export default function PlaybookRunsActions({
data,
refetch = () => {}
}: PlaybookRunActionsProps) {
const [selectedAction, setSelectedAction] = useState<
PlaybookRunAction | undefined
>(() => {
Expand Down Expand Up @@ -69,7 +73,15 @@ export default function PlaybookRunsActions({ data }: PlaybookRunActionsProps) {

<VerticalDescription
label="Status"
value={<PlaybookStatusDescription status={data.status} />}
value={
<PlaybookStatusDescription
status={data.status}
playbookTitle={data.playbooks?.title ?? data.playbooks?.name!}
showApproveButton
playbookRunId={data.id}
refetch={refetch}
/>
}
/>
<VerticalDescription
label="Started"
Expand Down
48 changes: 48 additions & 0 deletions src/components/Playbooks/Runs/ApprovePlaybookRunModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { approvePlaybookRun } from "@flanksource-ui/api/services/playbooks";
import { toastError } from "@flanksource-ui/components/Toast/toast";
import { ConfirmationPromptDialog } from "@flanksource-ui/ui/AlertDialog/ConfirmationPromptDialog";
import { useMutation } from "@tanstack/react-query";
import { AxiosError } from "axios";

type ApprovePlaybookRunModalProps = {
onClose: () => void;
open: boolean;
playbookTitle: string;
playbookRunId: string;
refetch?: () => void;
};

export default function ApprovePlaybookRunModal({
onClose,
open,
playbookTitle,
playbookRunId,
refetch = () => {}
}: ApprovePlaybookRunModalProps) {
const { mutate: approve, isLoading } = useMutation({
mutationFn: (id: string) => {
return approvePlaybookRun(id);
},
onError: (error: AxiosError) => {
console.error("Failed to approve playbook run", error);
toastError(`Failed to approve playbook run: ${error.message}`);
},
onSuccess: () => {
onClose();
refetch();
}
});

return (
<ConfirmationPromptDialog
title={`Approve Playbook ${playbookTitle} Run`}
description={<p>Are you sure you want to approve this playbook run?</p>}
onConfirm={() => approve(playbookRunId)}
open={open}
onClose={onClose}
isOpen={open}
yesLabel={isLoading ? "Approving..." : "Approve"}
closeLabel="Cancel"
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ const options: StateOption[] = [
icon: statusIconMap["pending"],
label: "Pending",
value: "pending"
},
{
icon: statusIconMap["pending_approval"],
label: "Pending Approval",
value: "pending_approval"
}
].sort((a, b) => a.label.localeCompare(b.label));

Expand Down
14 changes: 12 additions & 2 deletions src/components/Playbooks/Runs/PlaybookRunsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,19 @@ const playbookRunsTableColumns: MRT_ColumnDef<PlaybookRun>[] = [
{
header: "Status",
accessorKey: "status",
Cell: ({ cell }) => {
Cell: ({ cell, row }) => {
const status = cell.getValue<PlaybookRunStatus>();
return <PlaybookStatusDescription status={status} />;
const playbookRunId = row.original.id;
const title =
row.original.playbooks?.title ?? row.original.playbooks?.name!;

return (
<PlaybookStatusDescription
status={status}
playbookRunId={playbookRunId}
playbookTitle={title}
/>
);
}
},
{
Expand Down
48 changes: 47 additions & 1 deletion src/components/Playbooks/Runs/PlaybookRunsStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,55 @@
import { Button } from "@flanksource-ui/ui/Buttons/Button";
import { useState } from "react";
import { Tooltip } from "react-tooltip";
import {
PlaybookRunsStatusProps,
PlaybookStatusIcon
} from "../../../ui/Icons/PlaybookStatusIcon";
import ApprovePlaybookRunModal from "./ApprovePlaybookRunModal";

export function PlaybookStatusDescription({
status,
playbookTitle,
showApproveButton = false,
playbookRunId,
refetch = () => {}
}: PlaybookRunsStatusProps & {
playbookTitle: string;
showApproveButton?: boolean;
playbookRunId: string;
refetch?: () => void;
}) {
const [showApprovalModal, setShowApprovalModal] = useState(false);

if (status === "pending_approval" && showApproveButton) {
return (
<>
<Button
data-tooltip-content={`Approve ${playbookTitle} Run`}
id="approve-playbook-run"
size="xs"
className="btn-link flex flex-row items-center"
onClick={() => {
setShowApprovalModal(true);
}}
>
<PlaybookStatusIcon status={status} />
<span className="capitalize">Pending Approval</span>
</Button>
<ApprovePlaybookRunModal
onClose={() => {
setShowApprovalModal(false);
}}
open={showApprovalModal}
playbookTitle={playbookTitle}
playbookRunId={playbookRunId}
refetch={refetch}
/>
<Tooltip id="approve-playbook-run" />
</>
);
}

export function PlaybookStatusDescription({ status }: PlaybookRunsStatusProps) {
return (
<div className="flex flex-row items-center gap-1">
<PlaybookStatusIcon status={status} />
Expand Down
2 changes: 1 addition & 1 deletion src/pages/playbooks/PlaybookRunsDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export default function PlaybookRunsDetailsPage() {
<TabbedLinks activeTabName={`Runs`} tabLinks={playbookRunsPageTabs}>
<div className={`mx-auto flex h-full flex-col p-4`}>
{playbookRun ? (
<PlaybookRunsActions data={playbookRun} />
<PlaybookRunsActions data={playbookRun} refetch={refetch} />
) : (
<CardsSkeletonLoader />
)}
Expand Down
4 changes: 2 additions & 2 deletions src/ui/AlertDialog/ConfirmationPromptDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Dialog } from "@headlessui/react";
import { XIcon } from "@heroicons/react/outline";
import clsx from "clsx";
import { ComponentProps } from "react";
import React, { ComponentProps } from "react";
import { FaCircleNotch } from "react-icons/fa";

type ConfirmationPromptDialogProps = {
isOpen: boolean;
title: string;
description: string;
description: React.ReactNode;
onClose: () => void;
onConfirm: () => void;
yesLabel?: string;
Expand Down
19 changes: 11 additions & 8 deletions src/ui/Icons/PlaybookStatusIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,34 @@ export const statusIconMap: Record<
React.ReactElement
> = {
completed: (
<BsCheckCircle className="inline h-5 w-auto object-center pr-1 text-green-500" />
<BsCheckCircle className="inline h-5 w-auto object-center text-green-500" />
),
cancelled: (
<BsStopCircle className="inline h-5 w-auto object-center pr-1 text-red-500" />
<BsStopCircle className="inline h-5 w-auto object-center text-red-500" />
),
failed: (
<BsXCircle className="inline h-5 w-auto object-center pr-1 text-red-500" />
<BsXCircle className="inline h-5 w-auto object-center text-red-500" />
),
pending: (
<BsCircle className="inline h-5 w-auto object-center text-orange-500" />
),
pending_approval: (
<BsCircle className="inline h-5 w-auto object-center pr-1 text-orange-500" />
),
waiting: (
<BsCircle className="inline h-5 w-auto object-center pr-1 text-orange-500" />
<BsCircle className="inline h-5 w-auto object-center text-orange-500" />
),
running: (
<BsPlayCircle className="inline h-5 w-auto object-center pr-1 text-orange-500" />
<BsPlayCircle className="inline h-5 w-auto object-center text-orange-500" />
),
scheduled: (
<BsClock className="inline h-5 w-auto object-center pr-1 text-gray-500" />
<BsClock className="inline h-5 w-auto object-center text-gray-500" />
),
sleeping: (
<BsClock className="inline h-5 w-auto object-center pr-1 text-gray-400" />
<BsClock className="inline h-5 w-auto object-center text-gray-400" />
),
skipped: (
<BsSlashCircle className="inline h-5 w-auto object-center pr-1 text-gray-500" />
<BsSlashCircle className="inline h-5 w-auto object-center text-gray-500" />
)
};

Expand Down

0 comments on commit da59c6a

Please sign in to comment.