From 478df517182e94dfdee4fd8878cfe8134d850b89 Mon Sep 17 00:00:00 2001 From: Maina Wycliffe Date: Thu, 31 Oct 2024 22:48:19 +0300 Subject: [PATCH] feat: improve playbooks menu Fixes #2334 --- .../Runs/Submit/PlaybooksDropdownMenu.tsx | 109 ++++++++----- .../SelectPlaybookToRun.unit.test.tsx | 143 ------------------ 2 files changed, 68 insertions(+), 184 deletions(-) delete mode 100644 src/components/Playbooks/Runs/Submit/__tests__/SelectPlaybookToRun.unit.test.tsx diff --git a/src/components/Playbooks/Runs/Submit/PlaybooksDropdownMenu.tsx b/src/components/Playbooks/Runs/Submit/PlaybooksDropdownMenu.tsx index 996890123..15c72b3ca 100644 --- a/src/components/Playbooks/Runs/Submit/PlaybooksDropdownMenu.tsx +++ b/src/components/Playbooks/Runs/Submit/PlaybooksDropdownMenu.tsx @@ -1,14 +1,9 @@ import { AuthorizationAccessCheck } from "@flanksource-ui/components/Permissions/AuthorizationAccessCheck"; +import CollapsiblePanel from "@flanksource-ui/ui/CollapsiblePanel/CollapsiblePanel"; import { Icon } from "@flanksource-ui/ui/Icons/Icon"; -import { - Menu, - MenuButton, - MenuItem, - MenuItems, - Transition -} from "@headlessui/react"; +import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; import { ChevronDownIcon } from "@heroicons/react/solid"; -import { Fragment, useState } from "react"; +import { useMemo, useState } from "react"; import { useGetPlaybooksToRun } from "../../../../api/query-hooks/playbooks"; import { RunnablePlaybook } from "../../../../api/types/playbooks"; import PlaybookSpecIcon from "../../Settings/PlaybookSpecIcon"; @@ -43,47 +38,79 @@ export default function PlaybooksDropdownMenu({ config_id }); + const playbooksGroupedByCategory = useMemo( + () => + playbooks?.reduce( + (acc, playbook) => { + const category = playbook.spec?.category || "Uncategorized"; + if (!acc[category]) { + acc[category] = []; + } + acc[category].push(playbook); + return acc; + }, + {} as Record< + string, + (RunnablePlaybook & { + spec: any; + })[] + > + ), + [playbooks] + ); + if (error || playbooks?.length === 0 || isLoading) { return null; } return ( -
- - + <> + + Playbooks - - - {/* @ts-ignore */} - + + - - {playbooks?.map((playbook) => ( - setSelectedPlaybookSpec(playbook)} - key={playbook.id} - > - - - ))} - - - + {playbooksGroupedByCategory && ( + <> + {Object.entries(playbooksGroupedByCategory).map( + ([category, playbooks]) => ( + + {category} +
+ } + iconClassName="h-4 w-4" + isCollapsed + > +
+ {playbooks.map((playbook) => ( +
{ + setSelectedPlaybookSpec(playbook); + }} + className={`flex cursor-pointer flex-col justify-between gap-1 px-4 py-2 text-sm`} + > + +
+ ))} +
+ + ) + )} + + )} + + {selectedPlaybookSpec && ( )} - +
); } diff --git a/src/components/Playbooks/Runs/Submit/__tests__/SelectPlaybookToRun.unit.test.tsx b/src/components/Playbooks/Runs/Submit/__tests__/SelectPlaybookToRun.unit.test.tsx deleted file mode 100644 index 005353f25..000000000 --- a/src/components/Playbooks/Runs/Submit/__tests__/SelectPlaybookToRun.unit.test.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { AuthContext } from "@flanksource-ui/context"; -import { UserAccessStateContextProvider } from "@flanksource-ui/context/UserAccessContext/UserAccessContext"; -import { render, screen, waitFor } from "@flanksource-ui/test-utils"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import userEvent from "@testing-library/user-event"; -import { rest } from "msw"; -import { setupServer } from "msw/node"; -import { RunnablePlaybook } from "../../../../../api/types/playbooks"; -import PlaybooksDropdownMenu from "../PlaybooksDropdownMenu"; - -const playbooks: (RunnablePlaybook & { - spec: any; -})[] = [ - { - id: "1", - name: "Playbook 1", - created_at: "2021-09-01T00:00:00Z", - source: "UI", - parameters: [], - updated_at: "2021-09-01T00:00:00Z", - spec: { - icon: "playbook.svg" - } - }, - { - id: "2", - name: "Playbook 2", - created_at: "2021-09-01T00:00:00Z", - source: "UI", - parameters: [], - updated_at: "2021-09-01T00:00:00Z", - spec: { - icon: "playbook.svg" - } - } -]; - -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)); - }), - rest.get("/api/db/playbooks", (req, res, ctx) => { - return res(ctx.json(playbooks)); - }), - rest.get("/api/db/people_roles", (req, res, ctx) => { - return res( - ctx.json([ - { - id: "b149b5ee-db1c-4c0c-9711-98d06f1f1ce7", - name: "Admin", - email: "admin@local", - roles: ["admin"] - } - ]) - ); - }) -); - -const queryClient = new QueryClient(); - -beforeAll(() => server.listen()); -afterEach(() => server.resetHandlers()); -afterAll(() => server.close()); - -describe("SelectPlaybookToRun", () => { - it("should render dropdown list with playbooks", async () => { - render( - - - - - - - - ); - - const playbooksButton = await screen.findByRole( - "button", - { - name: /playbooks/i - }, - { - timeout: 3000 - } - ); - - 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( - - - - - - - - ); - - 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(); - }); - }); -});