mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
refactor(plugins): enhance plugin configuration handling and improve error messages
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState, type ChangeEvent } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -21,7 +21,12 @@ import { useHeaderRefresh } from '@/hooks/useHeaderRefresh';
|
||||
import { pluginsApi } from '@/services/api';
|
||||
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
||||
import { getErrorMessage, isRecord } from '@/utils/helpers';
|
||||
import type { PluginConfigField, PluginListEntry, PluginListResponse } from '@/types';
|
||||
import type {
|
||||
PluginConfigField,
|
||||
PluginConfigObject,
|
||||
PluginListEntry,
|
||||
PluginListResponse,
|
||||
} from '@/types';
|
||||
import { getPluginTitle, resolvePluginAssetURL } from './pluginResources';
|
||||
import styles from './PluginsPage.module.scss';
|
||||
|
||||
@@ -45,29 +50,11 @@ function PluginCardLogo({ src }: { src: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
const cloneRecord = (value: unknown): Record<string, unknown> =>
|
||||
isRecord(value) ? { ...value } : {};
|
||||
|
||||
const hasStatus = (error: unknown, status: number) =>
|
||||
isRecord(error) && error.status === status;
|
||||
|
||||
const normalizeFieldType = (field: PluginConfigField) => field.type.trim().toLowerCase();
|
||||
|
||||
const getPluginsConfigMap = (rawConfig: Record<string, unknown>): Record<string, unknown> => {
|
||||
const plugins = rawConfig.plugins;
|
||||
if (!isRecord(plugins)) return {};
|
||||
const configs = plugins.configs;
|
||||
return isRecord(configs) ? configs : {};
|
||||
};
|
||||
|
||||
const getPluginRawConfig = (
|
||||
rawConfig: Record<string, unknown>,
|
||||
pluginID: string
|
||||
): Record<string, unknown> => {
|
||||
const configs = getPluginsConfigMap(rawConfig);
|
||||
return cloneRecord(configs[pluginID]);
|
||||
};
|
||||
|
||||
const stringifyArrayItem = (value: unknown): string => {
|
||||
if (value === undefined || value === null) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
@@ -98,7 +85,7 @@ const getFieldDraftValue = (field: PluginConfigField, value: unknown): PluginDra
|
||||
|
||||
const buildDraft = (
|
||||
plugin: PluginListEntry,
|
||||
currentConfig: Record<string, unknown>
|
||||
currentConfig: PluginConfigObject
|
||||
): PluginConfigDraft => {
|
||||
const enabled = typeof currentConfig.enabled === 'boolean' ? currentConfig.enabled : plugin.enabled;
|
||||
const priority =
|
||||
@@ -146,11 +133,11 @@ const parseJSONField = (
|
||||
const buildConfigPayload = (
|
||||
draft: PluginConfigDraft,
|
||||
fields: PluginConfigField[],
|
||||
currentConfig: Record<string, unknown>,
|
||||
currentConfig: PluginConfigObject,
|
||||
t: (key: string, options?: Record<string, unknown>) => string
|
||||
) => {
|
||||
const errors: Record<string, string> = {};
|
||||
const nextConfig: Record<string, unknown> = { ...currentConfig };
|
||||
const nextConfig: PluginConfigObject = { ...currentConfig };
|
||||
const priorityText = draft.priority.trim();
|
||||
|
||||
nextConfig.enabled = draft.enabled;
|
||||
@@ -235,18 +222,19 @@ export function PluginsPage() {
|
||||
const navigate = useNavigate();
|
||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||
const apiBase = useAuthStore((state) => state.apiBase);
|
||||
const fetchConfig = useConfigStore((state) => state.fetchConfig);
|
||||
const clearConfigCache = useConfigStore((state) => state.clearCache);
|
||||
const showNotification = useNotificationStore((state) => state.showNotification);
|
||||
|
||||
const [data, setData] = useState<PluginListResponse | null>(null);
|
||||
const [rawConfig, setRawConfig] = useState<Record<string, unknown>>({});
|
||||
const [filter, setFilter] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [editingPlugin, setEditingPlugin] = useState<PluginListEntry | null>(null);
|
||||
const [editingConfig, setEditingConfig] = useState<PluginConfigObject>({});
|
||||
const [draft, setDraft] = useState<PluginConfigDraft | null>(null);
|
||||
const [mutatingID, setMutatingID] = useState('');
|
||||
const [openingConfigID, setOpeningConfigID] = useState('');
|
||||
const configRequestSeq = useRef(0);
|
||||
|
||||
const connected = connectionStatus === 'connected';
|
||||
|
||||
@@ -260,12 +248,8 @@ export function PluginsPage() {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const [plugins, config] = await Promise.all([
|
||||
pluginsApi.list(),
|
||||
fetchConfig(undefined, true).catch(() => null),
|
||||
]);
|
||||
const plugins = await pluginsApi.list();
|
||||
setData(plugins);
|
||||
setRawConfig(config?.raw ?? {});
|
||||
} catch (err: unknown) {
|
||||
setError(
|
||||
hasStatus(err, 404)
|
||||
@@ -275,7 +259,7 @@ export function PluginsPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [connected, fetchConfig, t]);
|
||||
}, [connected, t]);
|
||||
|
||||
useHeaderRefresh(loadPlugins, connected);
|
||||
|
||||
@@ -320,15 +304,48 @@ export function PluginsPage() {
|
||||
[apiBase]
|
||||
);
|
||||
|
||||
const openConfigSheet = (plugin: PluginListEntry) => {
|
||||
const currentConfig = getPluginRawConfig(rawConfig, plugin.id);
|
||||
const openConfigSheet = async (plugin: PluginListEntry) => {
|
||||
if (openingConfigID || mutatingID) return;
|
||||
|
||||
const requestSeq = configRequestSeq.current + 1;
|
||||
configRequestSeq.current = requestSeq;
|
||||
setOpeningConfigID(plugin.id);
|
||||
setEditingPlugin(plugin);
|
||||
setDraft(buildDraft(plugin, currentConfig));
|
||||
setEditingConfig({});
|
||||
setDraft(null);
|
||||
|
||||
try {
|
||||
const currentConfig = await pluginsApi.getConfig(plugin.id);
|
||||
if (configRequestSeq.current !== requestSeq) return;
|
||||
|
||||
setEditingConfig(currentConfig);
|
||||
setDraft(buildDraft(plugin, currentConfig));
|
||||
} catch (err: unknown) {
|
||||
if (configRequestSeq.current !== requestSeq) return;
|
||||
|
||||
setEditingPlugin(null);
|
||||
setEditingConfig({});
|
||||
setDraft(null);
|
||||
showNotification(
|
||||
hasStatus(err, 404)
|
||||
? t('plugin_management.config_not_found')
|
||||
: `${t('plugin_management.config_load_failed')}: ${getErrorMessage(
|
||||
err,
|
||||
t('plugin_management.config_load_failed')
|
||||
)}`,
|
||||
'error'
|
||||
);
|
||||
} finally {
|
||||
if (configRequestSeq.current === requestSeq) {
|
||||
setOpeningConfigID('');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const closeConfigSheet = () => {
|
||||
if (mutatingID) return;
|
||||
if (mutatingID || openingConfigID) return;
|
||||
setEditingPlugin(null);
|
||||
setEditingConfig({});
|
||||
setDraft(null);
|
||||
};
|
||||
|
||||
@@ -357,12 +374,11 @@ export function PluginsPage() {
|
||||
};
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
if (!editingPlugin || !draft) return;
|
||||
const currentConfig = getPluginRawConfig(rawConfig, editingPlugin.id);
|
||||
if (!editingPlugin || !draft || openingConfigID || mutatingID) return;
|
||||
const { nextConfig, errors } = buildConfigPayload(
|
||||
draft,
|
||||
editingPlugin.configFields,
|
||||
currentConfig,
|
||||
editingConfig,
|
||||
t
|
||||
);
|
||||
|
||||
@@ -378,6 +394,7 @@ export function PluginsPage() {
|
||||
clearConfigCache();
|
||||
await loadPlugins();
|
||||
setEditingPlugin(null);
|
||||
setEditingConfig({});
|
||||
setDraft(null);
|
||||
showNotification(t('plugin_management.save_success'), 'success');
|
||||
} catch (err: unknown) {
|
||||
@@ -706,7 +723,8 @@ export function PluginsPage() {
|
||||
{visiblePlugins.map((plugin) => {
|
||||
const logo = resolvePluginAsset(plugin.logo || plugin.metadata?.logo || '');
|
||||
const github = plugin.metadata?.githubRepository.trim();
|
||||
const mutating = mutatingID === plugin.id;
|
||||
const openingConfig = openingConfigID === plugin.id;
|
||||
const actionBusy = Boolean(mutatingID || openingConfigID);
|
||||
const version = plugin.metadata?.version;
|
||||
const author = plugin.metadata?.author;
|
||||
|
||||
@@ -780,14 +798,15 @@ export function PluginsPage() {
|
||||
<ToggleSwitch
|
||||
checked={plugin.enabled}
|
||||
onChange={(enabled) => handleTogglePlugin(plugin, enabled)}
|
||||
disabled={!connected || mutating}
|
||||
disabled={!connected || actionBusy}
|
||||
ariaLabel={t('plugin_management.enabled')}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => openConfigSheet(plugin)}
|
||||
disabled={!connected}
|
||||
disabled={!connected || actionBusy}
|
||||
loading={openingConfig}
|
||||
>
|
||||
<IconSettings size={14} />
|
||||
{t('plugin_management.edit_config')}
|
||||
|
||||
@@ -1105,6 +1105,8 @@
|
||||
"description": "Review discovered and registered plugins, then manage instance toggles, config fields, and resource links.",
|
||||
"refresh": "Refresh",
|
||||
"load_failed": "Failed to load plugins",
|
||||
"config_load_failed": "Failed to read plugin config",
|
||||
"config_not_found": "Unable to read plugin config because the backend could not find this plugin.",
|
||||
"unsupported_backend": "The current backend does not expose the plugin management API. Use a newer backend build that includes plugin management endpoints, then restart the service.",
|
||||
"global_status": "Global status",
|
||||
"global_enabled": "Enabled",
|
||||
|
||||
@@ -1105,6 +1105,8 @@
|
||||
"description": "查看已发现和已注册的插件,管理实例开关、配置字段和资源入口。",
|
||||
"refresh": "刷新",
|
||||
"load_failed": "加载插件失败",
|
||||
"config_load_failed": "读取插件配置失败",
|
||||
"config_not_found": "无法读取插件配置,后端未找到该插件。",
|
||||
"unsupported_backend": "当前后端未暴露插件管理 API。请使用包含插件管理接口的新后端构建,并重启服务。",
|
||||
"global_status": "全局状态",
|
||||
"global_enabled": "已启用",
|
||||
|
||||
@@ -1131,6 +1131,8 @@
|
||||
"description": "查看已發現和已註冊的插件,管理實例開關、設定欄位和資源入口。",
|
||||
"refresh": "重新整理",
|
||||
"load_failed": "載入插件失敗",
|
||||
"config_load_failed": "讀取插件設定失敗",
|
||||
"config_not_found": "無法讀取插件設定,後端未找到該插件。",
|
||||
"unsupported_backend": "目前後端未暴露插件管理 API。請使用包含插件管理介面的新版後端建置,並重新啟動服務。",
|
||||
"global_status": "全域狀態",
|
||||
"global_enabled": "已啟用",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { apiClient } from './client';
|
||||
import { isRecord } from '@/utils/helpers';
|
||||
import type {
|
||||
PluginConfigField,
|
||||
PluginConfigObject,
|
||||
PluginListEntry,
|
||||
PluginListResponse,
|
||||
PluginMetadata,
|
||||
@@ -114,6 +115,9 @@ const normalizePluginList = (value: unknown): PluginListResponse => {
|
||||
};
|
||||
};
|
||||
|
||||
const normalizePluginConfig = (value: unknown): PluginConfigObject =>
|
||||
isRecord(value) ? { ...value } : {};
|
||||
|
||||
const normalizeStoreEntry = (value: unknown): PluginStoreEntry | null => {
|
||||
if (!isRecord(value)) return null;
|
||||
const id = asString(value.id).trim();
|
||||
@@ -179,10 +183,15 @@ export const pluginsApi = {
|
||||
updateEnabled: (id: string, enabled: boolean) =>
|
||||
apiClient.patch(`/plugins/${encodeURIComponent(id)}/enabled`, { enabled }),
|
||||
|
||||
putConfig: (id: string, config: Record<string, unknown>) =>
|
||||
async getConfig(id: string): Promise<PluginConfigObject> {
|
||||
const data = await apiClient.get(`/plugins/${encodeURIComponent(id)}/config`);
|
||||
return normalizePluginConfig(data);
|
||||
},
|
||||
|
||||
putConfig: (id: string, config: PluginConfigObject) =>
|
||||
apiClient.put(`/plugins/${encodeURIComponent(id)}/config`, config),
|
||||
|
||||
patchConfig: (id: string, patch: Record<string, unknown>) =>
|
||||
patchConfig: (id: string, patch: PluginConfigObject) =>
|
||||
apiClient.patch(`/plugins/${encodeURIComponent(id)}/config`, patch),
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface PluginConfigField {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type PluginConfigObject = Record<string, unknown>;
|
||||
|
||||
export interface PluginMetadata {
|
||||
name: string;
|
||||
version: string;
|
||||
|
||||
Reference in New Issue
Block a user