refactor(openclaw): migrate config panels to TanStack Query hooks

Centralize query keys and extract reusable hooks (useOpenClaw.ts),
replacing manual useState/useEffect load/save patterns in Env, Tools,
and AgentsDefaults panels for consistency with MCP/Skills modules.
This commit is contained in:
Jason
2026-02-08 15:35:09 +08:00
parent bc87f9d9eb
commit 00b424628f
8 changed files with 228 additions and 120 deletions
+36 -42
View File
@@ -1,8 +1,12 @@
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Save } from "lucide-react";
import { toast } from "sonner";
import { openclawApi } from "@/lib/api/openclaw";
import {
useOpenClawAgentsDefaults,
useSaveOpenClawAgentsDefaults,
} from "@/hooks/useOpenClaw";
import { extractErrorMessage } from "@/utils/errorUtils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -10,11 +14,11 @@ import type { OpenClawAgentsDefaults } from "@/types";
const AgentsDefaultsPanel: React.FC = () => {
const { t } = useTranslation();
const { data: agentsData, isLoading } = useOpenClawAgentsDefaults();
const saveAgentsMutation = useSaveOpenClawAgentsDefaults();
const [defaults, setDefaults] = useState<OpenClawAgentsDefaults | null>(null);
const [primaryModel, setPrimaryModel] = useState("");
const [fallbacks, setFallbacks] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
// Extra known fields from agents.defaults
const [workspace, setWorkspace] = useState("");
@@ -22,38 +26,25 @@ const AgentsDefaultsPanel: React.FC = () => {
const [contextTokens, setContextTokens] = useState("");
const [maxConcurrent, setMaxConcurrent] = useState("");
const loadDefaults = useCallback(async () => {
try {
setLoading(true);
const data = await openclawApi.getAgentsDefaults();
setDefaults(data);
if (data) {
setPrimaryModel(data.model?.primary ?? "");
setFallbacks((data.model?.fallbacks ?? []).join(", "));
// Extract known extra fields
setWorkspace(String(data.workspace ?? ""));
setTimeout_(String(data.timeout ?? ""));
setContextTokens(String(data.contextTokens ?? ""));
setMaxConcurrent(String(data.maxConcurrent ?? ""));
}
} catch (err) {
toast.error(t("openclaw.agents.loadFailed"));
console.error("Failed to load agents defaults:", err);
} finally {
setLoading(false);
}
}, [t]);
useEffect(() => {
void loadDefaults();
}, [loadDefaults]);
// agentsData is undefined while loading, null when config section is absent
if (agentsData === undefined) return;
setDefaults(agentsData);
if (agentsData) {
setPrimaryModel(agentsData.model?.primary ?? "");
setFallbacks((agentsData.model?.fallbacks ?? []).join(", "));
// Extract known extra fields
setWorkspace(String(agentsData.workspace ?? ""));
setTimeout_(String(agentsData.timeout ?? ""));
setContextTokens(String(agentsData.contextTokens ?? ""));
setMaxConcurrent(String(agentsData.maxConcurrent ?? ""));
}
}, [agentsData]);
const handleSave = async () => {
try {
setSaving(true);
// Preserve all unknown fields from original data
const updated: OpenClawAgentsDefaults = { ...defaults };
@@ -94,18 +85,17 @@ const AgentsDefaultsPanel: React.FC = () => {
if (concNum !== undefined) updated.maxConcurrent = concNum;
else delete updated.maxConcurrent;
await openclawApi.setAgentsDefaults(updated);
await saveAgentsMutation.mutateAsync(updated);
toast.success(t("openclaw.agents.saveSuccess"));
await loadDefaults();
} catch (err) {
toast.error(t("openclaw.agents.saveFailed"));
console.error("Failed to save agents defaults:", err);
} finally {
setSaving(false);
} catch (error) {
const detail = extractErrorMessage(error);
toast.error(t("openclaw.agents.saveFailed"), {
description: detail || undefined,
});
}
};
if (loading) {
if (isLoading) {
return (
<div className="px-6 pt-4 pb-8 flex items-center justify-center min-h-[200px]">
<div className="text-sm text-muted-foreground">
@@ -222,9 +212,13 @@ const AgentsDefaultsPanel: React.FC = () => {
{/* Save button */}
<div className="flex justify-end">
<Button size="sm" onClick={handleSave} disabled={saving}>
<Button
size="sm"
onClick={handleSave}
disabled={saveAgentsMutation.isPending}
>
<Save className="w-4 h-4 mr-1" />
{saving ? t("common.saving") : t("common.save")}
{saveAgentsMutation.isPending ? t("common.saving") : t("common.save")}
</Button>
</div>
</div>
+22 -31
View File
@@ -1,8 +1,9 @@
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Plus, Trash2, Save, Eye, EyeOff } from "lucide-react";
import { toast } from "sonner";
import { openclawApi } from "@/lib/api/openclaw";
import { useOpenClawEnv, useSaveOpenClawEnv } from "@/hooks/useOpenClaw";
import { extractErrorMessage } from "@/utils/errorUtils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import type { OpenClawEnvConfig } from "@/types";
@@ -15,35 +16,23 @@ interface EnvEntry {
const EnvPanel: React.FC = () => {
const { t } = useTranslation();
const { data: envData, isLoading } = useOpenClawEnv();
const saveEnvMutation = useSaveOpenClawEnv();
const [entries, setEntries] = useState<EnvEntry[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set());
const loadEnv = useCallback(async () => {
try {
setLoading(true);
const env = await openclawApi.getEnv();
const items: EnvEntry[] = Object.entries(env).map(([key, value]) => ({
useEffect(() => {
if (envData) {
const items: EnvEntry[] = Object.entries(envData).map(([key, value]) => ({
key,
value: String(value ?? ""),
}));
setEntries(items.length > 0 ? items : []);
} catch (err) {
toast.error(t("openclaw.env.loadFailed"));
console.error("Failed to load env config:", err);
} finally {
setLoading(false);
}
}, [t]);
useEffect(() => {
void loadEnv();
}, [loadEnv]);
}, [envData]);
const handleSave = async () => {
try {
setSaving(true);
const env: OpenClawEnvConfig = {};
for (const entry of entries) {
const trimmedKey = entry.key.trim();
@@ -51,15 +40,13 @@ const EnvPanel: React.FC = () => {
env[trimmedKey] = entry.value;
}
}
await openclawApi.setEnv(env);
await saveEnvMutation.mutateAsync(env);
toast.success(t("openclaw.env.saveSuccess"));
// Reload to normalize
await loadEnv();
} catch (err) {
toast.error(t("openclaw.env.saveFailed"));
console.error("Failed to save env config:", err);
} finally {
setSaving(false);
} catch (error) {
const detail = extractErrorMessage(error);
toast.error(t("openclaw.env.saveFailed"), {
description: detail || undefined,
});
}
};
@@ -93,7 +80,7 @@ const EnvPanel: React.FC = () => {
const isApiKey = (key: string) => /key|token|secret|password/i.test(key);
if (loading) {
if (isLoading) {
return (
<div className="px-6 pt-4 pb-8 flex items-center justify-center min-h-[200px]">
<div className="text-sm text-muted-foreground">
@@ -168,9 +155,13 @@ const EnvPanel: React.FC = () => {
{t("openclaw.env.add")}
</Button>
<div className="flex-1" />
<Button size="sm" onClick={handleSave} disabled={saving}>
<Button
size="sm"
onClick={handleSave}
disabled={saveEnvMutation.isPending}
>
<Save className="w-4 h-4 mr-1" />
{saving ? t("common.saving") : t("common.save")}
{saveEnvMutation.isPending ? t("common.saving") : t("common.save")}
</Button>
</div>
</div>
+24 -32
View File
@@ -1,8 +1,9 @@
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Plus, Trash2, Save } from "lucide-react";
import { toast } from "sonner";
import { openclawApi } from "@/lib/api/openclaw";
import { useOpenClawTools, useSaveOpenClawTools } from "@/hooks/useOpenClaw";
import { extractErrorMessage } from "@/utils/errorUtils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -19,34 +20,22 @@ const PROFILE_OPTIONS = ["default", "strict", "permissive", "custom"];
const ToolsPanel: React.FC = () => {
const { t } = useTranslation();
const { data: toolsData, isLoading } = useOpenClawTools();
const saveToolsMutation = useSaveOpenClawTools();
const [config, setConfig] = useState<OpenClawToolsConfig>({});
const [allowList, setAllowList] = useState<string[]>([]);
const [denyList, setDenyList] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const loadTools = useCallback(async () => {
try {
setLoading(true);
const tools = await openclawApi.getTools();
setConfig(tools);
setAllowList(tools.allow ?? []);
setDenyList(tools.deny ?? []);
} catch (err) {
toast.error(t("openclaw.tools.loadFailed"));
console.error("Failed to load tools config:", err);
} finally {
setLoading(false);
}
}, [t]);
useEffect(() => {
void loadTools();
}, [loadTools]);
if (toolsData) {
setConfig(toolsData);
setAllowList(toolsData.allow ?? []);
setDenyList(toolsData.deny ?? []);
}
}, [toolsData]);
const handleSave = async () => {
try {
setSaving(true);
const { profile, allow, deny, ...other } = config;
const newConfig: OpenClawToolsConfig = {
...other,
@@ -54,14 +43,13 @@ const ToolsPanel: React.FC = () => {
allow: allowList.filter((s) => s.trim()),
deny: denyList.filter((s) => s.trim()),
};
await openclawApi.setTools(newConfig);
await saveToolsMutation.mutateAsync(newConfig);
toast.success(t("openclaw.tools.saveSuccess"));
await loadTools();
} catch (err) {
toast.error(t("openclaw.tools.saveFailed"));
console.error("Failed to save tools config:", err);
} finally {
setSaving(false);
} catch (error) {
const detail = extractErrorMessage(error);
toast.error(t("openclaw.tools.saveFailed"), {
description: detail || undefined,
});
}
};
@@ -82,7 +70,7 @@ const ToolsPanel: React.FC = () => {
setList(list.filter((_, i) => i !== index));
};
if (loading) {
if (isLoading) {
return (
<div className="px-6 pt-4 pb-8 flex items-center justify-center min-h-[200px]">
<div className="text-sm text-muted-foreground">
@@ -192,9 +180,13 @@ const ToolsPanel: React.FC = () => {
{/* Save button */}
<div className="flex justify-end">
<Button size="sm" onClick={handleSave} disabled={saving}>
<Button
size="sm"
onClick={handleSave}
disabled={saveToolsMutation.isPending}
>
<Save className="w-4 h-4 mr-1" />
{saving ? t("common.saving") : t("common.save")}
{saveToolsMutation.isPending ? t("common.saving") : t("common.save")}
</Button>
</div>
</div>
+11 -11
View File
@@ -19,8 +19,12 @@ import { useQuery } from "@tanstack/react-query";
import type { Provider } from "@/types";
import type { AppId } from "@/lib/api";
import { providersApi } from "@/lib/api/providers";
import { openclawApi } from "@/lib/api/openclaw";
import { useDragSort } from "@/hooks/useDragSort";
import {
useOpenClawLiveProviderIds,
useOpenClawDefaultModel,
} from "@/hooks/useOpenClaw";
// import { useStreamCheck } from "@/hooks/useStreamCheck"; // 测试功能已隐藏
import { ProviderCard } from "@/components/providers/ProviderCard";
import { ProviderEmptyState } from "@/components/providers/ProviderEmptyState";
import {
@@ -88,11 +92,9 @@ export function ProviderList({
});
// OpenClaw: 查询 live 配置中的供应商 ID 列表,用于判断 isInConfig
const { data: openclawLiveIds } = useQuery({
queryKey: ["openclawLiveProviderIds"],
queryFn: () => providersApi.getOpenClawLiveProviderIds(),
enabled: appId === "openclaw",
});
const { data: openclawLiveIds } = useOpenClawLiveProviderIds(
appId === "openclaw",
);
// 判断供应商是否已添加到配置(累加模式应用:OpenCode/OpenClaw
const isProviderInConfig = useCallback(
@@ -109,11 +111,9 @@ export function ProviderList({
);
// OpenClaw: query default model to determine which provider is default
const { data: openclawDefaultModel } = useQuery({
queryKey: ["openclawDefaultModel"],
queryFn: () => openclawApi.getDefaultModel(),
enabled: appId === "openclaw",
});
const { data: openclawDefaultModel } = useOpenClawDefaultModel(
appId === "openclaw",
);
const isProviderDefaultModel = useCallback(
(providerId: string): boolean => {