From 367a9b6e58a39c573bc8fedce8414093d47b5560 Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Mon, 4 Sep 2023 15:22:16 +0300 Subject: [PATCH] feat: add dropdown for components to submit playbook runs 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 --- src/api/axios.ts | 9 ++ src/api/query-hooks/playbooks.tsx | 52 +++++++- src/api/services/playbooks.ts | 22 +++- src/components/Menu/index.tsx | 3 +- .../Playbooks/Runs/AddPlaybookToRunParams.tsx | 32 +++++ .../Playbooks/Runs/SelectPlaybookToRun.tsx | 79 ++++++++++++ .../Playbooks/Runs/SubmitPlaybookRunForm.tsx | 116 ++++++++++++++++++ .../TopologyCard/TopologyDropdownMenu.tsx | 12 +- .../TopologySidebar/TopologyActionBar.tsx | 10 +- 9 files changed, 321 insertions(+), 14 deletions(-) create mode 100644 src/components/Playbooks/Runs/AddPlaybookToRunParams.tsx create mode 100644 src/components/Playbooks/Runs/SelectPlaybookToRun.tsx create mode 100644 src/components/Playbooks/Runs/SubmitPlaybookRunForm.tsx diff --git a/src/api/axios.ts b/src/api/axios.ts index 6564fb6dbd..b05f212d9a 100644 --- a/src/api/axios.ts +++ b/src/api/axios.ts @@ -87,6 +87,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: { diff --git a/src/api/query-hooks/playbooks.tsx b/src/api/query-hooks/playbooks.tsx index a1684ea742..ac83c1f65f 100644 --- a/src/api/query-hooks/playbooks.tsx +++ b/src/api/query-hooks/playbooks.tsx @@ -1,6 +1,17 @@ -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, + getPlaybookRun, + getPlaybookSpec, + submitPlaybookRun +} from "../services/playbooks"; +import { SubmitPlaybookRunFormValues } from "../../components/Playbooks/Runs/SubmitPlaybookRunForm"; export function useGetAllPlaybookSpecs( options: UseQueryOptions = {} @@ -16,6 +27,27 @@ export function useGetAllPlaybookSpecs( ); } +export type GetPlaybooksToRunParams = { + component_id?: string; + config_id?: string; + check_id?: string; +}; + +export function useGetPlaybooksToRun( + params: GetPlaybooksToRunParams, + options: UseQueryOptions = {} +) { + return useQuery( + ["playbooks", "run", params], + () => getPlaybookRun(params), + { + cacheTime: 0, + staleTime: 0, + ...options + } + ); +} + export function useGetPlaybookSpecsDetails(id: string) { return useQuery, Error>( ["playbooks", "settings", "specs", id], @@ -28,3 +60,19 @@ export function useGetPlaybookSpecsDetails(id: string) { } ); } + +export function useSubmitPlaybookRunMutation( + options: Omit< + UseMutationOptions, + "mutationFn" + > = {} +) { + return useMutation({ + mutationFn: async ( + input: Omit + ) => { + return submitPlaybookRun(input); + }, + ...options + }); +} diff --git a/src/api/services/playbooks.ts b/src/api/services/playbooks.ts index c33f06aaa6..76034f2afa 100644 --- a/src/api/services/playbooks.ts +++ b/src/api/services/playbooks.ts @@ -1,10 +1,12 @@ +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"; +import { GetPlaybooksToRunParams } from "../query-hooks/playbooks"; export async function getAllPlaybooksSpecs() { const res = await IncidentCommander.get( @@ -42,3 +44,21 @@ export async function deletePlaybookSpec(id: string) { ); return res.data; } + +export async function submitPlaybookRun( + input: Omit +) { + const res = await PlaybookAPI.post("/run", input); + return res.data; +} + +export async function getPlaybookRun(params: GetPlaybooksToRunParams) { + const paramsString = Object.entries(params) + .filter(([, value]) => value) + .map(([key, value]) => `${key}=${value}`) + .join("&"); + const res = await PlaybookAPI.get( + `/list?${paramsString}` + ); + return res.data ?? []; +} diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index aa397f9aa2..ae302ada2e 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -67,5 +67,6 @@ const MenuC = ({ children }: MenuProps) => ( export const Menu = Object.assign(MenuC, { Item: Item, Items: Items, - VerticalIconButton: VerticalIconButton + VerticalIconButton: VerticalIconButton, + Button: HLMenu.Button }); diff --git a/src/components/Playbooks/Runs/AddPlaybookToRunParams.tsx b/src/components/Playbooks/Runs/AddPlaybookToRunParams.tsx new file mode 100644 index 0000000000..7f0228f17b --- /dev/null +++ b/src/components/Playbooks/Runs/AddPlaybookToRunParams.tsx @@ -0,0 +1,32 @@ +import { useMemo } from "react"; +import FormikTextInput from "../../Forms/Formik/FormikTextInput"; +import { PlaybookSpec } from "../Settings/PlaybookSpecsTable"; + +type AddPlaybookToRunParamsProps = { + playbookSpec: PlaybookSpec; +}; + +export default function AddPlaybookToRunParams({ + playbookSpec +}: AddPlaybookToRunParamsProps) { + const playbookParams = useMemo( + () => + (playbookSpec.spec.parameters as { name: string; label: string }[]) ?? [], + [playbookSpec] + ); + + return ( +
+
Playbook Run Params
+
+ {playbookParams.length > 0 ? ( + playbookParams.map(({ name, label }) => ( + + )) + ) : ( +
No parameters for this playbook.
+ )} +
+
+ ); +} diff --git a/src/components/Playbooks/Runs/SelectPlaybookToRun.tsx b/src/components/Playbooks/Runs/SelectPlaybookToRun.tsx new file mode 100644 index 0000000000..3358de8517 --- /dev/null +++ b/src/components/Playbooks/Runs/SelectPlaybookToRun.tsx @@ -0,0 +1,79 @@ +import { Fragment, useState } from "react"; +import { FaChevronDown, FaChevronUp } from "react-icons/fa"; +import { useGetPlaybooksToRun } from "../../../api/query-hooks/playbooks"; +import { Button } from "../../Button"; +import SubmitPlaybookRunForm from "./SubmitPlaybookRunForm"; +import { Menu } from "../../Menu"; +import { PlaybookSpec } from "../Settings/PlaybookSpecsTable"; + +type SelectPlaybookToRunProps = { + component_id?: string; + config_id?: string; + check_id?: string; +}; + +export default function SelectPlaybookToRun({ + check_id, + component_id, + config_id +}: SelectPlaybookToRunProps) { + const [selectedPlaybookSpec, setSelectedPlaybookSpec] = + useState(); + + const { + data: playbooks, + isLoading, + error + } = useGetPlaybooksToRun({ + check_id, + component_id, + config_id + }); + + if (error || playbooks?.length === 0 || isLoading) { + return null; + } + + return ( +
+ + + {({ open }) => ( +
+
+ } + className="text-sm btn-white" + disabled={isLoading} + /> +
+ )} + + + {playbooks?.map((playbook) => ( + setSelectedPlaybookSpec(playbook)} + key={playbook.id} + > + {playbook.name} + + ))} + + + {selectedPlaybookSpec && ( + setSelectedPlaybookSpec(undefined)} + playbookSpec={selectedPlaybookSpec} + /> + )} + + ); +} diff --git a/src/components/Playbooks/Runs/SubmitPlaybookRunForm.tsx b/src/components/Playbooks/Runs/SubmitPlaybookRunForm.tsx new file mode 100644 index 0000000000..d4bdf4d076 --- /dev/null +++ b/src/components/Playbooks/Runs/SubmitPlaybookRunForm.tsx @@ -0,0 +1,116 @@ +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"; +import { PlaybookSpec } from "../Settings/PlaybookSpecsTable"; + +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; + // do not send this to the backend + playbook_spec?: Record; +}; + +type Props = { + isOpen: boolean; + onClose: () => void; + playbookSpec: PlaybookSpec; +}; + +type SubmitPlaybookRunFormForComponentProps = { + type: "component"; + componentId: string; +} & Props; + +type SubmitPlaybookRunFormForConfigProps = { + type: "config"; + configId: string; +} & Props; + +type SubmitPlaybookRunFormForCheckProps = { + type: "check"; + checkId: string; +} & Props; + +type SubmitPlaybookRunFormProps = + | SubmitPlaybookRunFormForComponentProps + | SubmitPlaybookRunFormForConfigProps + | SubmitPlaybookRunFormForCheckProps; + +export default function SubmitPlaybookRunForm({ + isOpen, + onClose, + playbookSpec, + ...props +}: SubmitPlaybookRunFormProps) { + const initialValues: Partial = useMemo( + () => ({ + playbook_id: playbookSpec.id, + params: undefined, + + ...(props.type === "component" + ? { component_id: props.componentId } + : props.type === "check" + ? { + check_id: props.checkId + } + : { + config_id: props.configId + }) + }), + [props, playbookSpec] + ); + + const { mutate: submitPlaybookRun } = useSubmitPlaybookRunMutation({ + onSuccess: () => { + toastError("Playbook run submitted successfully"); + onClose(); + }, + onError: (error) => { + toastError(error.message); + } + }); + + return ( + + { + const { playbook_spec, ...rest } = values; + submitPlaybookRun(rest as SubmitPlaybookRunFormValues); + }} + > + {({ values, handleSubmit }) => { + return ( +
+
+ +
+
+
+
+ ); + }} +
+
+ ); +} diff --git a/src/components/TopologyCard/TopologyDropdownMenu.tsx b/src/components/TopologyCard/TopologyDropdownMenu.tsx index 6a8a9c5bcf..6c1a876534 100644 --- a/src/components/TopologyCard/TopologyDropdownMenu.tsx +++ b/src/components/TopologyCard/TopologyDropdownMenu.tsx @@ -1,13 +1,13 @@ -import { Menu } from "../Menu"; -import { Topology } from "../../context/TopologyPageContext"; -import { topologyActionItems } from "../TopologySidebar/TopologyActionBar"; import { CSSProperties, useCallback, useMemo, useState } from "react"; -import { AttachEvidenceDialog } from "../AttachEvidenceDialog"; -import TopologySnapshotModal from "./TopologySnapshotModal"; import { EvidenceType } from "../../api/services/evidence"; -import { TopologyConfigLinkModal } from "../TopologyConfigLinkModal/TopologyConfigLinkModal"; import { useFeatureFlagsContext } from "../../context/FeatureFlagsContext"; +import { Topology } from "../../context/TopologyPageContext"; import { features } from "../../services/permissions/features"; +import { AttachEvidenceDialog } from "../AttachEvidenceDialog"; +import { Menu } from "../Menu"; +import { TopologyConfigLinkModal } from "../TopologyConfigLinkModal/TopologyConfigLinkModal"; +import { topologyActionItems } from "../TopologySidebar/TopologyActionBar"; +import TopologySnapshotModal from "./TopologySnapshotModal"; type TopologyMenuItemProps = { onClick?: () => void; diff --git a/src/components/TopologySidebar/TopologyActionBar.tsx b/src/components/TopologySidebar/TopologyActionBar.tsx index ec0f370b20..8bf841a48a 100644 --- a/src/components/TopologySidebar/TopologyActionBar.tsx +++ b/src/components/TopologySidebar/TopologyActionBar.tsx @@ -1,18 +1,19 @@ import React, { useMemo, 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 { useNavigate } from "react-router-dom"; import useUpdateComponentMutation from "../../api/query-hooks/mutations/useUpdateComponentMutation"; import { EvidenceType } from "../../api/services/evidence"; +import { useFeatureFlagsContext } from "../../context/FeatureFlagsContext"; import { Topology } from "../../context/TopologyPageContext"; +import { features } from "../../services/permissions/features"; +import { ActionLink } from "../ActionLink/ActionLink"; import { AttachEvidenceDialog } from "../AttachEvidenceDialog"; +import SelectPlaybookToRun from "../Playbooks/Runs/SelectPlaybookToRun"; import TopologySnapshotModal from "../TopologyCard/TopologySnapshotModal"; -import { ActionLink } from "../ActionLink/ActionLink"; import { TopologyConfigLinkModal } from "../TopologyConfigLinkModal/TopologyConfigLinkModal"; -import { useFeatureFlagsContext } from "../../context/FeatureFlagsContext"; -import { features } from "../../services/permissions/features"; type TopologyActionItem = { label: string; @@ -279,6 +280,7 @@ export default function TopologyActionBar({ } return null; })} +