Skip to content

Commit

Permalink
feat: add menu option to submit playbook run for component
Browse files Browse the repository at this point in the history
Build on top of #1352  PR

Closes #1353

fix: close modal on save

fix: fix submit relaoding full page and wrong endpoint

fix: fix playbook submission params
  • Loading branch information
mainawycliffe committed Sep 12, 2023
1 parent 8b9421f commit 6e4f3e0
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 5 deletions.
9 changes: 9 additions & 0 deletions src/api/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ export const Auth = axios.create({
}
});

export const PlaybookAPI = axios.create({
baseURL: `${API_BASE}/playbook`,
headers: {
Accept: "application/json",
Prefer: "return=representation",
"Content-Type": "application/json"
}
});

export const Rback = axios.create({
baseURL: `${API_BASE}/rbac`,
headers: {
Expand Down
30 changes: 28 additions & 2 deletions src/api/query-hooks/playbooks.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { UseQueryOptions, useQuery } from "@tanstack/react-query";
import {
UseMutationOptions,
UseQueryOptions,
useMutation,
useQuery
} from "@tanstack/react-query";
import { PlaybookSpec } from "../../components/Playbooks/Settings/PlaybookSpecsTable";
import { getAllPlaybooksSpecs, getPlaybookSpec } from "../services/playbooks";
import {
getAllPlaybooksSpecs,
getPlaybookSpec,
submitPlaybookRun
} from "../services/playbooks";
import { SubmitPlaybookRunFormValues } from "../../components/Playbooks/Runs/SubmitPlaybookRunForm";

export function useGetAllPlaybookSpecs(
options: UseQueryOptions<PlaybookSpec[], Error> = {}
Expand Down Expand Up @@ -28,3 +38,19 @@ export function useGetPlaybookSpecsDetails(id: string) {
}
);
}

export function useSubmitPlaybookRunMutation(
options: Omit<
UseMutationOptions<any, Error, SubmitPlaybookRunFormValues>,
"mutationFn"
> = {}
) {
return useMutation({
mutationFn: async (
input: Omit<SubmitPlaybookRunFormValues, "playbook_spec">
) => {
return submitPlaybookRun(input);
},
...options
});
}
10 changes: 9 additions & 1 deletion src/api/services/playbooks.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { SubmitPlaybookRunFormValues } from "../../components/Playbooks/Runs/SubmitPlaybookRunForm";
import {
NewPlaybookSpec,
PlaybookSpec,
UpdatePlaybookSpec
} from "../../components/Playbooks/Settings/PlaybookSpecsTable";
import { AVATAR_INFO } from "../../constants";
import { IncidentCommander } from "../axios";
import { IncidentCommander, PlaybookAPI } from "../axios";

export async function getAllPlaybooksSpecs() {
const res = await IncidentCommander.get<PlaybookSpec[] | null>(
Expand Down Expand Up @@ -42,3 +43,10 @@ export async function deletePlaybookSpec(id: string) {
);
return res.data;
}

export async function submitPlaybookRun(
input: Omit<SubmitPlaybookRunFormValues, "playbook_spec">
) {
const res = await PlaybookAPI.post("/run", input);
return res.data;
}
30 changes: 30 additions & 0 deletions src/components/Playbooks/Runs/AddPlaybookToRunParams.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useFormikContext } from "formik";
import { SubmitPlaybookRunFormValues } from "./SubmitPlaybookRunForm";
import { useMemo } from "react";
import FormikTextInput from "../../Forms/Formik/FormikTextInput";

export default function AddPlaybookToRunParams() {
const { values } = useFormikContext<SubmitPlaybookRunFormValues>();

const playbookParams = useMemo(
() =>
(values.playbook_spec?.parameters as { name: string; label: string }[]) ??
[],
[values.playbook_spec]
);

return (
<div className="flex flex-col gap-4">
<div className="font-bold">Playbook Run Params</div>
<div className="flex flex-col gap-2">
{playbookParams.length > 0 ? (
playbookParams.map(({ name, label }) => (
<FormikTextInput name={`params.${name}`} label={label} />
))
) : (
<div className="text-gray-400">No parameters for this playbook.</div>
)}
</div>
</div>
);
}
40 changes: 40 additions & 0 deletions src/components/Playbooks/Runs/SelectPlaybookToRun.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useFormikContext } from "formik";
import { useGetAllPlaybookSpecs } from "../../../api/query-hooks/playbooks";
import { Loading } from "../../Loading";
import { SubmitPlaybookRunFormValues } from "./SubmitPlaybookRunForm";

export default function SelectPlaybookToRun() {
const { data: playbooks, isLoading, error } = useGetAllPlaybookSpecs();

// access formik state here
const { setFieldValue } = useFormikContext<SubmitPlaybookRunFormValues>();

return (
<div className="flex flex-col gap-2">
<div className="font-bold">Select a Playbook</div>
<div className="flex flex-col gap-2">
{error && !playbooks && (
<div className="text-red-500 text-center">{error.message}</div>
)}
{isLoading && !playbooks && <Loading />}
{playbooks && (
<div className="flex flex-col gap-2">
{playbooks?.map(({ id, name, spec }) => (
<div
role="button"
onClick={() => {
// update formik state here
setFieldValue("id", id);
setFieldValue("playbook_spec", spec);
}}
className="flex flex-row rounded-md border border-gray-200 hover:border-gray-400 hover:bg-gray-200 hover:font-semibold cursor py-2 px-2"
>
{name}
</div>
))}
</div>
)}
</div>
</div>
);
}
106 changes: 106 additions & 0 deletions src/components/Playbooks/Runs/SubmitPlaybookRunForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Form, Formik } from "formik";
import SelectPlaybookToRun from "./SelectPlaybookToRun";
import { useMemo } from "react";
import AddPlaybookToRunParams from "./AddPlaybookToRunParams";
import { Modal } from "../../Modal";
import { Button } from "../../Button";
import { useSubmitPlaybookRunMutation } from "../../../api/query-hooks/playbooks";
import { toastError } from "../../Toast/toast";

export type SubmitPlaybookRunFormValues = {
// if this is present in the form, we show step to add params
id: string;
component_id?: string;
config_id?: string;
params?: Record<string, any>;
// do not send this to the backend
playbook_spec?: Record<string, any>;
};

type Props = {
isOpen: boolean;
onClose: () => void;
};

type SubmitPlaybookRunFormForComponentProps = {
type: "component";
componentId: string;
} & Props;

type SubmitPlaybookRunFormForConfigProps = {
type: "config";
configId: string;
} & Props;

type SubmitPlaybookRunFormProps =
| SubmitPlaybookRunFormForComponentProps
| SubmitPlaybookRunFormForConfigProps;

export default function SubmitPlaybookRunForm({
isOpen,
onClose,
...props
}: SubmitPlaybookRunFormProps) {
const initialValues: Partial<SubmitPlaybookRunFormValues> = useMemo(
() => ({
playbook_id: undefined,
params: undefined,
...(props.type === "component"
? { component_id: props.componentId }
: {
config_id: props.configId
})
}),
[props]
);

const { mutate: submitPlaybookRun } = useSubmitPlaybookRunMutation({
onSuccess: () => {
toastError("Playbook run submitted successfully");
onClose();
},
onError: (error) => {
toastError(error.message);
}
});

return (
<Modal
title="Submit a Playbook Run"
open={isOpen}
onClose={onClose}
size="slightly-small"
bodyClass=""
containerClassName=""
>
<Formik
initialValues={initialValues}
onSubmit={(values) => {
const { playbook_spec, ...rest } = values;
submitPlaybookRun(rest as SubmitPlaybookRunFormValues);
}}
>
{({ values, handleSubmit }) => {
return (
<Form onSubmit={handleSubmit} className="flex flex-col gap-2">
<div className="flex p-4 flex-col gap-2">
{!values.id ? (
<SelectPlaybookToRun />
) : (
<AddPlaybookToRunParams />
)}
</div>
<div className="flex flex-row p-4 justify-end bg-gray-200 rounded-b rounded-md">
<Button
disabled={values.id === undefined}
text="Submit"
type="submit"
/>
</div>
</Form>
);
}}
</Formik>
</Modal>
);
}
11 changes: 11 additions & 0 deletions src/components/TopologyCard/TopologyDropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AttachEvidenceDialog } from "../AttachEvidenceDialog";
import TopologySnapshotModal from "./TopologySnapshotModal";
import { EvidenceType } from "../../api/services/evidence";
import { TopologyConfigLinkModal } from "../TopologyConfigLinkModal/TopologyConfigLinkModal";
import SubmitPlaybookRunForm from "../Playbooks/Runs/SubmitPlaybookRunForm";

type TopologyMenuItemProps = {
onClick?: () => void;
Expand Down Expand Up @@ -72,6 +73,7 @@ export const TopologyDropdownMenu = ({
] = useState(false);
const [attachAsAsset, setAttachAsAsset] = useState(false);
const [linkToConfig, setLinkToConfig] = useState(false);
const [submitPlaybookRun, setSubmitPlaybookRun] = useState(false);

return (
<>
Expand Down Expand Up @@ -101,6 +103,8 @@ export const TopologyDropdownMenu = ({
? () => setAttachAsAsset(true)
: label === "Link to config"
? () => setLinkToConfig(true)
: label === "Submit Playbook Run"
? () => setSubmitPlaybookRun(true)
: undefined
}
/>
Expand All @@ -123,6 +127,13 @@ export const TopologyDropdownMenu = ({
topology={topology}
/>

<SubmitPlaybookRunForm
isOpen={submitPlaybookRun}
onClose={() => setSubmitPlaybookRun(false)}
type="component"
componentId={topology.id}
/>

{linkToConfig && (
<TopologyConfigLinkModal
onCloseModal={() => setLinkToConfig(false)}
Expand Down
32 changes: 30 additions & 2 deletions src/components/TopologySidebar/TopologyActionBar.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React, { useState } from "react";
import { IconType } from "react-icons";
import { BiShow, BiHide, BiZoomIn, BiLink } from "react-icons/bi";
import { BiHide, BiLink, BiShow, BiZoomIn } from "react-icons/bi";
import { ImTree } from "react-icons/im";
import { MdAlarmAdd, MdTableRows } from "react-icons/md";
import { VscDebugConsole } from "react-icons/vsc";
import { useNavigate } from "react-router-dom";
import useUpdateComponentMutation from "../../api/query-hooks/mutations/useUpdateComponentMutation";
import { EvidenceType } from "../../api/services/evidence";
import { Topology } from "../../context/TopologyPageContext";
import { ActionLink } from "../ActionLink/ActionLink";
import { AttachEvidenceDialog } from "../AttachEvidenceDialog";
import TopologySnapshotModal from "../TopologyCard/TopologySnapshotModal";
import { ActionLink } from "../ActionLink/ActionLink";
import { TopologyConfigLinkModal } from "../TopologyConfigLinkModal/TopologyConfigLinkModal";
import SubmitPlaybookRunForm from "../Playbooks/Runs/SubmitPlaybookRunForm";

type TopologyActionItem = {
label: string;
Expand Down Expand Up @@ -198,6 +200,22 @@ export const topologyActionItems: Readonly<TopologyActionItem>[] = [
<ChildComponent onClick={() => onModalOpen()} icon={icon} text={text} />
);
}
},
{
label: "Submit Playbook Run",
icon: VscDebugConsole,
isShown: () => true,
ContainerComponent: function Container({
child: ChildComponent,
topology,
icon,
text,
openModalAction: onModalOpen = () => {}
}) {
return (
<ChildComponent onClick={() => onModalOpen()} icon={icon} text={text} />
);
}
}
];

Expand All @@ -217,6 +235,7 @@ export default function TopologyActionBar({

const [attachAsAsset, setAttachAsAsset] = useState(false);
const [linkToConfig, setLinkToConfig] = useState(false);
const [submitPlaybookRun, setSubmitPlaybookRun] = useState(false);

const onOpenModal = (label: string) => {
switch (label) {
Expand All @@ -226,6 +245,8 @@ export default function TopologyActionBar({
return setAttachAsAsset(true);
case "Link to config":
return setLinkToConfig(true);
case "Submit Playbook Run":
return setSubmitPlaybookRun(true);
default:
break;
}
Expand Down Expand Up @@ -272,6 +293,13 @@ export default function TopologyActionBar({
topology={topology}
/>

<SubmitPlaybookRunForm
isOpen={submitPlaybookRun}
onClose={() => setSubmitPlaybookRun(false)}
type="component"
componentId={topology.id}
/>

{linkToConfig && (
<TopologyConfigLinkModal
onCloseModal={() => setLinkToConfig(false)}
Expand Down

0 comments on commit 6e4f3e0

Please sign in to comment.