mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
fix: finish 2.9.0 plugin moves and get every check green
This commit is contained in:
1 parent
8c1e02af89
commit
6489b62ece
335 files changed
+29878
-17749
No files matched your search
@@ -497,34 +497,6 @@ export function AdminHostDefaultsSection({
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("admin.hostDefaultsSessionLogging")}
|
||||
description={t("admin.hostDefaultsSessionLoggingDesc")}
|
||||
>
|
||||
<AdminToggle
|
||||
on={defaults.enableSessionLogging ?? true}
|
||||
onToggle={() =>
|
||||
setDefaults((p) => ({
|
||||
...p,
|
||||
enableSessionLogging: !(p.enableSessionLogging ?? true),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("admin.hostDefaultsCommandHistory")}
|
||||
description={t("admin.hostDefaultsCommandHistoryDesc")}
|
||||
>
|
||||
<AdminToggle
|
||||
on={defaults.enableCommandHistory ?? true}
|
||||
onToggle={() =>
|
||||
setDefaults((p) => ({
|
||||
...p,
|
||||
enableCommandHistory: !(p.enableCommandHistory ?? true),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -76,6 +76,10 @@ import {
|
||||
import { useConnectionDefaults } from "@/contexts/ConnectionDefaultsContext";
|
||||
import { HostEditorGeneralTab } from "./HostEditorGeneralTab";
|
||||
import { withProtocolSettings } from "./host-protocols";
|
||||
import {
|
||||
usePluginHostSections,
|
||||
withNewHostDefaults,
|
||||
} from "@/settings/HostPluginSections";
|
||||
import { canEditHost } from "./host-permissions";
|
||||
import {
|
||||
getHostEditorSection,
|
||||
@@ -129,6 +133,7 @@ export function HostEditor({
|
||||
onEditCredential?: (credentialId: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const hostSettingPlugins = usePluginHostSections();
|
||||
const { setPreviewTerminalTheme } = useTabsSafe();
|
||||
const connectionDefaults = useConnectionDefaults();
|
||||
const [form, setForm] = useState(() =>
|
||||
@@ -315,9 +320,12 @@ export function HostEditor({
|
||||
}
|
||||
// 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.
|
||||
const pluginValues = withProtocolSettings(form.pluginSettings, protocols);
|
||||
await savePluginHostSettings(
|
||||
Number(saved.id),
|
||||
withProtocolSettings(form.pluginSettings, protocols),
|
||||
host
|
||||
? pluginValues
|
||||
: withNewHostDefaults(pluginValues ?? {}, hostSettingPlugins),
|
||||
);
|
||||
|
||||
toast.success(host ? t("hosts.hostUpdated") : t("hosts.hostCreated"));
|
||||
|
||||
@@ -99,10 +99,6 @@ export function createHostEditorForm(
|
||||
...(connectionDefaults?.terminal ?? {}),
|
||||
...(host?.terminalConfig ?? {}),
|
||||
};
|
||||
const proxmoxSettings = (host?.pluginSettings?.proxmox ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const rawTheme = terminalConfig.theme ?? d?.theme;
|
||||
const normalizedTheme =
|
||||
!rawTheme ||
|
||||
@@ -152,27 +148,6 @@ export function createHostEditorForm(
|
||||
socks5ProxyChain: (host?.socks5ProxyChain ?? []) as HostSocks5ProxyNode[],
|
||||
connectionOrigin: (host?.connectionOrigin ?? null) as
|
||||
"local" | "remote" | null,
|
||||
enableTerminal: host?.enableTerminal ?? true,
|
||||
enableSessionLogging:
|
||||
host?.enableSessionLogging ?? d?.enableSessionLogging ?? true,
|
||||
enableCommandHistory:
|
||||
host?.enableCommandHistory ?? d?.enableCommandHistory ?? true,
|
||||
enableFileManager: host?.enableFileManager ?? false,
|
||||
scpLegacy: host?.scpLegacy ?? false,
|
||||
enableTmuxMonitor: host?.enableTmuxMonitor ?? false,
|
||||
enableTerminalToolbar: host?.enableTerminalToolbar ?? true,
|
||||
enableProxmox: (proxmoxSettings.enableProxmox as boolean) ?? false,
|
||||
// The proxmox tab fills in its own defaults when this is unset.
|
||||
proxmoxConfig:
|
||||
(proxmoxSettings.proxmoxConfig as Host["proxmoxConfig"]) ?? null,
|
||||
enableProxmoxStats:
|
||||
(proxmoxSettings.enableProxmoxStats as boolean) ?? false,
|
||||
proxmoxStatsConfig:
|
||||
((proxmoxSettings.proxmoxStatsConfig as Host["proxmoxStatsConfig"]) ?? {
|
||||
pollInterval: 60,
|
||||
nodeName: null,
|
||||
}) as Host["proxmoxStatsConfig"],
|
||||
defaultPath: host?.defaultPath ?? "/",
|
||||
forceKeyboardInteractive: host?.forceKeyboardInteractive ?? false,
|
||||
inheritTerminalAppearance:
|
||||
!host ||
|
||||
@@ -409,23 +384,6 @@ export function buildHostEditorPayload(
|
||||
usesCredential && form.credentialId ? Number(form.credentialId) : null,
|
||||
overrideCredentialUsername: form.overrideCredentialUsername,
|
||||
notes: form.notes,
|
||||
enableTerminal: form.enableTerminal,
|
||||
enableSessionLogging: form.enableSessionLogging,
|
||||
enableCommandHistory: form.enableCommandHistory,
|
||||
enableFileManager: form.enableFileManager,
|
||||
scpLegacy: form.scpLegacy,
|
||||
enableTmuxMonitor: form.enableTmuxMonitor,
|
||||
enableTerminalToolbar: form.enableTerminalToolbar,
|
||||
enableProxmox: form.enableProxmox,
|
||||
proxmoxConfig:
|
||||
form.enableProxmox || form.proxmoxConfig?.source
|
||||
? form.proxmoxConfig
|
||||
: null,
|
||||
enableProxmoxStats: form.enableProxmoxStats,
|
||||
proxmoxStatsConfig: form.enableProxmoxStats
|
||||
? form.proxmoxStatsConfig
|
||||
: null,
|
||||
defaultPath: form.defaultPath || "/",
|
||||
useSocks5: form.useSocks5,
|
||||
socks5Host:
|
||||
form.socks5ProxyMode === "single" ? form.socks5Host || null : null,
|
||||
|
||||
@@ -6,16 +6,7 @@ import { Input } from "@/components/input";
|
||||
import { PasswordInput } from "@/components/password-input";
|
||||
import { FakeSwitch, SectionCard, SettingRow } from "@/components/section-card";
|
||||
import type { Host } from "@/types/ui-types";
|
||||
import {
|
||||
Activity,
|
||||
Globe,
|
||||
LayoutGrid,
|
||||
Plus,
|
||||
Tag,
|
||||
Terminal,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Activity, Globe, Plus, Tag, Terminal, Trash2, X } from "lucide-react";
|
||||
import { FolderPathPicker } from "./FolderPathPicker";
|
||||
import { HostParentPicker } from "./HostParentPicker";
|
||||
import { getSSHFolders, isElectron } from "@/main-axios";
|
||||
@@ -851,23 +842,6 @@ export function HostEditorGeneralTab({
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
title={t("hosts.connectionToolbar")}
|
||||
icon={<LayoutGrid className="size-3.5" />}
|
||||
>
|
||||
<div className="flex flex-col gap-4 py-3">
|
||||
<SettingRow
|
||||
label={t("hosts.showConnectionToolbar")}
|
||||
description={t("hosts.showConnectionToolbarDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={form.enableTerminalToolbar}
|
||||
onChange={(value) => setField("enableTerminalToolbar", value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
</div>
|
||||
</SectionCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -46,10 +46,6 @@ export function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
const isSshHost = h.connectionType === "ssh" || !h.connectionType;
|
||||
const parsedTerminalConfig = parseJson(h.terminalConfig) as
|
||||
(Host["terminalConfig"] & { sudoPassword?: string }) | undefined;
|
||||
const proxmoxSettings = (h.pluginSettings?.proxmox ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
return {
|
||||
id: String(h.id),
|
||||
name: h.name,
|
||||
@@ -83,23 +79,6 @@ export function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
sortOrder: h.sortOrder ?? null,
|
||||
connectionOrigin: h.connectionOrigin ?? null,
|
||||
enableSsh: h.enableSsh != null ? h.enableSsh : isSshHost,
|
||||
enableTerminal:
|
||||
h.enableTerminal ?? (h.enableSsh != null ? h.enableSsh : isSshHost),
|
||||
enableSessionLogging: h.enableSessionLogging ?? true,
|
||||
enableCommandHistory: h.enableCommandHistory ?? true,
|
||||
enableTunnel: h.enableTunnel ?? false,
|
||||
enableFileManager: h.enableFileManager ?? true,
|
||||
enableWebUi: h.enableWebUi ?? false,
|
||||
enableProxmox: (proxmoxSettings.enableProxmox as boolean) ?? false,
|
||||
enableProxmoxStats:
|
||||
(proxmoxSettings.enableProxmoxStats as boolean) ?? false,
|
||||
enableTmuxMonitor: h.enableTmuxMonitor ?? false,
|
||||
enableTerminalToolbar: h.enableTerminalToolbar ?? true,
|
||||
proxmoxConfig:
|
||||
(proxmoxSettings.proxmoxConfig as Host["proxmoxConfig"]) ?? null,
|
||||
proxmoxStatsConfig:
|
||||
(proxmoxSettings.proxmoxStatsConfig as Host["proxmoxStatsConfig"]) ??
|
||||
null,
|
||||
sshPort:
|
||||
h.sshPort ??
|
||||
(h.connectionType === "ssh" || !h.connectionType ? h.port : 22),
|
||||
@@ -132,12 +111,10 @@ export function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
name: a.name,
|
||||
snippetId: String(a.snippetId),
|
||||
})),
|
||||
serverTunnels: parseJson(h.tunnelConnections) ?? [],
|
||||
jumpHosts: (parseJson<HostJumpHost[]>(h.jumpHosts) ?? []).map((j) => ({
|
||||
hostId: String(j.hostId ?? j.hostid ?? j),
|
||||
})),
|
||||
portKnockSequence: parseJson(h.portKnockSequence) ?? [],
|
||||
defaultPath: h.defaultPath,
|
||||
terminalConfig: parsedTerminalConfig as Host["terminalConfig"],
|
||||
hasSudoPassword:
|
||||
!!host.hasSudoPassword || !!parsedTerminalConfig?.sudoPassword,
|
||||
|
||||
@@ -30,6 +30,7 @@ import { HostShareModal } from "@/sidebar/HostShareModal";
|
||||
import { HostExportDialog } from "@/sidebar/HostExportDialog";
|
||||
import { CustomizeSidebarPanel } from "@/sidebar/CustomizeSidebarPanel";
|
||||
import { ComponentSlot } from "@/shell/ActionSlot";
|
||||
import { usePluginHostSections } from "@/settings/HostPluginSections";
|
||||
import { Button } from "@/components/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -124,7 +125,14 @@ function groupHosts(
|
||||
return { name: "root", children };
|
||||
}
|
||||
|
||||
function hostPassesFilters(host: Host, filters: FilterState): boolean {
|
||||
/** Plugin id to the host setting that switches it on. */
|
||||
type HostSwitches = Record<string, string>;
|
||||
|
||||
function hostPassesFilters(
|
||||
host: Host,
|
||||
filters: FilterState,
|
||||
switches: HostSwitches,
|
||||
): boolean {
|
||||
if (filters.status.length > 0) {
|
||||
const ok =
|
||||
(filters.status.includes("online") && host.online) ||
|
||||
@@ -150,9 +158,12 @@ function hostPassesFilters(host: Host, filters: FilterState): boolean {
|
||||
);
|
||||
if (!ok) return false;
|
||||
}
|
||||
if (filters.features.length > 0) {
|
||||
const ok =
|
||||
filters.features.includes("fileManager") && host.enableFileManager;
|
||||
// A saved filter for a plugin that is gone or off is ignored.
|
||||
const features = filters.features.filter((id) => switches[id]);
|
||||
if (features.length > 0) {
|
||||
const ok = features.some(
|
||||
(id) => host.pluginSettings?.[id]?.[switches[id]] === true,
|
||||
);
|
||||
if (!ok) return false;
|
||||
}
|
||||
if (filters.tags.length > 0) {
|
||||
@@ -162,14 +173,18 @@ function hostPassesFilters(host: Host, filters: FilterState): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyFilters(folder: HostFolder, filters: FilterState): HostFolder {
|
||||
function applyFilters(
|
||||
folder: HostFolder,
|
||||
filters: FilterState,
|
||||
switches: HostSwitches,
|
||||
): HostFolder {
|
||||
const active = Object.values(filters).some((arr) => arr.length > 0);
|
||||
if (!active) return folder;
|
||||
|
||||
const filteredChildren = folder.children
|
||||
.map((child) => {
|
||||
if (isFolder(child)) return applyFilters(child, filters);
|
||||
return hostPassesFilters(child, filters) ? child : null;
|
||||
if (isFolder(child)) return applyFilters(child, filters, switches);
|
||||
return hostPassesFilters(child, filters, switches) ? child : null;
|
||||
})
|
||||
.filter((child): child is Host | HostFolder => {
|
||||
if (child === null) return false;
|
||||
@@ -203,6 +218,15 @@ export function HostsPanel({
|
||||
active?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const hostSwitchPlugins = usePluginHostSections().filter(
|
||||
(plugin) => !!plugin.contributes?.settings?.host?.enableKey,
|
||||
);
|
||||
const hostSwitches: Record<string, string> = Object.fromEntries(
|
||||
hostSwitchPlugins.map((plugin) => [
|
||||
plugin.id,
|
||||
plugin.contributes!.settings!.host!.enableKey!,
|
||||
]),
|
||||
);
|
||||
const hostProtocols = useHostProtocols();
|
||||
const sshAuthProviders = useSshAuthProviders();
|
||||
const [hostSearch, setHostSearch] = useState("");
|
||||
@@ -777,20 +801,18 @@ export function HostsPanel({
|
||||
<DropdownMenuLabel>
|
||||
{t("hosts.filterFeaturesGroup")}
|
||||
</DropdownMenuLabel>
|
||||
{([["fileManager", "FileManager"]] as const).map(
|
||||
([val, key]) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={val}
|
||||
checked={filterState.features.includes(val)}
|
||||
onCheckedChange={() =>
|
||||
handleFilterToggle("features", val)
|
||||
}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{t(`hosts.filterFeature${key}`)}
|
||||
</DropdownMenuCheckboxItem>
|
||||
),
|
||||
)}
|
||||
{hostSwitchPlugins.map((plugin) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={plugin.id}
|
||||
checked={filterState.features.includes(plugin.id)}
|
||||
onCheckedChange={() =>
|
||||
handleFilterToggle("features", plugin.id)
|
||||
}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
{plugin.name}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
{allTags.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -999,6 +1021,7 @@ export function HostsPanel({
|
||||
applyFilters(
|
||||
sortHostTree(hostTree, sortKey, pinnedFirst),
|
||||
filterState,
|
||||
hostSwitches,
|
||||
),
|
||||
groupKey,
|
||||
groupLabel,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "@/components/dialog";
|
||||
import { useUiPreferencesContext } from "@/contexts/UiPreferencesContext";
|
||||
import { applyPresetSideEffects } from "@/lib/apply-ui-preset";
|
||||
import { usePluginStore } from "@/plugin-host/plugin-store";
|
||||
import {
|
||||
HOMEPAGE_AREA_KEY,
|
||||
UI_AREA_KEYS,
|
||||
@@ -58,14 +59,37 @@ export function InterfacePresetSettings({
|
||||
// the customizations are listed separately below.
|
||||
const activeLabel = ctx?.preferences.preset ?? "balanced";
|
||||
|
||||
const pluginStore = usePluginStore();
|
||||
const customizedAreas = useMemo(() => {
|
||||
if (!ctx) return [] as UiAreaKey[];
|
||||
return UI_AREA_KEYS.filter(
|
||||
if (!ctx) return [] as Array<{ key: string; label: string }>;
|
||||
const overrides = ctx.preferences.overrides as Record<
|
||||
string,
|
||||
Record<string, unknown> | undefined
|
||||
>;
|
||||
const core = UI_AREA_KEYS.filter(
|
||||
(area) =>
|
||||
!HIDDEN_OVERRIDE_AREAS.has(area) &&
|
||||
Object.keys(ctx.preferences.overrides[area] ?? {}).length > 0,
|
||||
);
|
||||
}, [ctx]);
|
||||
Object.keys(overrides[area] ?? {}).length > 0,
|
||||
).map((area) => ({
|
||||
key: area as string,
|
||||
label: t(`newUi.sidebar.userProfile.uiArea_${area}`),
|
||||
}));
|
||||
// A plugin's own area is labelled with the plugin's name.
|
||||
const plugins = Object.keys(overrides)
|
||||
.filter(
|
||||
(area) =>
|
||||
area.startsWith("plugin:") &&
|
||||
Object.keys(overrides[area] ?? {}).length > 0,
|
||||
)
|
||||
.map((area) => {
|
||||
const pluginId = area.slice("plugin:".length);
|
||||
return {
|
||||
key: area,
|
||||
label: pluginStore.records.get(pluginId)?.summary.name ?? pluginId,
|
||||
};
|
||||
});
|
||||
return [...core, ...plugins];
|
||||
}, [ctx, pluginStore, t]);
|
||||
|
||||
if (!ctx) return null;
|
||||
|
||||
@@ -143,15 +167,15 @@ export function InterfacePresetSettings({
|
||||
</span>
|
||||
{customizedAreas.map((area) => (
|
||||
<div
|
||||
key={area}
|
||||
key={area.key}
|
||||
className="flex items-center justify-between gap-2 py-0.5"
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate text-xs">
|
||||
{t(`newUi.sidebar.userProfile.uiArea_${area}`)}
|
||||
{area.label}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => ctx.clearArea(area)}
|
||||
onClick={() => ctx.clearArea(area.key as UiAreaKey)}
|
||||
className="shrink-0 text-[10px] text-muted-foreground underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
{t("newUi.sidebar.userProfile.presetRevertArea")}
|
||||
|
||||
@@ -38,30 +38,9 @@ export const FIELD_GROUP_KEYS: Record<FieldGroup, string[]> = {
|
||||
],
|
||||
jumpHosts: ["jumpHosts"],
|
||||
quickActions: ["quickActions"],
|
||||
featureFlags: [
|
||||
"enableTerminal",
|
||||
"enableCommandHistory",
|
||||
"enableTerminalToolbar",
|
||||
"enableFileManager",
|
||||
"enableWebUi",
|
||||
"enableProxmox",
|
||||
"enableTmuxMonitor",
|
||||
"showTerminalInSidebar",
|
||||
"showFileManagerInSidebar",
|
||||
"showTunnelInSidebar",
|
||||
"showDockerInSidebar",
|
||||
"showServerStatsInSidebar",
|
||||
"defaultPath",
|
||||
"forceKeyboardInteractive",
|
||||
],
|
||||
advanced: [
|
||||
"statusCheckEnabled",
|
||||
"statusCheckInterval",
|
||||
"webUiConfig",
|
||||
"proxmoxConfig",
|
||||
"terminalConfig",
|
||||
"guacamoleConfig",
|
||||
],
|
||||
// Every plugin's host settings, enable switches included.
|
||||
featureFlags: ["pluginSettings", "forceKeyboardInteractive"],
|
||||
advanced: ["statusCheckEnabled", "statusCheckInterval", "terminalConfig"],
|
||||
};
|
||||
|
||||
export const SECRET_KEYS = [
|
||||
@@ -102,6 +81,25 @@ function nestedSecret(
|
||||
return field in record ? record : null;
|
||||
}
|
||||
|
||||
/** Plugin host settings can carry the same nested secrets as the host. */
|
||||
function mapPluginSettings(
|
||||
pluginSettings: unknown,
|
||||
apply: (values: Record<string, unknown>) => void,
|
||||
): unknown {
|
||||
if (!pluginSettings || typeof pluginSettings !== "object") {
|
||||
return pluginSettings;
|
||||
}
|
||||
const perPlugin: Record<string, unknown> = {};
|
||||
for (const [pluginId, values] of Object.entries(
|
||||
pluginSettings as Record<string, unknown>,
|
||||
)) {
|
||||
const copy = { ...(values as Record<string, unknown>) };
|
||||
apply(copy);
|
||||
perPlugin[pluginId] = copy;
|
||||
}
|
||||
return perPlugin;
|
||||
}
|
||||
|
||||
export function hostKey(host: Record<string, unknown>): string {
|
||||
return JSON.stringify(TUPLE_KEYS.map((k) => String(host[k] ?? "")));
|
||||
}
|
||||
@@ -129,10 +127,17 @@ export function buildExportPayload(
|
||||
if (allowed.has(key)) shaped[key] = value;
|
||||
}
|
||||
if (!withCredentials) {
|
||||
for (const { container, field } of NESTED_SECRETS) {
|
||||
const record = nestedSecret(shaped, container, field);
|
||||
if (record) shaped[container] = { ...record, [field]: null };
|
||||
}
|
||||
const clearNested = (target: Record<string, unknown>) => {
|
||||
for (const { container, field } of NESTED_SECRETS) {
|
||||
const record = nestedSecret(target, container, field);
|
||||
if (record) target[container] = { ...record, [field]: null };
|
||||
}
|
||||
};
|
||||
clearNested(shaped);
|
||||
shaped.pluginSettings = mapPluginSettings(
|
||||
shaped.pluginSettings,
|
||||
clearNested,
|
||||
);
|
||||
for (const { container, field } of NESTED_SECRET_ARRAYS) {
|
||||
const arr = shaped[container];
|
||||
if (Array.isArray(arr)) {
|
||||
@@ -175,12 +180,19 @@ export function maskSecrets(payload: ExportPayload): ExportPayload {
|
||||
masked[key] = "<included>";
|
||||
}
|
||||
}
|
||||
for (const { container, field } of NESTED_SECRETS) {
|
||||
const record = nestedSecret(masked, container, field);
|
||||
if (record && record[field]) {
|
||||
masked[container] = { ...record, [field]: "<included>" };
|
||||
const maskNested = (target: Record<string, unknown>) => {
|
||||
for (const { container, field } of NESTED_SECRETS) {
|
||||
const record = nestedSecret(target, container, field);
|
||||
if (record && record[field]) {
|
||||
target[container] = { ...record, [field]: "<included>" };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
maskNested(masked);
|
||||
masked.pluginSettings = mapPluginSettings(
|
||||
masked.pluginSettings,
|
||||
maskNested,
|
||||
);
|
||||
for (const { container, field } of NESTED_SECRET_ARRAYS) {
|
||||
const arr = masked[container];
|
||||
if (Array.isArray(arr)) {
|
||||
|
||||
@@ -30,11 +30,6 @@ export function createQuickConnectHost(input: QuickConnectInput): Host {
|
||||
...createQuickConnectHost({ ...input, protocol: undefined, port: 22 }),
|
||||
...login,
|
||||
port: input.port,
|
||||
enableTerminal: false,
|
||||
enableCommandHistory: false,
|
||||
enableFileManager: false,
|
||||
enableTunnel: false,
|
||||
enableTerminalToolbar: false,
|
||||
enableSsh: false,
|
||||
domain: input.domain,
|
||||
pluginSettings: {
|
||||
@@ -62,17 +57,7 @@ export function createQuickConnectHost(input: QuickConnectInput): Host {
|
||||
ram: null,
|
||||
lastAccess: new Date().toISOString(),
|
||||
pin: false,
|
||||
defaultPath: "",
|
||||
serverTunnels: [],
|
||||
quickActions: [],
|
||||
enableTerminal: true,
|
||||
enableCommandHistory: true,
|
||||
enableFileManager: true,
|
||||
enableTunnel: true,
|
||||
enableProxmox: false,
|
||||
enableProxmoxStats: false,
|
||||
enableTmuxMonitor: false,
|
||||
enableTerminalToolbar: true,
|
||||
enableSsh: true,
|
||||
sshPort: input.port,
|
||||
};
|
||||
@@ -92,20 +77,6 @@ export function quickConnectHostToPayload(host: Host): SSHHostData {
|
||||
: null,
|
||||
folder: host.folder,
|
||||
pin: host.pin,
|
||||
defaultPath: host.defaultPath,
|
||||
enableTerminal: true,
|
||||
enableSessionLogging: true,
|
||||
enableCommandHistory: host.enableCommandHistory,
|
||||
enableFileManager: host.enableFileManager,
|
||||
enableTunnel: host.enableTunnel,
|
||||
enableProxmox: host.enableProxmox,
|
||||
enableTmuxMonitor: host.enableTmuxMonitor,
|
||||
enableTerminalToolbar: host.enableTerminalToolbar,
|
||||
showTerminalInSidebar: true,
|
||||
showFileManagerInSidebar: true,
|
||||
showTunnelInSidebar: true,
|
||||
showDockerInSidebar: true,
|
||||
showServerStatsInSidebar: true,
|
||||
connectionType: "ssh",
|
||||
enableSsh: true,
|
||||
sshPort: host.sshPort,
|
||||
|
||||
@@ -7,15 +7,15 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { updatePluginHostSettings } from "@/api/plugins-api";
|
||||
import { PluginIcon } from "@/lib/plugin-icon";
|
||||
import { usePluginHostSections } from "@/settings/HostPluginSections";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useAreaPreferences } from "@/contexts/UiPreferencesContext";
|
||||
import {
|
||||
Boxes,
|
||||
ChevronDown,
|
||||
Download,
|
||||
FolderOpen,
|
||||
FolderSearch,
|
||||
Loader2,
|
||||
Plus,
|
||||
Server,
|
||||
@@ -117,6 +117,9 @@ export function SidebarTree({
|
||||
focusExistingTab?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const hostSwitchPlugins = usePluginHostSections().filter(
|
||||
(plugin) => !!plugin.contributes?.settings?.host?.enableKey,
|
||||
);
|
||||
// Knobs with no other owner come straight from the interface preset; the
|
||||
// ones above still come from the host sidebar preferences blob.
|
||||
const { showResourceBars, showStatusStripes, rowActions } =
|
||||
@@ -607,9 +610,6 @@ export function SidebarTree({
|
||||
credentialId: host.credentialId ? Number(host.credentialId) : null,
|
||||
overrideCredentialUsername: host.overrideCredentialUsername ?? false,
|
||||
enableSsh: host.enableSsh,
|
||||
enableTerminal: host.enableTerminal,
|
||||
enableTunnel: host.enableTunnel,
|
||||
enableFileManager: host.enableFileManager,
|
||||
sshPort: host.sshPort,
|
||||
rdpUser: host.rdpUser ?? null,
|
||||
rdpPassword: host.rdpPassword ?? null,
|
||||
@@ -622,7 +622,6 @@ export function SidebarTree({
|
||||
vncUser: host.vncUser ?? null,
|
||||
telnetUser: host.telnetUser ?? null,
|
||||
telnetPassword: host.telnetPassword ?? null,
|
||||
defaultPath: host.defaultPath ?? "/",
|
||||
forceKeyboardInteractive: host.forceKeyboardInteractive ?? false,
|
||||
useSocks5: host.useSocks5,
|
||||
socks5Host: host.socks5Host ?? null,
|
||||
@@ -634,7 +633,6 @@ export function SidebarTree({
|
||||
hostId: Number(j.hostId),
|
||||
})),
|
||||
portKnockSequence: host.portKnockSequence ?? [],
|
||||
tunnelConnections: host.serverTunnels ?? [],
|
||||
quickActions: (host.quickActions ?? []).map((a) => ({
|
||||
name: a.name,
|
||||
snippetId: Number(a.snippetId),
|
||||
@@ -1145,65 +1143,40 @@ export function SidebarTree({
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="text-xs">
|
||||
{[
|
||||
{
|
||||
labelKey: "hosts.enableTerminalFeature",
|
||||
field: "enableTerminal",
|
||||
value: true,
|
||||
icon: Terminal,
|
||||
},
|
||||
{
|
||||
labelKey: "hosts.disableTerminalFeature",
|
||||
field: "enableTerminal",
|
||||
value: false,
|
||||
icon: Terminal,
|
||||
},
|
||||
{
|
||||
labelKey: "hosts.enableFilesFeature",
|
||||
field: "enableFileManager",
|
||||
value: true,
|
||||
icon: FolderSearch,
|
||||
},
|
||||
{
|
||||
labelKey: "hosts.disableFilesFeature",
|
||||
field: "enableFileManager",
|
||||
value: false,
|
||||
icon: FolderSearch,
|
||||
},
|
||||
{
|
||||
labelKey: "hosts.enableProxmoxFeature",
|
||||
field: "enableProxmox",
|
||||
value: true,
|
||||
icon: Boxes,
|
||||
},
|
||||
{
|
||||
labelKey: "hosts.disableProxmoxFeature",
|
||||
field: "enableProxmox",
|
||||
value: false,
|
||||
icon: Boxes,
|
||||
},
|
||||
].map(({ labelKey, field, value, icon: Icon }) => (
|
||||
<DropdownMenuItem
|
||||
key={labelKey}
|
||||
onClick={async () => {
|
||||
const ids = Array.from(selectedHostIds).map(Number);
|
||||
try {
|
||||
await bulkUpdateSSHHosts(ids, { [field]: value });
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("termix:hosts-changed"),
|
||||
);
|
||||
toast.success(
|
||||
t("hosts.updatedCount", { count: ids.length }),
|
||||
);
|
||||
} catch {
|
||||
toast.error(t("hosts.bulkUpdateFailed"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon className="size-3.5 mr-2" />
|
||||
{t(labelKey)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{hostSwitchPlugins.flatMap((plugin) =>
|
||||
[true, false].map((value) => (
|
||||
<DropdownMenuItem
|
||||
key={`${plugin.id}:${value}`}
|
||||
onClick={async () => {
|
||||
const ids = Array.from(selectedHostIds).map(Number);
|
||||
try {
|
||||
await bulkUpdateSSHHosts(ids, {
|
||||
pluginEnable: { [plugin.id]: value },
|
||||
});
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("termix:hosts-changed"),
|
||||
);
|
||||
toast.success(
|
||||
t("hosts.updatedCount", { count: ids.length }),
|
||||
);
|
||||
} catch {
|
||||
toast.error(t("hosts.bulkUpdateFailed"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PluginIcon
|
||||
name={plugin.icon}
|
||||
className="size-3.5 mr-2"
|
||||
/>
|
||||
{t(
|
||||
value
|
||||
? "hosts.enablePluginFeature"
|
||||
: "hosts.disablePluginFeature",
|
||||
{ name: plugin.name },
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)),
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
|
||||
Reference in new issue
Block a user