Upgrade plugin system: lifecycle hooks, dependency checks, enabled gating
Local Build and Deploy / deploy (push) Failing after 48s
Local Build and Deploy / deploy (push) Failing after 48s
- PluginDefinition: add license, homepage, dependencies, onEnable, onDisable - store.ts: call onEnable/onDisable hooks on toggle, fix circular dependency - admin-nav.ts: async allEnabledHubs/allEnabledNavGroups filters by enabled state - layout.tsx: pass filtered nav groups to sidebar - client.tsx: show Disabled/Missing deps badges, dim disabled plugins - API: return dependency info, lifecycle hooks on PATCH - Radio depends on API, Events depends on Notifications
This commit is contained in:
1 parent
d56c48a0b3
commit
127d5dcfa6
9 files changed
+197
-78
No files matched your search
@@ -7,17 +7,39 @@ import type { PluginDefinition, PluginConfigField } from "@/lib/plugins/types";
|
||||
interface PluginWithState extends PluginDefinition {
|
||||
enabled: boolean;
|
||||
config: Record<string, string | boolean>;
|
||||
missingDeps: string[];
|
||||
allDepIds: string[];
|
||||
}
|
||||
|
||||
export function PluginsClient() {
|
||||
const [plugins, setPlugins] = useState<PluginWithState[]>([]);
|
||||
const [editing, setEditing] = useState<string | null>(null);
|
||||
const [toggling, setToggling] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/admin/plugins");
|
||||
const data = await res.json();
|
||||
setPlugins(data.plugins ?? []);
|
||||
const allDepIds = new Set<string>();
|
||||
for (const p of data.plugins ?? []) {
|
||||
if (p.dependencies) {
|
||||
for (const d of p.dependencies) allDepIds.add(d);
|
||||
}
|
||||
}
|
||||
const items: PluginWithState[] = (data.plugins ?? []).map(
|
||||
(p: PluginDefinition & { enabled: boolean; config: Record<string, string | boolean> }) => {
|
||||
const missing =
|
||||
p.dependencies?.filter(
|
||||
(d: string) => !allDepIds.has(d),
|
||||
) ?? [];
|
||||
return {
|
||||
...p,
|
||||
missingDeps: missing,
|
||||
allDepIds: Array.from(allDepIds),
|
||||
};
|
||||
},
|
||||
);
|
||||
setPlugins(items);
|
||||
} catch {}
|
||||
}, []);
|
||||
|
||||
@@ -26,6 +48,7 @@ export function PluginsClient() {
|
||||
}, [load]);
|
||||
|
||||
const toggleEnabled = async (id: string, current: boolean) => {
|
||||
setToggling(id);
|
||||
try {
|
||||
await fetch(`/api/admin/plugins`, {
|
||||
method: "PATCH",
|
||||
@@ -34,6 +57,7 @@ export function PluginsClient() {
|
||||
});
|
||||
load();
|
||||
} catch {}
|
||||
setToggling(null);
|
||||
};
|
||||
|
||||
const saveConfig = async (id: string, config: Record<string, string | boolean>) => {
|
||||
@@ -48,6 +72,9 @@ export function PluginsClient() {
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const hasDependencyIssues = (p: PluginWithState) =>
|
||||
p.dependencies?.length && p.missingDeps.length > 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{plugins.length === 0 && (
|
||||
@@ -58,16 +85,26 @@ export function PluginsClient() {
|
||||
{plugins.map((plugin) => (
|
||||
<div
|
||||
key={plugin.id}
|
||||
className="admin-card p-4 flex items-start justify-between gap-4"
|
||||
className={`admin-card p-4 flex items-start justify-between gap-4 ${!plugin.enabled ? "opacity-60" : ""}`}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="text-sm font-semibold text-[var(--admin-text)]">
|
||||
{plugin.name}
|
||||
</h3>
|
||||
<span className="text-[0.55rem] font-bold px-1.5 py-0.5 rounded-full bg-[var(--admin-accent)]/12 text-[var(--admin-accent)]">
|
||||
v{plugin.version}
|
||||
</span>
|
||||
{!plugin.enabled && (
|
||||
<span className="text-[0.55rem] font-bold px-1.5 py-0.5 rounded-full" style={{ backgroundColor: "rgba(245,158,11,0.15)", color: "var(--admin-text)" }}>
|
||||
Disabled
|
||||
</span>
|
||||
)}
|
||||
{hasDependencyIssues(plugin) && (
|
||||
<span className="text-[0.55rem] font-bold px-1.5 py-0.5 rounded-full" style={{ backgroundColor: "rgba(239,68,68,0.15)", color: "var(--admin-text)" }} title={`Missing: ${plugin.missingDeps.join(", ")}`}>
|
||||
Missing deps
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mt-1">
|
||||
{plugin.description}
|
||||
@@ -75,6 +112,10 @@ export function PluginsClient() {
|
||||
<div className="flex items-center gap-3 mt-2 text-[0.6rem] text-[var(--admin-text-muted)]">
|
||||
<span>ID: {plugin.id}</span>
|
||||
{plugin.author && <span>Author: {plugin.author}</span>}
|
||||
{plugin.license && <span>License: {plugin.license}</span>}
|
||||
{plugin.dependencies && plugin.dependencies.length > 0 && (
|
||||
<span>Deps: {plugin.dependencies.join(", ")}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{plugin.configFields && plugin.configFields.length > 0 && (
|
||||
@@ -105,9 +146,12 @@ export function PluginsClient() {
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={plugin.enabled}
|
||||
disabled={toggling === plugin.id}
|
||||
onChange={() => toggleEnabled(plugin.id, plugin.enabled)}
|
||||
/>
|
||||
<div className="w-9 h-5 bg-[var(--admin-text-muted)]/30 rounded-full peer peer-checked:bg-[var(--admin-accent)] after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full" />
|
||||
<div
|
||||
className={`w-9 h-5 rounded-full peer peer-checked:bg-[var(--admin-accent)] after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full ${toggling === plugin.id ? "bg-[var(--admin-accent)]/50" : "bg-[var(--admin-text-muted)]/30"}`}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user