generated from Dicken/dickendock
Commit 8: Plugin-System (Scanner erweitern, Geräte importieren, Icons)
This commit is contained in:
17
apps/frontend/src/hooks/usePlugins.ts
Normal file
17
apps/frontend/src/hooks/usePlugins.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { PluginInfo } from "@launchpad/shared";
|
||||
|
||||
async function fetchPlugins(): Promise<PluginInfo[]> {
|
||||
const res = await fetch("/api/plugins");
|
||||
if (!res.ok) {
|
||||
throw new Error(`Plugins konnten nicht geladen werden (HTTP ${res.status})`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export function usePlugins() {
|
||||
return useQuery({
|
||||
queryKey: ["plugins"],
|
||||
queryFn: fetchPlugins,
|
||||
});
|
||||
}
|
||||
@@ -1,18 +1,109 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@launchpad/ui";
|
||||
import type { PluginInfo } from "@launchpad/shared";
|
||||
import { usePlugins } from "../../hooks/usePlugins.js";
|
||||
import { AdminPageHeader } from "./AdminPageHeader.js";
|
||||
|
||||
export function PluginsPage() {
|
||||
const CAPABILITY_LABELS: Record<string, string> = {
|
||||
scanner: "Erweitert die Softwareerkennung",
|
||||
"device-import": "Kann Geräte importieren",
|
||||
};
|
||||
|
||||
async function importFromPlugin(name: string) {
|
||||
const res = await fetch(`/api/plugins/${name}/import`, { method: "POST" });
|
||||
const body = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new Error(body.detail ?? body.error ?? `Import fehlgeschlagen (HTTP ${res.status})`);
|
||||
}
|
||||
return body as { imported: number };
|
||||
}
|
||||
|
||||
function PluginCard({ plugin }: { plugin: PluginInfo }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
const importMutation = useMutation({
|
||||
mutationFn: () => importFromPlugin(plugin.name),
|
||||
onSuccess: (result) => {
|
||||
setMessage(`${result.imported} Gerät(e) importiert.`);
|
||||
queryClient.invalidateQueries({ queryKey: ["devices"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["logs"] });
|
||||
},
|
||||
onError: (err: Error) => setMessage(err.message),
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminPageHeader title="Plugins" />
|
||||
<div className="rounded-2xl border border-dashed border-black/15 p-8 text-center dark:border-white/15">
|
||||
<p className="text-black/60 dark:text-white/60">
|
||||
Das Plugin-System (Scanner registrieren, Geräte importieren, Menüs erweitern,
|
||||
Icons bereitstellen) ist noch nicht gebaut.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-black/40 dark:text-white/40">
|
||||
Geplant als eigener Commit – siehe <code className="rounded bg-black/5 px-1 dark:bg-white/10">docs/ROADMAP.md</code>.
|
||||
</p>
|
||||
<div className="rounded-2xl border border-black/10 p-5 dark:border-white/10">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="font-medium text-black dark:text-white">
|
||||
{plugin.name} <span className="text-black/40 dark:text-white/40">v{plugin.version}</span>
|
||||
</h2>
|
||||
{plugin.description ? (
|
||||
<p className="mt-1 text-sm text-black/50 dark:text-white/50">{plugin.description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{plugin.capabilities.length === 0 ? (
|
||||
<span className="text-xs text-black/30 dark:text-white/30">Keine Capabilities</span>
|
||||
) : (
|
||||
plugin.capabilities.map((cap) => (
|
||||
<span
|
||||
key={cap}
|
||||
className="rounded-full bg-black/5 px-2.5 py-1 text-xs text-black/60 dark:bg-white/10 dark:text-white/60"
|
||||
>
|
||||
{CAPABILITY_LABELS[cap] ?? cap}
|
||||
</span>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{plugin.capabilities.includes("device-import") ? (
|
||||
<div className="mt-4">
|
||||
<Button size="sm" variant="primary" onClick={() => importMutation.mutate()} disabled={importMutation.isPending}>
|
||||
{importMutation.isPending ? "Importiere …" : "Jetzt importieren"}
|
||||
</Button>
|
||||
{message ? (
|
||||
<p className="mt-2 text-xs text-black/50 dark:text-white/50">{message}</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PluginsPage() {
|
||||
const { data: plugins, isLoading, isError } = usePlugins();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminPageHeader
|
||||
title="Plugins"
|
||||
description="Plugins liegen als Ordner unter apps/backend/plugins/ und werden beim Start des Backends geladen – kein Rebuild nötig, nur ein Container-Neustart."
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<p className="text-sm text-black/40 dark:text-white/40">Lade Plugins …</p>
|
||||
) : isError ? (
|
||||
<p className="text-sm text-red-500">Plugins konnten nicht geladen werden.</p>
|
||||
) : plugins && plugins.length > 0 ? (
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{plugins.map((plugin) => (
|
||||
<PluginCard key={plugin.name} plugin={plugin} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-2xl border border-dashed border-black/15 p-8 text-center dark:border-white/15">
|
||||
<p className="text-black/60 dark:text-white/60">Keine Plugins geladen.</p>
|
||||
<p className="mt-2 text-sm text-black/40 dark:text-white/40">
|
||||
Siehe <code className="rounded bg-black/5 px-1 dark:bg-white/10">apps/backend/plugins/README.md</code>{" "}
|
||||
für eine Anleitung, wie du ein eigenes Plugin schreibst.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user