diff --git a/src/api/services/plugins.ts b/src/api/services/plugins.ts new file mode 100644 index 000000000..72ddb81c3 --- /dev/null +++ b/src/api/services/plugins.ts @@ -0,0 +1,24 @@ +import axios from "axios"; +import { ScrapePlugin } from "../types/plugins"; + +const API_BASE = "/api/plugins"; + +export async function getScrapePlugins(): Promise { + const { data } = await axios.get(API_BASE); + return data; +} + +export async function createScrapePlugin(plugin: Partial): Promise { + const { data } = await axios.post(API_BASE, plugin); + return data; +} + +export async function updateScrapePlugin(id: string, plugin: Partial): Promise { + const { data } = await axios.put(`${API_BASE}/${id}`, plugin); + return data; +} + +export async function deleteScrapePlugin(id: string): Promise<{ id: string }> { + const { data } = await axios.delete<{ id: string }>(`${API_BASE}/${id}`); + return data; +} \ No newline at end of file diff --git a/src/api/types/plugins.ts b/src/api/types/plugins.ts new file mode 100644 index 000000000..0afef2931 --- /dev/null +++ b/src/api/types/plugins.ts @@ -0,0 +1,11 @@ +export interface ScrapePlugin { + id: string; + name: string; + namespace: string; + spec?: any; // JSON holding spec object + source?: string; + created_by?: string; + created_at: string; + updated_at?: string | null; + deleted_at?: string | null; +} \ No newline at end of file diff --git a/src/components/Configs/ConfigPageTabs.tsx b/src/components/Configs/ConfigPageTabs.tsx index f8c82dd00..6e67f126e 100644 --- a/src/components/Configs/ConfigPageTabs.tsx +++ b/src/components/Configs/ConfigPageTabs.tsx @@ -40,6 +40,11 @@ export default function ConfigPageTabs({ label: "Scrapers", key: "Scrapers", path: `/catalog/scrapers` + }, + { + label: "Plugins", + key: "Plugins", + path: `/catalog/plugins` } ]; }, [type]); @@ -51,4 +56,4 @@ export default function ConfigPageTabs({ ); -} +} \ No newline at end of file diff --git a/src/components/Configs/Plugins/PluginsPage.tsx b/src/components/Configs/Plugins/PluginsPage.tsx new file mode 100644 index 000000000..521672143 --- /dev/null +++ b/src/components/Configs/Plugins/PluginsPage.tsx @@ -0,0 +1,231 @@ +import { useCallback, useState, ReactNode } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { getScrapePlugins, createScrapePlugin, updateScrapePlugin, deleteScrapePlugin } from "../../../api/services/plugins"; +import { ScrapePlugin } from "../../../api/types/plugins"; +import { Button } from "@flanksource-ui/ui/Buttons/Button"; + +// Simple Modal implementation since Modal import failed. +// If you have a Modal component available elsewhere, substitute here. +interface SimpleModalProps { + open: boolean; + onClose: () => void; + title: ReactNode; + children: ReactNode; +} +function SimpleModal({ open, onClose, title, children }: SimpleModalProps) { + if (!open) return null; + return ( +
+
+ +

{title}

+ {children} +
+
+ ); +} + +// NOTE: Fallback DataTable implementation (uses simple table for now) +function SimpleDataTable({ columns, data, isLoading }: { columns: any[]; data: any[]; isLoading: boolean }) { + return ( +
+ + + + {columns.map((col, idx) => ( + + ))} + + + + {isLoading ? ( + + ) : ( + data.length === 0 ? ( + + ) : ( + data.map((row, ridx) => ( + + {columns.map((col, cidx) => + + )} + + )) + ) + )} + +
{col.header}
Loading...
No plugins found.
+ {col.cell ? col.cell({ row }) : row[col.accessorKey]} +
+
+ ); +} + +const columns = [ + { header: "Name", accessorKey: "name" }, + { header: "Namespace", accessorKey: "namespace" }, + { header: "Source", accessorKey: "source" }, + { header: "Created By", accessorKey: "created_by" }, + { header: "Created At", accessorKey: "created_at" }, +]; + +type EditablePlugin = Partial & { id?: string }; + +const defaultPlugin: EditablePlugin = { + name: "", + namespace: "", + spec: {}, + source: "", +}; + +const PluginsPage = () => { + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + const [editing, setEditing] = useState(null); + + const { data = [], isLoading } = useQuery({ + queryKey: ["plugins"], + queryFn: getScrapePlugins, + }); + + const mutationCreate = useMutation({ + mutationFn: createScrapePlugin, + onSuccess: () => { + queryClient.invalidateQueries(["plugins"]); + setOpen(false); + }, + }); + + const mutationUpdate = useMutation({ + mutationFn: ({ id, ...update }: EditablePlugin) => updateScrapePlugin(id!, update), + onSuccess: () => { + queryClient.invalidateQueries(["plugins"]); + setOpen(false); + setEditing(null); + }, + }); + + const mutationDelete = useMutation({ + mutationFn: (id: string) => deleteScrapePlugin(id), + onSuccess: () => queryClient.invalidateQueries(["plugins"]), + }); + + const handleEdit = useCallback((plugin: ScrapePlugin) => { + setEditing(plugin); + setOpen(true); + }, []); + + const handleDelete = useCallback((plugin: ScrapePlugin) => { + if (window.confirm(`Delete plugin "${plugin.name}"?`)) { + mutationDelete.mutate(plugin.id); + } + }, [mutationDelete]); + + const handleSave = (plugin: EditablePlugin) => { + if (plugin.id) { + mutationUpdate.mutate(plugin as EditablePlugin); + } else { + mutationCreate.mutate(plugin as EditablePlugin); + } + }; + + return ( +
+
+

Scrape Plugins

+ +
+ + ( +
+ + +
+ ), + } + ]} + data={data} + /> + + setOpen(false)} title={editing ? "Edit Scrape Plugin" : "Add Scrape Plugin"}> + setOpen(false)} + /> + +
+ ); +}; + +type PluginFormProps = { + initial: EditablePlugin; + onSave: (plugin: EditablePlugin) => void; + onCancel: () => void; +}; + +function PluginForm({ initial, onSave, onCancel }: PluginFormProps) { + const [plugin, setPlugin] = useState(initial); + + // If initial changes (edit -> add), update state + React.useEffect(() => { setPlugin(initial); }, [initial]); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setPlugin({ ...plugin, [name]: value }); + }; + + // For advanced spec editing, consider a code editor in the future + return ( +
{ e.preventDefault(); onSave(plugin); }} + > + + + +