feat: give plugins their own settings and one settings screen

This commit is contained in:
LukeGus committed 2026-09-22 18:31:19 -05:00
1 parent cc15559ea1
commit 49f0286a14
66 files changed
+36497 -451

No files matched your search

-32
View File
@@ -64,11 +64,6 @@ import {
type TerminalImageStorageTestResult,
type BrandingSettings,
} from "@/api/settings-api";
import {
getTailscaleSettings,
updateTailscaleSettings,
} from "../../../plugins/tailscale/src/frontend/tailscale-api";
import { AdminTailscaleSection } from "../../../plugins/tailscale/src/frontend/AdminTailscaleSection";
import {
getSessionSharingGloballyEnabled,
updateSessionSharingGloballyEnabled,
@@ -185,8 +180,6 @@ export function AdminSettingsPanel({
const [guacEnabled, setGuacEnabled] = useState(false);
const [guacUrl, setGuacUrl] = useState("guacd:4822");
const [logLevel, setLogLevel] = useState("info");
const [tailscaleApiKey, setTailscaleApiKey] = useState("");
const [tailscaleApiBaseUrl, setTailscaleApiBaseUrl] = useState("");
const [commandHistoryEnabled, setCommandHistoryEnabled] = useState(true);
const [analyticsEnabled, setAnalyticsEnabled] = useState(true);
const [analyticsLocked, setAnalyticsLocked] = useState(false);
@@ -413,7 +406,6 @@ export function AdminSettingsPanel({
guac,
oidcProv,
oidcSilent,
tailscale,
cmdHistory,
analytics,
sessionSharingEnabled,
@@ -435,7 +427,6 @@ export function AdminSettingsPanel({
getGuacamoleSettings(),
getOidcAutoProvision(),
getOidcSilentLoginDefault(),
getTailscaleSettings(),
getCommandHistoryEnabled(),
getAnalyticsEnabled(),
getSessionSharingGloballyEnabled(),
@@ -474,10 +465,6 @@ export function AdminSettingsPanel({
setGuacEnabled(guac.value.enabled);
setGuacUrl(guac.value.url || "guacd:4822");
}
if (tailscale.status === "fulfilled") {
setTailscaleApiKey(tailscale.value.apiKey ?? "");
setTailscaleApiBaseUrl(tailscale.value.apiBaseUrl ?? "");
}
if (cmdHistory.status === "fulfilled") {
setCommandHistoryEnabled(cmdHistory.value.enabled);
}
@@ -886,15 +873,6 @@ export function AdminSettingsPanel({
}
}
async function handleSaveTailscaleApiKey() {
try {
await updateTailscaleSettings(tailscaleApiKey, tailscaleApiBaseUrl);
toast.success(t("admin.tailscaleSettingsSaved"));
} catch {
toast.error(t("admin.tailscaleSettingsSaveFailed"));
}
}
async function handleSaveLogLevel(level: string) {
setLogLevel(level);
try {
@@ -1341,16 +1319,6 @@ export function AdminSettingsPanel({
handleSaveLogLevel={handleSaveLogLevel}
/>
<AdminTailscaleSection
open={openSections.has("tailscale")}
onToggle={() => toggle("tailscale")}
tailscaleApiKey={tailscaleApiKey}
setTailscaleApiKey={setTailscaleApiKey}
tailscaleApiBaseUrl={tailscaleApiBaseUrl}
setTailscaleApiBaseUrl={setTailscaleApiBaseUrl}
handleSaveTailscaleApiKey={handleSaveTailscaleApiKey}
/>
<AdminSSOSection
open={openSections.has("sso")}
onToggle={() => toggle("sso")}
+28
View File
@@ -67,6 +67,7 @@ import {
import type { Host, VaultProfile } from "@/types/ui-types";
import type { SSHHost, TunnelStatus } from "@/types";
import { useTabsSafe } from "@/shell/TabContext";
import { updatePluginHostSettings } from "@/api/plugins-api";
import {
buildHostEditorPayload,
createHostEditorForm,
@@ -333,6 +334,29 @@ export function HostEditor({
.finally(() => setTailscaleLoading(false));
}, [form.authType]);
/**
* Writes each plugin's host-scope values through its own route.
*
* One request per plugin rather than one per field, and a plugin that
* rejects a value is reported without failing the host save that already
* succeeded.
*/
const savePluginHostSettings = async (
hostId: number,
values: Record<string, Record<string, unknown>> | undefined,
) => {
if (!values || !Number.isInteger(hostId)) return;
for (const [pluginId, fields] of Object.entries(values)) {
if (!fields || Object.keys(fields).length === 0) continue;
try {
await updatePluginHostSettings(pluginId, hostId, fields);
} catch {
toast.error(t("settings.pluginSettingsSaveFailed"));
}
}
};
const handleSave = async () => {
setSaving(true);
try {
@@ -350,6 +374,10 @@ export function HostEditor({
? await updateSSHHost(Number(host.id), data)
: await createSSHHost(data);
}
// After the host: a new one has no id to scope settings to until it
// exists. A failure here must not claim the host itself failed to save.
await savePluginHostSettings(Number(saved.id), form.pluginSettings);
toast.success(host ? t("hosts.hostUpdated") : t("hosts.hostCreated"));
setPreviewTerminalTheme(null);
onSave(saved);
+7
View File
@@ -342,6 +342,13 @@ export function createHostEditorForm(
excludedMounts: [] as string[],
monitoredMounts: [] as Array<{ path: string; label?: string }>,
},
// Host-scope plugin settings, keyed by plugin id. Loaded with the host and
// saved through the plugin's own scoped route, not in the host payload.
pluginSettings: (host?.pluginSettings ?? {}) as Record<
string,
Record<string, unknown>
>,
};
}