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