Skip to content

Commit

Permalink
Merge pull request #1357 from flanksource/1352-settings-playbooks
Browse files Browse the repository at this point in the history
feat: add settings page to manage playbooks specs
  • Loading branch information
moshloop authored Sep 7, 2023
2 parents a146e71 + 1079687 commit 4d19d4e
Show file tree
Hide file tree
Showing 7 changed files with 422 additions and 1 deletion.
17 changes: 17 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { ConfigInsightsPage } from "./pages/config/ConfigInsightsList";
import { HealthPage } from "./pages/health";
import { features } from "./services/permissions/features";
import { stringSortHelper } from "./utils/common";
import { PlaybookSettingsPage } from "./pages/Settings/PlaybookSettingsPage";

export type NavigationItems = {
name: string;
Expand Down Expand Up @@ -179,6 +180,13 @@ const settingsNav: SettingsNavigationItems = {
icon: FaTasks,
featureName: features["settings.event_queue_status"],
resourceName: tables.database
},
{
name: "Playbooks",
href: "/settings/playbooks",
icon: FaTasks,
featureName: features["settings.playbooks"],
resourceName: tables.database
}
].sort((v1, v2) => stringSortHelper(v1.name, v2.name))
};
Expand Down Expand Up @@ -289,6 +297,15 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) {
)}
/>

<Route
path="playbooks"
element={withAccessCheck(
<PlaybookSettingsPage />,
tables.database,
"read"
)}
/>

{settingsNav.submenu
.filter((v) => (v as SchemaResourceType).table)
.map((x) => {
Expand Down
30 changes: 30 additions & 0 deletions src/api/query-hooks/playbooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { UseQueryOptions, useQuery } from "@tanstack/react-query";
import { PlaybookSpec } from "../../components/Playbooks/Settings/PlaybookSpecsTable";
import { getAllPlaybooksSpecs, getPlaybookSpec } from "../services/playbooks";

export function useGetAllPlaybookSpecs(
options: UseQueryOptions<PlaybookSpec[], Error> = {}
) {
return useQuery<PlaybookSpec[], Error>(
["playbooks", "all"],
getAllPlaybooksSpecs,
{
cacheTime: 0,
staleTime: 0,
...options
}
);
}

export function useGetPlaybookSpecsDetails(id: string) {
return useQuery<Record<string, any>, Error>(
["playbooks", "settings", "specs", id],
async () => getPlaybookSpec(id),
{
enabled: !!id,
cacheTime: 0,
staleTime: 0,
keepPreviousData: false
}
);
}
44 changes: 44 additions & 0 deletions src/api/services/playbooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {
NewPlaybookSpec,
PlaybookSpec,
UpdatePlaybookSpec
} from "../../components/Playbooks/Settings/PlaybookSpecsTable";
import { AVATAR_INFO } from "../../constants";
import { IncidentCommander } from "../axios";

export async function getAllPlaybooksSpecs() {
const res = await IncidentCommander.get<PlaybookSpec[] | null>(
`/playbooks?select=*,created_by(${AVATAR_INFO})&deleted_at=is.null`
);
return res.data ?? [];
}

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

export async function createPlaybookSpec(spec: NewPlaybookSpec) {
const res = await IncidentCommander.post<PlaybookSpec>("/playbooks", spec);
return res.data;
}

export async function updatePlaybookSpec(spec: UpdatePlaybookSpec) {
const res = await IncidentCommander.patch<PlaybookSpec>(
`/playbooks?id=eq.${spec.id}`,
spec
);
return res.data;
}

export async function deletePlaybookSpec(id: string) {
const res = await IncidentCommander.patch<PlaybookSpec>(
`/playbooks?id=eq.${id}`,
{
deleted_at: "now()"
}
);
return res.data;
}
159 changes: 159 additions & 0 deletions src/components/Playbooks/Settings/PlaybookSpecsForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { useMutation } from "@tanstack/react-query";
import clsx from "clsx";
import { Form, Formik } from "formik";
import { FaTrash } from "react-icons/fa";
import {
createPlaybookSpec,
deletePlaybookSpec,
updatePlaybookSpec
} from "../../../api/services/playbooks";
import { useUser } from "../../../context";
import { Button } from "../../Button";
import { FormikCodeEditor } from "../../Forms/Formik/FormikCodeEditor";
import FormikTextInput from "../../Forms/Formik/FormikTextInput";
import { Modal } from "../../Modal";
import { toastSuccess } from "../../Toast/toast";
import {
NewPlaybookSpec,
PlaybookSpec,
UpdatePlaybookSpec
} from "./PlaybookSpecsTable";

type PlaybookSpecsFormProps = {
playbook?: PlaybookSpec;
isOpen: boolean;
onClose: () => void;
refresh?: () => void;
};

export default function PlaybookSpecsForm({
playbook,
isOpen,
onClose,
refresh = () => {}
}: PlaybookSpecsFormProps) {
const { user } = useUser();

const { mutate: createPlaybook } = useMutation({
mutationFn: async (payload: NewPlaybookSpec) => {
const res = await createPlaybookSpec({
...payload,
created_by: user?.id
});
return res;
},
onSuccess: () => {
toastSuccess("Playbook Spec created successfully");
refresh();
onClose();
}
});

const { mutate: updatePlaybook } = useMutation({
mutationFn: async ({ id, name, source, spec }: PlaybookSpec) => {
// let's avoid updating fields that are not editable
const newPayload: UpdatePlaybookSpec = {
id,
name,
source,
spec
};
const res = await updatePlaybookSpec(newPayload);
return res;
},
onSuccess: () => {
toastSuccess("Playbook Spec updated successfully");
refresh();
onClose();
},
onError: (err: Error) => {
toastSuccess("Unable to update playbook spec: " + err.message);
}
});

const { mutate: deletePlaybook, isLoading: isDeleting } = useMutation({
mutationFn: async (id: string) => {
const res = await deletePlaybookSpec(id);
return res;
},
onSuccess: () => {
toastSuccess("Playbook Spec updated successfully");
onClose();
},
onError: (err: Error) => {
toastSuccess("Unable to delete playbook spec: " + err.message);
}
});

return (
<Modal
title={
playbook
? `Edit Playbook Spec: ${playbook.name}`
: "Create Playbook Spec"
}
onClose={onClose}
open={isOpen}
bodyClass="flex flex-col w-full flex-1 h-full overflow-y-auto"
>
<Formik
initialValues={
playbook || {
name: "",
source: "UI",
spec: "",
created_by: user?.id
}
}
onSubmit={(value) => {
if (playbook?.id) {
updatePlaybook(value as PlaybookSpec);
} else {
createPlaybook(value as NewPlaybookSpec);
}
}}
>
{({ handleSubmit }) => (
<Form
className="flex flex-col flex-1 overflow-y-auto"
onSubmit={handleSubmit}
>
<div className={clsx("flex flex-col h-full my-2 overflow-y-auto")}>
<div className={clsx("flex flex-col px-2 mb-2 overflow-y-auto")}>
<div className="flex flex-col space-y-4 overflow-y-auto p-4">
<FormikTextInput name="name" label="Name" required />
<FormikCodeEditor
fieldName="spec"
label="Spec"
format="yaml"
className="flex flex-col h-[400px]"
required
/>
</div>
</div>
</div>
<div className="flex items-center justify-between py-4 px-5 bg-gray-100">
{playbook?.id && (
<Button
type="button"
text={isDeleting ? "Deleting..." : "Delete"}
icon={<FaTrash />}
onClick={() => {
deletePlaybook(playbook.id);
}}
className="btn-danger"
/>
)}

<Button
type="submit"
text={playbook?.id ? "Update" : "Save"}
className="btn-primary"
/>
</div>
</Form>
)}
</Formik>
</Modal>
);
}
92 changes: 92 additions & 0 deletions src/components/Playbooks/Settings/PlaybookSpecsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { CellContext, ColumnDef } from "@tanstack/react-table";
import { User } from "../../../api/services/users";
import { Avatar } from "../../Avatar";
import { DateCell } from "../../ConfigViewer/columns";
import { DataTable } from "../../DataTable";

export type PlaybookSpec = {
id: string;
name: string;
created_by?: User;
spec: any;
source: "KubernetesCRD" | "ConfigFile" | "UI";
created_at: string;
updated_at: string;
deleted_at?: string;
};

export type NewPlaybookSpec = Omit<
PlaybookSpec,
"id" | "created_at" | "updated_at" | "deleted_at" | "created_by"
> & {
created_by?: string;
};

export type UpdatePlaybookSpec = Omit<
PlaybookSpec,
"created_at" | "updated_at" | "deleted_at" | "created_by"
>;

const playbookSpecsTableColumns: ColumnDef<PlaybookSpec>[] = [
{
header: "Name",
accessorKey: "name",
id: "name",
size: 150
},
{
header: "Source Config",
accessorKey: "source",
id: "source",
size: 150
},
{
header: "Created By",
accessorKey: "created_by",
cell: ({ getValue }: CellContext<PlaybookSpec, any>) => {
const user = getValue<User>();
return <Avatar user={user} circular />;
}
},
{
header: "Created At",
accessorKey: "created_at",
cell: DateCell,
sortingFn: "datetime"
},
{
header: "Updated At",
accessorKey: "updated_at",
cell: DateCell,
sortingFn: "datetime"
}
];

type Props = {
data: PlaybookSpec[];
isLoading?: boolean;
onRowClick?: (data: PlaybookSpec) => void;
} & Omit<React.HTMLProps<HTMLDivElement>, "data">;

export default function PlaybookSpecsTable({
data,
isLoading,
className,
onRowClick,
...rest
}: Props) {
return (
<div className="flex flex-col h-full overflow-y-hidden" {...rest}>
<DataTable
stickyHead
columns={playbookSpecsTableColumns}
data={data}
tableStyle={{ borderSpacing: "0" }}
isLoading={isLoading}
preferencesKey="connections-list"
savePreferences={false}
handleRowClick={(row) => onRowClick?.(row.original)}
/>
</div>
);
}
Loading

0 comments on commit 4d19d4e

Please sign in to comment.