mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-12 17:37:30 +08:00
Fix config hot reload and release v0.2.1
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/YspCoder/clawgo/pkg/agent"
|
||||
@@ -176,6 +177,7 @@ func gatewayCmd() {
|
||||
registryServer.SetGatewayVersion(version)
|
||||
registryServer.SetWebUIVersion(version)
|
||||
registryServer.SetConfigPath(getConfigPath())
|
||||
registryServer.SetToken(cfg.Gateway.Token)
|
||||
registryServer.SetWorkspacePath(cfg.WorkspacePath())
|
||||
registryServer.SetLogFilePath(cfg.LogFilePath())
|
||||
registryServer.SetWebUIDir(filepath.Join(cfg.WorkspacePath(), "webui"))
|
||||
@@ -200,12 +202,15 @@ func gatewayCmd() {
|
||||
}
|
||||
return out
|
||||
})
|
||||
reloadReqCh := make(chan struct{}, 1)
|
||||
registryServer.SetConfigAfterHook(func() {
|
||||
select {
|
||||
case reloadReqCh <- struct{}{}:
|
||||
default:
|
||||
var reloadMu sync.Mutex
|
||||
var applyReload func() error
|
||||
registryServer.SetConfigAfterHook(func() error {
|
||||
reloadMu.Lock()
|
||||
defer reloadMu.Unlock()
|
||||
if applyReload == nil {
|
||||
return fmt.Errorf("reload handler not ready")
|
||||
}
|
||||
return applyReload()
|
||||
})
|
||||
registryServer.SetSubagentHandler(func(cctx context.Context, action string, args map[string]interface{}) (interface{}, error) {
|
||||
return agentLoop.HandleSubagentRuntime(cctx, action, args)
|
||||
@@ -373,12 +378,11 @@ func gatewayCmd() {
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, gatewayNotifySignals()...)
|
||||
applyReload := func() {
|
||||
applyReload = func() error {
|
||||
fmt.Println("\nReloading config...")
|
||||
newCfg, err := config.LoadConfig(getConfigPath())
|
||||
if err != nil {
|
||||
fmt.Printf("Reload failed (load config): %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "1") || strings.EqualFold(strings.TrimSpace(os.Getenv(envRootGranted)), "true") {
|
||||
applyMaximumPermissionPolicy(newCfg)
|
||||
@@ -392,7 +396,12 @@ func gatewayCmd() {
|
||||
|
||||
if reflect.DeepEqual(cfg, newCfg) {
|
||||
fmt.Println("Config unchanged, skip reload")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if cfg.Gateway.Host != newCfg.Gateway.Host || cfg.Gateway.Port != newCfg.Gateway.Port {
|
||||
fmt.Printf("Warning: gateway host/port change detected (%s:%d -> %s:%d); restart required to rebind listener\n",
|
||||
cfg.Gateway.Host, cfg.Gateway.Port, newCfg.Gateway.Host, newCfg.Gateway.Port)
|
||||
}
|
||||
|
||||
if shouldEmbedWhatsAppBridge(newCfg) {
|
||||
@@ -421,15 +430,18 @@ func gatewayCmd() {
|
||||
}
|
||||
cfg = newCfg
|
||||
runtimecfg.Set(cfg)
|
||||
registryServer.SetToken(cfg.Gateway.Token)
|
||||
registryServer.SetWorkspacePath(cfg.WorkspacePath())
|
||||
registryServer.SetLogFilePath(cfg.LogFilePath())
|
||||
registryServer.SetWebUIDir(filepath.Join(cfg.WorkspacePath(), "webui"))
|
||||
configureGatewayNodeP2P(agentLoop, registryServer, cfg)
|
||||
fmt.Println("Config hot-reload applied (logging/metadata only)")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
newAgentLoop, newChannelManager, err := buildGatewayRuntime(ctx, newCfg, msgBus, cronService)
|
||||
if err != nil {
|
||||
fmt.Printf("Reload failed (init runtime): %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("init runtime: %w", err)
|
||||
}
|
||||
|
||||
channelManager.StopAll(ctx)
|
||||
@@ -446,6 +458,11 @@ func gatewayCmd() {
|
||||
whatsAppBridge = newWhatsAppBridge
|
||||
whatsAppEmbedded = newWhatsAppBridge != nil
|
||||
runtimecfg.Set(cfg)
|
||||
configureLogging(newCfg)
|
||||
registryServer.SetToken(cfg.Gateway.Token)
|
||||
registryServer.SetWorkspacePath(cfg.WorkspacePath())
|
||||
registryServer.SetLogFilePath(cfg.LogFilePath())
|
||||
registryServer.SetWebUIDir(filepath.Join(cfg.WorkspacePath(), "webui"))
|
||||
configureGatewayNodeP2P(agentLoop, registryServer, cfg)
|
||||
registryServer.SetWhatsAppBridge(whatsAppBridge, embeddedWhatsAppBridgeBasePath)
|
||||
sentinelService.Stop()
|
||||
@@ -462,21 +479,24 @@ func gatewayCmd() {
|
||||
sentinelService.SetManager(channelManager)
|
||||
|
||||
if err := channelManager.StartAll(ctx); err != nil {
|
||||
fmt.Printf("Reload failed (start channels): %v\n", err)
|
||||
return
|
||||
return fmt.Errorf("start channels: %w", err)
|
||||
}
|
||||
go agentLoop.Run(ctx)
|
||||
fmt.Println("Config hot-reload applied")
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-reloadReqCh:
|
||||
applyReload()
|
||||
case sig := <-sigChan:
|
||||
switch {
|
||||
case isGatewayReloadSignal(sig):
|
||||
applyReload()
|
||||
reloadMu.Lock()
|
||||
err := applyReload()
|
||||
reloadMu.Unlock()
|
||||
if err != nil {
|
||||
fmt.Printf("Reload failed: %v\n", err)
|
||||
}
|
||||
default:
|
||||
fmt.Println("\nShutting down...")
|
||||
cancel()
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
//go:embed workspace
|
||||
var embeddedFiles embed.FS
|
||||
|
||||
var version = "0.2.0"
|
||||
var version = "0.2.1"
|
||||
var buildTime = "unknown"
|
||||
|
||||
const logo = ">"
|
||||
|
||||
@@ -56,7 +56,7 @@ type Server struct {
|
||||
logFilePath string
|
||||
onChat func(ctx context.Context, sessionKey, content string) (string, error)
|
||||
onChatHistory func(sessionKey string) []map[string]interface{}
|
||||
onConfigAfter func()
|
||||
onConfigAfter func() error
|
||||
onCron func(action string, args map[string]interface{}) (interface{}, error)
|
||||
onSubagents func(ctx context.Context, action string, args map[string]interface{}) (interface{}, error)
|
||||
onNodeDispatch func(ctx context.Context, req nodes.Request, mode string) (nodes.Response, error)
|
||||
@@ -291,13 +291,14 @@ func (s *Server) publishSubagentLiveSnapshot(ctx context.Context, key, taskID, p
|
||||
func (s *Server) SetConfigPath(path string) { s.configPath = strings.TrimSpace(path) }
|
||||
func (s *Server) SetWorkspacePath(path string) { s.workspacePath = strings.TrimSpace(path) }
|
||||
func (s *Server) SetLogFilePath(path string) { s.logFilePath = strings.TrimSpace(path) }
|
||||
func (s *Server) SetToken(token string) { s.token = strings.TrimSpace(token) }
|
||||
func (s *Server) SetChatHandler(fn func(ctx context.Context, sessionKey, content string) (string, error)) {
|
||||
s.onChat = fn
|
||||
}
|
||||
func (s *Server) SetChatHistoryHandler(fn func(sessionKey string) []map[string]interface{}) {
|
||||
s.onChatHistory = fn
|
||||
}
|
||||
func (s *Server) SetConfigAfterHook(fn func()) { s.onConfigAfter = fn }
|
||||
func (s *Server) SetConfigAfterHook(fn func() error) { s.onConfigAfter = fn }
|
||||
func (s *Server) SetCronHandler(fn func(action string, args map[string]interface{}) (interface{}, error)) {
|
||||
s.onCron = fn
|
||||
}
|
||||
@@ -489,16 +490,14 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
mux.HandleFunc("/webui/api/logs/stream", s.handleWebUILogsStream)
|
||||
mux.HandleFunc("/webui/api/logs/live", s.handleWebUILogsLive)
|
||||
mux.HandleFunc("/webui/api/logs/recent", s.handleWebUILogsRecent)
|
||||
if strings.TrimSpace(s.whatsAppBase) != "" {
|
||||
base := strings.TrimRight(strings.TrimSpace(s.whatsAppBase), "/")
|
||||
if base == "" {
|
||||
base = "/whatsapp"
|
||||
}
|
||||
mux.HandleFunc(base, s.handleWhatsAppBridgeWS)
|
||||
mux.HandleFunc(joinServerRoute(base, "ws"), s.handleWhatsAppBridgeWS)
|
||||
mux.HandleFunc(joinServerRoute(base, "status"), s.handleWhatsAppBridgeStatus)
|
||||
mux.HandleFunc(joinServerRoute(base, "logout"), s.handleWhatsAppBridgeLogout)
|
||||
base := strings.TrimRight(strings.TrimSpace(s.whatsAppBase), "/")
|
||||
if base == "" {
|
||||
base = "/whatsapp"
|
||||
}
|
||||
mux.HandleFunc(base, s.handleWhatsAppBridgeWS)
|
||||
mux.HandleFunc(joinServerRoute(base, "ws"), s.handleWhatsAppBridgeWS)
|
||||
mux.HandleFunc(joinServerRoute(base, "status"), s.handleWhatsAppBridgeStatus)
|
||||
mux.HandleFunc(joinServerRoute(base, "logout"), s.handleWhatsAppBridgeLogout)
|
||||
s.server = &http.Server{Addr: s.addr, Handler: mux}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
@@ -866,9 +865,15 @@ func (s *Server) handleWebUIConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if s.onConfigAfter != nil {
|
||||
s.onConfigAfter()
|
||||
if err := s.onConfigAfter(); err != nil {
|
||||
http.Error(w, "config saved but reload failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_ = requestSelfReloadSignal()
|
||||
if err := requestSelfReloadSignal(); err != nil {
|
||||
http.Error(w, "config saved but reload signal failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "reloaded": true})
|
||||
default:
|
||||
@@ -1490,9 +1495,13 @@ func (s *Server) saveProviderConfig(cfg *cfgpkg.Config, name string, pc cfgpkg.P
|
||||
return err
|
||||
}
|
||||
if s.onConfigAfter != nil {
|
||||
s.onConfigAfter()
|
||||
if err := s.onConfigAfter(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
_ = requestSelfReloadSignal()
|
||||
if err := requestSelfReloadSignal(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -5890,7 +5899,10 @@ func (s *Server) handleWebUIExecApprovals(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
if s.onConfigAfter != nil {
|
||||
s.onConfigAfter()
|
||||
if err := s.onConfigAfter(); err != nil {
|
||||
http.Error(w, "config saved but reload failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "reloaded": true})
|
||||
return
|
||||
|
||||
@@ -340,6 +340,80 @@ func TestHandleWebUIConfigRequiresConfirmForCustomProviderSecretChange(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWebUIConfigRunsReloadHookSynchronously(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmp := t.TempDir()
|
||||
cfgPath := filepath.Join(tmp, "config.json")
|
||||
cfg := cfgpkg.DefaultConfig()
|
||||
cfg.Logging.Enabled = false
|
||||
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
||||
t.Fatalf("save config: %v", err)
|
||||
}
|
||||
|
||||
body, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal body: %v", err)
|
||||
}
|
||||
|
||||
srv := NewServer("127.0.0.1", 0, "", nil)
|
||||
srv.SetConfigPath(cfgPath)
|
||||
called := false
|
||||
srv.SetConfigAfterHook(func() error {
|
||||
called = true
|
||||
return nil
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/webui/api/config", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
srv.handleWebUIConfig(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if !called {
|
||||
t.Fatalf("expected reload hook to run")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleWebUIConfigReturnsReloadHookError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmp := t.TempDir()
|
||||
cfgPath := filepath.Join(tmp, "config.json")
|
||||
cfg := cfgpkg.DefaultConfig()
|
||||
cfg.Logging.Enabled = false
|
||||
if err := cfgpkg.SaveConfig(cfgPath, cfg); err != nil {
|
||||
t.Fatalf("save config: %v", err)
|
||||
}
|
||||
|
||||
body, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal body: %v", err)
|
||||
}
|
||||
|
||||
srv := NewServer("127.0.0.1", 0, "", nil)
|
||||
srv.SetConfigPath(cfgPath)
|
||||
srv.SetConfigAfterHook(func() error {
|
||||
return fmt.Errorf("reload boom")
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/webui/api/config", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
srv.handleWebUIConfig(rec, req)
|
||||
|
||||
if rec.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("expected 500, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "reload failed") {
|
||||
t.Fatalf("expected reload failure in body, got: %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleNodeConnectRegistersAndHeartbeatsNode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clawgo-webui",
|
||||
"private": true,
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx server.ts",
|
||||
|
||||
@@ -9,9 +9,11 @@ type UI = {
|
||||
type UseConfigSaveActionArgs = {
|
||||
cfg: any;
|
||||
cfgRaw: string;
|
||||
loadConfig: (force?: boolean, tokenOverride?: string) => Promise<any>;
|
||||
q: string;
|
||||
setBaseline: React.Dispatch<React.SetStateAction<any>>;
|
||||
setConfigEditing: (editing: boolean) => void;
|
||||
setToken: (token: string) => void;
|
||||
setShowDiff: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
showRaw: boolean;
|
||||
t: (key: string, options?: any) => string;
|
||||
@@ -21,9 +23,11 @@ type UseConfigSaveActionArgs = {
|
||||
export function useConfigSaveAction({
|
||||
cfg,
|
||||
cfgRaw,
|
||||
loadConfig,
|
||||
q,
|
||||
setBaseline,
|
||||
setConfigEditing,
|
||||
setToken,
|
||||
setShowDiff,
|
||||
showRaw,
|
||||
t,
|
||||
@@ -68,8 +72,14 @@ export function useConfigSaveAction({
|
||||
throw new Error(result.data?.error || result.text || 'save failed');
|
||||
}
|
||||
|
||||
const hasGatewayToken = typeof payload?.gateway?.token === 'string';
|
||||
const nextToken = hasGatewayToken ? payload.gateway.token.trim() : '';
|
||||
const reloaded = await loadConfig(true, nextToken || undefined);
|
||||
if (hasGatewayToken) {
|
||||
setToken(nextToken);
|
||||
}
|
||||
await ui.notify({ title: t('saved'), message: t('configSaved') });
|
||||
setBaseline(cloneJSON(payload));
|
||||
setBaseline(cloneJSON(reloaded ?? payload));
|
||||
setConfigEditing(false);
|
||||
setShowDiff(false);
|
||||
} catch (error) {
|
||||
|
||||
@@ -87,7 +87,7 @@ interface AppContextType {
|
||||
refreshTaskQueue: () => Promise<void>;
|
||||
refreshEKGSummary: () => Promise<void>;
|
||||
refreshVersion: () => Promise<void>;
|
||||
loadConfig: (force?: boolean) => Promise<void>;
|
||||
loadConfig: (force?: boolean, tokenOverride?: string) => Promise<any>;
|
||||
gatewayVersion: string;
|
||||
webuiVersion: string;
|
||||
compiledChannels: string[];
|
||||
@@ -145,15 +145,20 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
|
||||
const q = token ? `?token=${encodeURIComponent(token)}` : '';
|
||||
|
||||
const loadConfig = useCallback(async (force = false) => {
|
||||
const loadConfig = useCallback(async (force = false, tokenOverride?: string) => {
|
||||
let loadedConfig: any = null;
|
||||
try {
|
||||
const hotQ = q ? `${q}&include_hot_reload_fields=1` : '?include_hot_reload_fields=1';
|
||||
const authQ = tokenOverride
|
||||
? `?token=${encodeURIComponent(tokenOverride)}`
|
||||
: q;
|
||||
const hotQ = authQ ? `${authQ}&include_hot_reload_fields=1` : '?include_hot_reload_fields=1';
|
||||
const r = await fetch(`/webui/api/config${hotQ}`);
|
||||
if (!r.ok) throw new Error('Failed to load config');
|
||||
const txt = await r.text();
|
||||
try {
|
||||
const parsed = JSON.parse(txt);
|
||||
if (parsed && parsed.config) {
|
||||
loadedConfig = parsed.config;
|
||||
if (!configEditing || force) {
|
||||
setCfg(parsed.config);
|
||||
setCfgRaw(JSON.stringify(parsed.config, null, 2));
|
||||
@@ -161,15 +166,23 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
setHotReloadFields(Array.isArray(parsed.hot_reload_fields) ? parsed.hot_reload_fields : []);
|
||||
setHotReloadFieldDetails(Array.isArray(parsed.hot_reload_field_details) ? parsed.hot_reload_field_details : []);
|
||||
} else {
|
||||
loadedConfig = parsed || {};
|
||||
if (!configEditing || force) {
|
||||
setCfg(parsed || {});
|
||||
setCfgRaw(txt);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
loadedConfig = null;
|
||||
if (!configEditing || force) {
|
||||
setCfgRaw(txt);
|
||||
try { setCfg(JSON.parse(txt)); } catch { setCfg({}); }
|
||||
try {
|
||||
loadedConfig = JSON.parse(txt);
|
||||
setCfg(loadedConfig);
|
||||
} catch {
|
||||
loadedConfig = {};
|
||||
setCfg({});
|
||||
}
|
||||
}
|
||||
}
|
||||
setIsGatewayOnline(true);
|
||||
@@ -177,6 +190,7 @@ export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
setIsGatewayOnline(false);
|
||||
console.error(e);
|
||||
}
|
||||
return loadedConfig;
|
||||
}, [q, configEditing]);
|
||||
|
||||
const refreshNodes = useCallback(async () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { cloneJSON } from '../utils/object';
|
||||
const Config: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const ui = useUI();
|
||||
const { cfg, setCfg, cfgRaw, setCfgRaw, loadConfig, hotReloadFieldDetails, q, setConfigEditing } = useAppContext();
|
||||
const { cfg, setCfg, cfgRaw, setCfgRaw, loadConfig, hotReloadFieldDetails, q, setConfigEditing, setToken } = useAppContext();
|
||||
const [showRaw, setShowRaw] = useState(false);
|
||||
const [basicMode, setBasicMode] = useState(true);
|
||||
const [hotOnly, setHotOnly] = useState(false);
|
||||
@@ -43,9 +43,11 @@ const Config: React.FC = () => {
|
||||
const { saveConfig } = useConfigSaveAction({
|
||||
cfg,
|
||||
cfgRaw,
|
||||
loadConfig,
|
||||
q,
|
||||
setBaseline,
|
||||
setConfigEditing,
|
||||
setToken,
|
||||
setShowDiff,
|
||||
showRaw,
|
||||
t,
|
||||
@@ -87,7 +89,10 @@ const Config: React.FC = () => {
|
||||
basicMode={basicMode}
|
||||
hotOnly={hotOnly}
|
||||
onHotOnlyChange={setHotOnly}
|
||||
onReload={async () => { await loadConfig(true); setTimeout(() => setBaseline(cloneJSON(cfg)), 0); }}
|
||||
onReload={async () => {
|
||||
const reloaded = await loadConfig(true);
|
||||
setBaseline(cloneJSON(reloaded ?? cfg));
|
||||
}}
|
||||
onSearchChange={setSearch}
|
||||
onShowDiff={() => setShowDiff(true)}
|
||||
onToggleBasicMode={() => setBasicMode((value) => !value)}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { cloneJSON } from '../utils/object';
|
||||
const Providers: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const ui = useUI();
|
||||
const { cfg, setCfg, cfgRaw, loadConfig, q, setConfigEditing, providerRuntimeItems } = useAppContext();
|
||||
const { cfg, setCfg, cfgRaw, loadConfig, q, setConfigEditing, providerRuntimeItems, setToken } = useAppContext();
|
||||
const [newProxyName, setNewProxyName] = useState('');
|
||||
const [runtimeAutoRefresh, setRuntimeAutoRefresh] = useState(true);
|
||||
const [runtimeRefreshSec, setRuntimeRefreshSec] = useState(10);
|
||||
@@ -134,9 +134,11 @@ const Providers: React.FC = () => {
|
||||
const { saveConfig } = useConfigSaveAction({
|
||||
cfg,
|
||||
cfgRaw,
|
||||
loadConfig,
|
||||
q,
|
||||
setBaseline,
|
||||
setConfigEditing,
|
||||
setToken,
|
||||
setShowDiff,
|
||||
showRaw: false,
|
||||
t,
|
||||
@@ -152,7 +154,10 @@ const Providers: React.FC = () => {
|
||||
titleClassName="ui-text-primary"
|
||||
actions={
|
||||
<div className="flex items-center gap-2 flex-wrap justify-end">
|
||||
<FixedButton onClick={async () => { await loadConfig(true); setTimeout(() => setBaseline(cloneJSON(cfg)), 0); }} label={t('reload')}>
|
||||
<FixedButton onClick={async () => {
|
||||
const reloaded = await loadConfig(true);
|
||||
setBaseline(cloneJSON(reloaded ?? cfg));
|
||||
}} label={t('reload')}>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</FixedButton>
|
||||
<Button onClick={() => setShowDiff(true)} size="sm">{t('configDiffPreview')}</Button>
|
||||
|
||||
Reference in New Issue
Block a user