Upgrade plugin system: lifecycle hooks, dependency checks, enabled gating
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:
openhands committed 2026-07-18 14:38:56 +02:00
1 parent d56c48a0b3
commit 127d5dcfa6
9 files changed
+197 -78

No files matched your search

+54 -27
View File
@@ -18,6 +18,7 @@ import {
Wrench,
} from "lucide-react";
import { pluginRegistry } from "@/lib/plugins";
import { getAllPluginStates } from "@/lib/plugins/store";
import "@/lib/plugins/loader";
export interface AdminHubTab {
@@ -346,25 +347,33 @@ export const ADMIN_HUBS: AdminHubDefinition[] = [
},
];
export function allHubs(): AdminHubDefinition[] {
const pluginHubs: AdminHubDefinition[] = pluginRegistry.getHubs().map((h) => ({
id: h.id,
titleKey: h.titleKey,
subtitleKey: h.subtitleKey,
icon: h.icon,
prefixes: h.prefixes,
tabs: h.tabs.map((t) => ({
href: t.href,
labelKey: t.labelKey,
match: t.match,
group: t.group,
})),
}));
export function allHubs(enabled?: Record<string, boolean>): AdminHubDefinition[] {
const allReg = pluginRegistry.getHubs();
const pluginHubs: AdminHubDefinition[] = [];
for (const h of allReg) {
const hubPlugin = pluginRegistry
.getAll()
.find((p) => (p.hubs ?? []).some((ph) => ph.id === h.id));
if (hubPlugin && enabled && enabled[hubPlugin.id] === false) continue;
pluginHubs.push({
id: h.id,
titleKey: h.titleKey,
subtitleKey: h.subtitleKey,
icon: h.icon,
prefixes: h.prefixes,
tabs: h.tabs.map((t) => ({
href: t.href,
labelKey: t.labelKey,
match: t.match,
group: t.group,
})),
});
}
return [...ADMIN_HUBS, ...pluginHubs];
}
export function findAdminHub(pathname: string): AdminHubDefinition | null {
const hubs = allHubs();
export function findAdminHub(pathname: string, enabled?: Record<string, boolean>): AdminHubDefinition | null {
const hubs = allHubs(enabled);
return (
hubs.find((hub) =>
hub.prefixes.some((p) => pathname === p || pathname.startsWith(`${p}/`)),
@@ -372,17 +381,25 @@ export function findAdminHub(pathname: string): AdminHubDefinition | null {
);
}
export function allNavGroups(): AdminNavGroup[] {
const pluginGroups: AdminNavGroup[] = pluginRegistry.getNavGroups().map((g) => ({
labelKey: g.labelKey,
icon: g.icon,
items: g.items.map((item) => ({
href: item.href,
labelKey: item.labelKey,
icon: item.icon,
matchPrefixes: item.matchPrefixes,
})),
}));
export function allNavGroups(enabled?: Record<string, boolean>): AdminNavGroup[] {
const allReg = pluginRegistry.getNavGroups();
const pluginGroups: AdminNavGroup[] = [];
for (const g of allReg) {
const groupPlugin = pluginRegistry
.getAll()
.find((p) => (p.navGroups ?? []).some((pg) => pg.labelKey === g.labelKey));
if (groupPlugin && enabled && enabled[groupPlugin.id] === false) continue;
pluginGroups.push({
labelKey: g.labelKey,
icon: g.icon,
items: g.items.map((item) => ({
href: item.href,
labelKey: item.labelKey,
icon: item.icon,
matchPrefixes: item.matchPrefixes,
})),
});
}
if (pluginGroups.length === 0) return ADMIN_NAV_GROUPS;
return [
...ADMIN_NAV_GROUPS,
@@ -394,6 +411,16 @@ export function allNavGroups(): AdminNavGroup[] {
];
}
export async function allEnabledHubs(): Promise<AdminHubDefinition[]> {
const states = await getAllPluginStates();
return allHubs(states.enabled);
}
export async function allEnabledNavGroups(): Promise<AdminNavGroup[]> {
const states = await getAllPluginStates();
return allNavGroups(states.enabled);
}
function hubPrefixes(id: string): string[] {
return ADMIN_HUBS.find((h) => h.id === id)?.prefixes ?? [];
}
+35 -27
View File
@@ -10,38 +10,16 @@ interface PluginStoreData {
configs: Record<string, Record<string, string | boolean>>;
}
function defaultStore(): PluginStoreData {
const plugins = pluginRegistry.getAll();
const enabled: Record<string, boolean> = {};
const configs: Record<string, Record<string, string | boolean>> = {};
for (const p of plugins) {
enabled[p.id] = p.enabled ?? true;
if (p.configFields) {
const cfg: Record<string, string | boolean> = {};
for (const field of p.configFields) {
cfg[field.key] = field.default;
}
configs[p.id] = cfg;
}
}
return { enabled, configs };
}
async function load(): Promise<PluginStoreData> {
try {
const row = await prisma.websiteSetting.findUnique({
where: { key: STORE_KEY },
});
if (row?.value) {
const parsed = JSON.parse(row.value) as PluginStoreData;
const defs = defaultStore();
return {
enabled: { ...defs.enabled, ...parsed.enabled },
configs: { ...defs.configs, ...parsed.configs },
};
return JSON.parse(row.value) as PluginStoreData;
}
} catch {}
return defaultStore();
return { enabled: {}, configs: {} };
}
async function save(data: PluginStoreData): Promise<void> {
@@ -56,7 +34,12 @@ async function save(data: PluginStoreData): Promise<void> {
export async function isPluginEnabled(pluginId: string): Promise<boolean> {
const data = await load();
return data.enabled[pluginId] ?? true;
const saved = data.enabled[pluginId];
if (saved === undefined) {
const def = pluginRegistry.get(pluginId);
return def?.enabled ?? true;
}
return saved;
}
export async function setPluginEnabled(
@@ -64,15 +47,33 @@ export async function setPluginEnabled(
enabled: boolean,
): Promise<void> {
const data = await load();
const prev = data.enabled[pluginId];
if (prev === enabled) return;
data.enabled[pluginId] = enabled;
await save(data);
const plugin = pluginRegistry.get(pluginId);
if (enabled) {
await plugin?.onEnable?.();
} else {
await plugin?.onDisable?.();
}
}
export async function getPluginConfig(
pluginId: string,
): Promise<Record<string, string | boolean>> {
const data = await load();
return data.configs[pluginId] ?? {};
const saved = data.configs[pluginId] ?? {};
const plugin = pluginRegistry.get(pluginId);
if (!plugin?.configFields) return saved;
const defaults: Record<string, string | boolean> = {};
for (const f of plugin.configFields) {
defaults[f.key] = f.default;
}
return { ...defaults, ...saved };
}
export async function setPluginConfig(
@@ -88,5 +89,12 @@ export async function getAllPluginStates(): Promise<{
enabled: Record<string, boolean>;
configs: Record<string, Record<string, string | boolean>>;
}> {
return load();
const data = await load();
const out = data;
for (const p of pluginRegistry.getAll()) {
if (out.enabled[p.id] === undefined) {
out.enabled[p.id] = p.enabled ?? true;
}
}
return out;
}
+5
View File
@@ -66,7 +66,10 @@ export interface PluginDefinition {
description: string;
version: string;
author?: string;
license?: string;
homepage?: string;
enabled?: boolean;
dependencies?: string[];
hubs?: PluginHub[];
navGroups?: PluginNavGroup[];
permissions?: PluginPermission[];
@@ -77,4 +80,6 @@ export interface PluginDefinition {
onLoad?: () => void | Promise<void>;
onUnload?: () => void | Promise<void>;
onEnable?: () => void | Promise<void>;
onDisable?: () => void | Promise<void>;
}