Skip to content

Commit

Permalink
Merge pull request #1455 from flanksource/1445-playbook-improvements
Browse files Browse the repository at this point in the history
1445 playbook improvements
  • Loading branch information
moshloop authored Oct 24, 2023
2 parents c556673 + 025e2bc commit 336c173
Show file tree
Hide file tree
Showing 13 changed files with 332 additions and 51 deletions.
14 changes: 9 additions & 5 deletions src/api/query-hooks/playbooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import {
useMutation,
useQuery
} from "@tanstack/react-query";
import { SubmitPlaybookRunFormValues } from "../../components/Playbooks/Runs/Submit/SubmitPlaybookRunForm";
import { PlaybookSpec } from "../../components/Playbooks/Settings/PlaybookSpecsTable";
import {
getAllPlaybooksSpecs,
getPlaybookToRunForResource,
getPlaybookSpec,
getPlaybookToRunForResource,
submitPlaybookRun
} from "../services/playbooks";
import { SubmitPlaybookRunFormValues } from "../../components/Playbooks/Runs/SubmitPlaybookRunForm";

export function useGetAllPlaybookSpecs(
options: UseQueryOptions<PlaybookSpec[], Error> = {}
Expand Down Expand Up @@ -48,15 +48,19 @@ export function useGetPlaybooksToRun(
);
}

export function useGetPlaybookSpecsDetails(id: string) {
return useQuery<Record<string, any>, Error>(
export function useGetPlaybookSpecsDetails(
id: string,
options: UseQueryOptions<PlaybookSpec | undefined, Error> = {}
) {
return useQuery<PlaybookSpec | undefined, Error>(
["playbooks", "settings", "specs", id],
async () => getPlaybookSpec(id),
{
enabled: !!id,
cacheTime: 0,
staleTime: 0,
keepPreviousData: false
keepPreviousData: false,
...options
}
);
}
Expand Down
19 changes: 13 additions & 6 deletions src/api/services/playbooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
PlaybookRun,
PlaybookRunAction
} from "../../components/Playbooks/Runs/PlaybookRunTypes";
import { SubmitPlaybookRunFormValues } from "../../components/Playbooks/Runs/SubmitPlaybookRunForm";
import { SubmitPlaybookRunFormValues } from "../../components/Playbooks/Runs/Submit/SubmitPlaybookRunForm";
import {
NewPlaybookSpec,
PlaybookSpec,
Expand All @@ -22,10 +22,10 @@ export async function getAllPlaybooksSpecs() {
}

export async function getPlaybookSpec(id: string) {
const res = await IncidentCommander.get<PlaybookSpec | null>(
`/playbooks/${id},created_by(${AVATAR_INFO})`
const res = await IncidentCommander.get<PlaybookSpec[] | null>(
`/playbooks?id=eq.${id}&select=*,created_by(${AVATAR_INFO})`
);
return res.data ?? undefined;
return res.data?.[0] ?? undefined;
}

export async function createPlaybookSpec(spec: NewPlaybookSpec) {
Expand Down Expand Up @@ -89,23 +89,30 @@ export async function getPlaybookRuns({
componentId,
configId,
pageIndex,
pageSize
pageSize,
playbookId
}: {
componentId?: string;
configId?: string;
pageIndex: number;
pageSize: number;
playbookId?: string;
}) {
const componentParamString = componentId
? `&component_id=eq.${componentId}`
: "";

const configParamString = configId ? `&config_id=eq.${configId}` : "";

const pagingParams = `&limit=${pageSize}&offset=${pageIndex * pageSize}`;

const playbookParamsString = playbookId
? `&playbook_id=eq.${playbookId}`
: "";

const res = await resolve(
ConfigDB.get<PlaybookRun[] | null>(
`/playbook_runs?select=*,playbooks(id,name),component:components(id,name,icon),check:checks(id,name,icon),config:config_items(id,name,type,config_class)&order=created_at.desc${componentParamString}&${configParamString}${pagingParams}}`,
`/playbook_runs?select=*,playbooks(id,name),component:components(id,name,icon),check:checks(id,name,icon),config:config_items(id,name,type,config_class)&&order=created_at.desc${playbookParamsString}${componentParamString}&${configParamString}${pagingParams}}`,
{
headers: {
Prefer: "count=exact"
Expand Down
2 changes: 1 addition & 1 deletion src/components/Canary/minimal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { HealthCheck } from "../../types/healthChecks";
import AttachAsEvidenceButton from "../AttachEvidenceDialog/AttachAsEvidenceDialogButton";
import { timeRanges } from "../Dropdown/TimeRange";
import { Modal } from "../Modal";
import SelectPlaybookToRun from "../Playbooks/Runs/SelectPlaybookToRun";
import SelectPlaybookToRun from "../Playbooks/Runs/Submit/SelectPlaybookToRun";
import { toastError } from "../Toast/toast";
import { CheckDetails } from "./CanaryPopup/CheckDetails";
import { CheckTitle } from "./CanaryPopup/CheckTitle";
Expand Down
2 changes: 1 addition & 1 deletion src/components/ConfigSidebar/ConfigActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { EvidenceType } from "../../api/services/evidence";
import { usePartialUpdateSearchParams } from "../../hooks/usePartialUpdateSearchParams";
import { ActionLink } from "../ActionLink/ActionLink";
import AttachAsEvidenceButton from "../AttachEvidenceDialog/AttachAsEvidenceDialogButton";
import SelectPlaybookToRun from "../Playbooks/Runs/SelectPlaybookToRun";
import SelectPlaybookToRun from "../Playbooks/Runs/Submit/SelectPlaybookToRun";

type ConfigActionBarProps = {
configId: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMemo } from "react";
import FormikTextInput from "../../Forms/Formik/FormikTextInput";
import { PlaybookSpec } from "../Settings/PlaybookSpecsTable";
import FormikTextInput from "../../../Forms/Formik/FormikTextInput";
import { PlaybookSpec } from "../../Settings/PlaybookSpecsTable";

type AddPlaybookToRunParamsProps = {
playbookSpec: PlaybookSpec;
Expand All @@ -21,7 +21,11 @@ export default function AddPlaybookToRunParams({
<div className="flex flex-col gap-2">
{playbookParams.length > 0 ? (
playbookParams.map(({ name, label }) => (
<FormikTextInput name={`params.${name}`} label={label} />
<FormikTextInput
name={`params.${name}`}
label={label}
key={`${label}-${name}`}
/>
))
) : (
<div className="text-gray-400">No parameters for this playbook.</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Float } from "@headlessui-float/react";
import { Fragment, useState } from "react";
import { FaChevronDown, FaChevronUp } from "react-icons/fa";
import { useGetPlaybooksToRun } from "../../../api/query-hooks/playbooks";
import { Button } from "../../Button";
import { useGetPlaybooksToRun } from "../../../../api/query-hooks/playbooks";
import { Button } from "../../../Button";
import { Menu } from "@headlessui/react";
import { PlaybookSpec } from "../Settings/PlaybookSpecsTable";
import { PlaybookSpec } from "../../Settings/PlaybookSpecsTable";
import SubmitPlaybookRunForm from "./SubmitPlaybookRunForm";

type SelectPlaybookToRunProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Form, Formik } from "formik";
import { useMemo } from "react";
import { useSubmitPlaybookRunMutation } from "../../../api/query-hooks/playbooks";
import { Button } from "../../Button";
import { Modal } from "../../Modal";
import { toastError, toastSuccess } from "../../Toast/toast";
import { PlaybookSpec } from "../Settings/PlaybookSpecsTable";
import { useSubmitPlaybookRunMutation } from "../../../../api/query-hooks/playbooks";
import { Button } from "../../../Button";
import { Modal } from "../../../Modal";
import { toastError, toastSuccess } from "../../../Toast/toast";
import { PlaybookSpec } from "../../Settings/PlaybookSpecsTable";
import AddPlaybookToRunParams from "./AddPlaybookToRunParams";

export type SubmitPlaybookRunFormValues = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { PlaybookSpec } from "../../../Settings/PlaybookSpecsTable";
import SelectPlaybookToRun from "./../SelectPlaybookToRun";

const playbooks: PlaybookSpec[] = [
{
id: "1",
name: "Playbook 1",
created_at: "2021-09-01T00:00:00Z",
source: "UI",
spec: {},
updated_at: "2021-09-01T00:00:00Z"
},
{
id: "2",
name: "Playbook 2",
created_at: "2021-09-01T00:00:00Z",
source: "UI",
spec: {},
updated_at: "2021-09-01T00:00:00Z"
}
];

global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn()
}));

// Define a mock server to handle PATCH requests
const server = setupServer(
rest.get("/api/playbook/list", (req, res, ctx) => {
return res(ctx.json(playbooks));
})
);

const queryClient = new QueryClient();

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe("SelectPlaybookToRun", () => {
it("should render dropdown list with playbooks", async () => {
render(
<QueryClientProvider client={queryClient}>
<SelectPlaybookToRun component_id="component_id" />
</QueryClientProvider>
);

const playbooksButton = await screen.findByRole("button", {
name: /playbooks/i
});

userEvent.click(playbooksButton);

expect(await screen.findByText(/playbook 1/i)).toBeInTheDocument();
expect(await screen.findByText(/playbook 2/i)).toBeInTheDocument();
});

it("should open runs page, when you click a playbook item", async () => {
render(
<QueryClientProvider client={queryClient}>
<SelectPlaybookToRun check_id="check_id" />
</QueryClientProvider>
);

const playbooksButton = await screen.findByRole("button", {
name: /playbooks/i
});

userEvent.click(playbooksButton);

const playbook1 = await screen.findByText(/playbook 1/i);

userEvent.click(playbook1);

await waitFor(() => {
expect(
screen.getByRole("heading", { level: 1, name: /playbook 1/i })
).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { PlaybookSpec } from "../../../Settings/PlaybookSpecsTable";
import SubmitPlaybookRunForm from "./../SubmitPlaybookRunForm";

const playbookSpec: PlaybookSpec = {
id: "1",
name: "Playbook 1",
source: "UI",
spec: {
parameters: [
{
label: "Label",
name: "name"
}
]
},
created_at: "2021-09-01T00:00:00Z",
updated_at: "2021-09-01T00:00:00Z"
};

global.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: jest.fn(),
unobserve: jest.fn(),
disconnect: jest.fn()
}));

// Define a mock server to handle PATCH requests
const server = setupServer(
rest.post("/api/playbook/run", (req, res, ctx) => {
return res(ctx.json(playbookSpec));
})
);

const queryClient = new QueryClient();

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe("SubmitPlaybookRunForm", () => {
const componentId = "component-1";
const checkId = "check-1";
const configId = "config-1";

it("should render the form with the correct initial values", async () => {
const closeFn = jest.fn();

render(
<QueryClientProvider client={queryClient}>
<SubmitPlaybookRunForm
isOpen={true}
onClose={closeFn}
playbookSpec={playbookSpec}
componentId={componentId}
checkId={checkId}
configId={configId}
/>
</QueryClientProvider>
);

expect(
await screen.findByRole("heading", { level: 1, name: /Playbook 1/i })
).toBeInTheDocument();

expect(screen.getByLabelText("Label")).toBeInTheDocument();

expect(screen.getByRole("button", { name: /Submit/i })).toBeInTheDocument();

userEvent.click(screen.getByRole("button", { name: /close/i }));

await waitFor(() => {
expect(closeFn).toHaveBeenCalled();
});
});

it("should submit the form when the submit button is clicked", async () => {
const closeFn = jest.fn();
render(
<QueryClientProvider client={queryClient}>
<SubmitPlaybookRunForm
isOpen={true}
onClose={closeFn}
playbookSpec={playbookSpec}
componentId={componentId}
checkId={checkId}
configId={configId}
/>
</QueryClientProvider>
);

expect(
await screen.findByRole("heading", { level: 1, name: /Playbook 1/i })
).toBeInTheDocument();

const input = screen.getByLabelText("Label");

fireEvent.change(input, { target: { value: "test" } });

const btn = screen.getByRole("button", { name: /Submit/i });

userEvent.click(btn);

await waitFor(() => {
expect(closeFn).toHaveBeenCalledTimes(1);
});
});
});
2 changes: 1 addition & 1 deletion src/components/TopologySidebar/TopologyActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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 SelectPlaybookToRun from "../Playbooks/Runs/Submit/SelectPlaybookToRun";
import TopologySnapshotModal from "../TopologyCard/TopologySnapshotModal";
import { TopologyConfigLinkModal } from "../TopologyConfigLinkModal/TopologyConfigLinkModal";

Expand Down
Loading

0 comments on commit 336c173

Please sign in to comment.