mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
chore: lock the plugin boundary and remove 2.9.0 leftovers
This commit is contained in:
1 parent
6489b62ece
commit
2961297989
399 files changed
+3964
-5627
No files matched your search
@@ -768,7 +768,7 @@ export function AdminSettingsPanel({
|
||||
const total =
|
||||
(s.sshHostsImported || 0) +
|
||||
(s.sshCredentialsImported || 0) +
|
||||
(s.fileManagerItemsImported || 0) +
|
||||
(s.pluginItemsImported || 0) +
|
||||
(s.dismissedAlertsImported || 0) +
|
||||
(s.settingsImported || 0);
|
||||
toast.success(
|
||||
|
||||
@@ -24,10 +24,6 @@ import {
|
||||
adminDeleteUserHost,
|
||||
adminGetUserCredentials,
|
||||
adminDeleteUserCredential,
|
||||
adminGetUserSnippets,
|
||||
adminCreateUserSnippet,
|
||||
adminUpdateUserSnippet,
|
||||
adminDeleteUserSnippet,
|
||||
adminResetUserPassword,
|
||||
adminExportUserData,
|
||||
getSessions,
|
||||
@@ -55,6 +51,7 @@ import { HostEditor } from "./HostEditor";
|
||||
import { mapCredentials, sshHostToHost } from "./HostManagerData";
|
||||
import type { AdminSession, AdminUser } from "./AdminManagementSections";
|
||||
import { makeCredentialTabs, makeHostTabs, TabStrip } from "./HostManagerTabs";
|
||||
import { useActionSlot } from "@/hooks/use-action-slot";
|
||||
|
||||
type ApiErrorLike = {
|
||||
response?: {
|
||||
@@ -65,18 +62,9 @@ type ApiErrorLike = {
|
||||
};
|
||||
};
|
||||
|
||||
// user-snippets rather than the snippets plugin's own id: this is a tab key
|
||||
// in this admin panel's own local state, not a lookup into the plugin registry.
|
||||
type ManageTabId =
|
||||
"account" | "hosts" | "credentials" | "user-snippets" | "sessions" | "danger";
|
||||
|
||||
type ManagedSnippet = {
|
||||
id: number;
|
||||
name: string;
|
||||
content: string;
|
||||
description?: string | null;
|
||||
folder?: string | null;
|
||||
};
|
||||
// Core tabs, or "plugin:<actionId>" for a tab a plugin contributes to the
|
||||
// admin.userTabs slot.
|
||||
type ManageTabId = string;
|
||||
|
||||
type EditorState =
|
||||
| { kind: "host"; host: Host | null }
|
||||
@@ -91,19 +79,6 @@ function apiErrorMessage(error: unknown, fallback: string) {
|
||||
return (error as ApiErrorLike).response?.data?.error || fallback;
|
||||
}
|
||||
|
||||
function mapSnippets(res: unknown): ManagedSnippet[] {
|
||||
const list = Array.isArray(res)
|
||||
? res
|
||||
: ((res as { snippets?: unknown[] })?.snippets ?? []);
|
||||
return (list as Record<string, unknown>[]).map((s) => ({
|
||||
id: Number(s.id),
|
||||
name: String(s.name ?? ""),
|
||||
content: String(s.content ?? ""),
|
||||
description: (s.description as string | null) ?? null,
|
||||
folder: (s.folder as string | null) ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
export function AdminUserManagePanel({
|
||||
user,
|
||||
roles,
|
||||
@@ -150,23 +125,11 @@ export function AdminUserManagePanel({
|
||||
|
||||
const [hosts, setHosts] = useState<Host[]>([]);
|
||||
const [credentials, setCredentials] = useState<Credential[]>([]);
|
||||
const [snippets, setSnippets] = useState<ManagedSnippet[]>([]);
|
||||
const [sessions, setSessions] = useState<AdminSession[]>([]);
|
||||
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||
const [userRoles, setUserRoles] = useState<UserRole[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Snippet inline editor
|
||||
const [editingSnippet, setEditingSnippet] = useState<
|
||||
ManagedSnippet | "new" | null
|
||||
>(null);
|
||||
const [snippetForm, setSnippetForm] = useState({
|
||||
name: "",
|
||||
content: "",
|
||||
folder: "",
|
||||
});
|
||||
const [snippetSaving, setSnippetSaving] = useState(false);
|
||||
|
||||
// Account tab state
|
||||
const [resetPassword, setResetPassword] = useState("");
|
||||
const [resetLoading, setResetLoading] = useState(false);
|
||||
@@ -194,13 +157,6 @@ export function AdminUserManagePanel({
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const reloadSnippets = () => {
|
||||
if (!dataUnlocked) return;
|
||||
adminGetUserSnippets(user.id)
|
||||
.then((res) => setSnippets(mapSnippets(res)))
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const reloadSessions = () => {
|
||||
getSessions()
|
||||
.then(({ sessions: s }) =>
|
||||
@@ -214,12 +170,11 @@ export function AdminUserManagePanel({
|
||||
Promise.allSettled([
|
||||
dataUnlocked ? adminGetUserHosts(user.id) : Promise.resolve([]),
|
||||
dataUnlocked ? adminGetUserCredentials(user.id) : Promise.resolve([]),
|
||||
dataUnlocked ? adminGetUserSnippets(user.id) : Promise.resolve([]),
|
||||
getSessions(),
|
||||
getApiKeys(),
|
||||
getUserRoles(user.id),
|
||||
])
|
||||
.then(([h, c, s, sess, keys, ur]) => {
|
||||
.then(([h, c, sess, keys, ur]) => {
|
||||
if (h.status === "fulfilled" && dataUnlocked) {
|
||||
setHosts(
|
||||
(h.value as SSHHostWithStatus[]).map((raw) => sshHostToHost(raw)),
|
||||
@@ -228,9 +183,6 @@ export function AdminUserManagePanel({
|
||||
if (c.status === "fulfilled" && dataUnlocked) {
|
||||
setCredentials(mapCredentials(c.value));
|
||||
}
|
||||
if (s.status === "fulfilled" && dataUnlocked) {
|
||||
setSnippets(mapSnippets(s.value));
|
||||
}
|
||||
if (sess.status === "fulfilled") {
|
||||
setSessions(
|
||||
sess.value.sessions.filter((session) => session.userId === user.id),
|
||||
@@ -246,11 +198,21 @@ export function AdminUserManagePanel({
|
||||
.finally(() => setLoading(false));
|
||||
}, [user.id, dataUnlocked]);
|
||||
|
||||
const pluginTabs = useActionSlot("admin.userTabs").filter(
|
||||
(contribution) =>
|
||||
contribution.kind === "component" && contribution.component,
|
||||
);
|
||||
const activePluginTab = pluginTabs.find(
|
||||
(contribution) => `plugin:${contribution.actionId}` === activeTab,
|
||||
);
|
||||
const manageTabs: { id: ManageTabId; label: string }[] = [
|
||||
{ id: "account", label: t("admin.manageTabAccount") },
|
||||
{ id: "hosts", label: t("admin.manageTabHosts") },
|
||||
{ id: "credentials", label: t("admin.manageTabCredentials") },
|
||||
{ id: "user-snippets", label: t("admin.manageTabSnippets") },
|
||||
...pluginTabs.map((contribution) => ({
|
||||
id: `plugin:${contribution.actionId}`,
|
||||
label: t(contribution.titleKey),
|
||||
})),
|
||||
{ id: "sessions", label: t("admin.manageTabSessions") },
|
||||
{ id: "danger", label: t("admin.manageTabDanger") },
|
||||
];
|
||||
@@ -348,33 +310,6 @@ export function AdminUserManagePanel({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveSnippet() {
|
||||
if (!snippetForm.name.trim() || !snippetForm.content.trim()) {
|
||||
toast.error(t("admin.snippetRequiredFields"));
|
||||
return;
|
||||
}
|
||||
setSnippetSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
name: snippetForm.name.trim(),
|
||||
content: snippetForm.content,
|
||||
folder: snippetForm.folder.trim() || null,
|
||||
};
|
||||
if (editingSnippet !== "new" && editingSnippet) {
|
||||
await adminUpdateUserSnippet(user.id, editingSnippet.id, payload);
|
||||
} else {
|
||||
await adminCreateUserSnippet(user.id, payload);
|
||||
}
|
||||
setEditingSnippet(null);
|
||||
reloadSnippets();
|
||||
toast.success(t("admin.snippetSaved"));
|
||||
} catch (e) {
|
||||
toast.error(apiErrorMessage(e, t("admin.snippetSaveFailed")));
|
||||
} finally {
|
||||
setSnippetSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
const lockedNotice = (
|
||||
<div className="flex items-start gap-2.5 p-3 mt-2 border border-border bg-muted/20 text-xs text-muted-foreground">
|
||||
<Lock className="size-3.5 shrink-0 mt-0.5" />
|
||||
@@ -939,161 +874,11 @@ export function AdminUserManagePanel({
|
||||
</div>
|
||||
))}
|
||||
|
||||
{activeTab === "user-snippets" &&
|
||||
{activePluginTab &&
|
||||
(!dataUnlocked ? (
|
||||
lockedNotice
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center justify-between py-2 border-b border-border">
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("admin.snippetsCount", { count: snippets.length })}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-6 text-muted-foreground hover:text-foreground"
|
||||
onClick={reloadSnippets}
|
||||
>
|
||||
<RefreshCw className="size-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-[10px] border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
|
||||
onClick={() => {
|
||||
setSnippetForm({ name: "", content: "", folder: "" });
|
||||
setEditingSnippet("new");
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3" />
|
||||
{t("admin.addSnippetForUser")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{editingSnippet && (
|
||||
<div className="flex flex-col gap-2.5 py-3 border-b border-border">
|
||||
<Input
|
||||
className="h-8 text-xs"
|
||||
placeholder={t("admin.snippetNamePlaceholder")}
|
||||
value={snippetForm.name}
|
||||
onChange={(e) =>
|
||||
setSnippetForm((p) => ({ ...p, name: e.target.value }))
|
||||
}
|
||||
/>
|
||||
<textarea
|
||||
rows={4}
|
||||
placeholder={t("admin.snippetContentPlaceholder")}
|
||||
value={snippetForm.content}
|
||||
onChange={(e) =>
|
||||
setSnippetForm((p) => ({
|
||||
...p,
|
||||
content: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="w-full px-3 py-2 text-[10px] bg-background border border-border text-foreground placeholder:text-muted-foreground resize-none outline-none focus:ring-1 focus:ring-ring font-mono"
|
||||
/>
|
||||
<Input
|
||||
className="h-8 text-xs"
|
||||
placeholder={t("admin.snippetFolderPlaceholder")}
|
||||
value={snippetForm.folder}
|
||||
onChange={(e) =>
|
||||
setSnippetForm((p) => ({
|
||||
...p,
|
||||
folder: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 text-[10px]"
|
||||
onClick={() => setEditingSnippet(null)}
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-[10px] border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand"
|
||||
disabled={snippetSaving}
|
||||
onClick={handleSaveSnippet}
|
||||
>
|
||||
{snippetSaving ? t("hosts.savingBtn") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!loading && snippets.length === 0 && !editingSnippet && (
|
||||
<span className="text-xs text-muted-foreground py-3">
|
||||
{t("admin.noSnippetsForUser")}
|
||||
</span>
|
||||
)}
|
||||
{snippets.map((snippet) => (
|
||||
<div
|
||||
key={snippet.id}
|
||||
className="flex items-center justify-between py-2.5 border-b border-border last:border-0"
|
||||
>
|
||||
<div className="flex flex-col gap-0.5 min-w-0">
|
||||
<span className="text-xs font-semibold truncate max-w-[180px]">
|
||||
{snippet.name}
|
||||
</span>
|
||||
<span className="text-[10px] font-mono text-muted-foreground truncate max-w-[220px]">
|
||||
{snippet.content}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-6 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
setSnippetForm({
|
||||
name: snippet.name,
|
||||
content: snippet.content,
|
||||
folder: snippet.folder ?? "",
|
||||
});
|
||||
setEditingSnippet(snippet);
|
||||
}}
|
||||
>
|
||||
<Pencil className="size-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-6 text-muted-foreground hover:text-destructive"
|
||||
onClick={() =>
|
||||
setConfirmDialog({
|
||||
message: t("admin.deleteSnippetConfirm", {
|
||||
name: snippet.name,
|
||||
username: user.username,
|
||||
}),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await adminDeleteUserSnippet(user.id, snippet.id);
|
||||
setSnippets((prev) =>
|
||||
prev.filter((s) => s.id !== snippet.id),
|
||||
);
|
||||
toast.success(t("admin.snippetDeletedSuccess"));
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
apiErrorMessage(
|
||||
e,
|
||||
t("admin.snippetDeleteFailed"),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<Trash2 className="size-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<activePluginTab.component user={user} />
|
||||
))}
|
||||
|
||||
{activeTab === "sessions" && (
|
||||
|
||||
@@ -294,13 +294,13 @@ export function CredentialEditorView({
|
||||
onChange={(e) => setCredField("password", e.target.value)}
|
||||
/>
|
||||
<PluginComponent
|
||||
id="secret-sources.hint"
|
||||
id="credentials.secretHint"
|
||||
onManage={() => setShowSecretSources((v) => !v)}
|
||||
/>
|
||||
</div>
|
||||
{showSecretSources && (
|
||||
<PluginComponent
|
||||
id="secret-sources.manager"
|
||||
id="credentials.secretManager"
|
||||
onClose={() => setShowSecretSources(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -34,18 +34,17 @@ import {
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { listSnippets } from "@/lib/snippet-provider";
|
||||
import { SectionCard, SettingRow, FakeSwitch } from "@/components/section-card";
|
||||
import { TerminalPreview } from "@/components/terminal-preview/TerminalPreview";
|
||||
import {
|
||||
createSSHHost,
|
||||
updateSSHHost,
|
||||
getSnippets,
|
||||
getUserInfo,
|
||||
getHostPassword,
|
||||
adminCreateUserHost,
|
||||
adminUpdateUserHost,
|
||||
adminGetHostPassword,
|
||||
adminGetUserSnippets,
|
||||
createCredential,
|
||||
adminCreateUserCredential,
|
||||
} from "@/main-axios";
|
||||
@@ -240,10 +239,9 @@ export function HostEditor({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadSnippets = adminTargetUserId
|
||||
? adminGetUserSnippets(adminTargetUserId)
|
||||
: getSnippets();
|
||||
loadSnippets
|
||||
listSnippets(
|
||||
adminTargetUserId ? { targetUserId: adminTargetUserId } : undefined,
|
||||
)
|
||||
.then((res) => setSnippets(mapSnippetResponse(res)))
|
||||
.catch(() => {});
|
||||
}, [adminTargetUserId]);
|
||||
@@ -690,7 +688,7 @@ export function HostEditor({
|
||||
onChange={(e) => setField("password", e.target.value)}
|
||||
/>
|
||||
<PluginComponent
|
||||
id="secret-sources.hint"
|
||||
id="credentials.secretHint"
|
||||
onManage={() => setShowSecretSources((v) => !v)}
|
||||
/>
|
||||
</div>
|
||||
@@ -698,7 +696,7 @@ export function HostEditor({
|
||||
{(authMethod === "password" || authMethod === "key") &&
|
||||
showSecretSources && (
|
||||
<PluginComponent
|
||||
id="secret-sources.manager"
|
||||
id="credentials.secretManager"
|
||||
onClose={() => setShowSecretSources(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -21,7 +21,6 @@ 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,
|
||||
type UiAreaKey,
|
||||
type UiPreset,
|
||||
@@ -39,9 +38,6 @@ const PRESET_ICONS = {
|
||||
advanced: Wrench,
|
||||
} as const;
|
||||
|
||||
/** Areas that have nothing worth listing as a per-area override. */
|
||||
const HIDDEN_OVERRIDE_AREAS = new Set<UiAreaKey>([HOMEPAGE_AREA_KEY]);
|
||||
|
||||
export function InterfacePresetSettings({
|
||||
onRunSetupAgain,
|
||||
}: {
|
||||
@@ -67,9 +63,7 @@ export function InterfacePresetSettings({
|
||||
Record<string, unknown> | undefined
|
||||
>;
|
||||
const core = UI_AREA_KEYS.filter(
|
||||
(area) =>
|
||||
!HIDDEN_OVERRIDE_AREAS.has(area) &&
|
||||
Object.keys(overrides[area] ?? {}).length > 0,
|
||||
(area) => Object.keys(overrides[area] ?? {}).length > 0,
|
||||
).map((area) => ({
|
||||
key: area as string,
|
||||
label: t(`newUi.sidebar.userProfile.uiArea_${area}`),
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
getUserPreferences,
|
||||
parseCustomKeybindings,
|
||||
} from "@/api/open-tabs-api";
|
||||
import { saveUserPreferences, getSnippets } from "@/main-axios";
|
||||
import { saveUserPreferences } from "@/main-axios";
|
||||
import { listSnippets } from "@/lib/snippet-provider";
|
||||
import { BUILT_IN_DEFAULTS } from "@/lib/default-keybindings";
|
||||
import type {
|
||||
CustomKeybinding,
|
||||
@@ -87,7 +88,7 @@ export function KeybindingsDialog({
|
||||
setLoading(true);
|
||||
Promise.all([
|
||||
getUserPreferences().catch(() => null),
|
||||
getSnippets().catch(() => []),
|
||||
listSnippets().catch(() => []),
|
||||
])
|
||||
.then(([prefs, snippetList]) => {
|
||||
setBindings(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Eye, EyeOff, FolderSearch, Terminal } from "lucide-react";
|
||||
import { Eye, EyeOff, Terminal } from "lucide-react";
|
||||
import { Input } from "@/components/input";
|
||||
import type { Host } from "@/types/ui-types";
|
||||
import { getCredentials } from "@/api/credentials-api";
|
||||
@@ -9,6 +9,12 @@ import { createQuickConnectHost } from "./quick-connect-host";
|
||||
import { useHostProtocols } from "./host-protocols";
|
||||
import { Select2 } from "@/components/select2";
|
||||
import { resolveHostTabType } from "@/lib/host-connection-tabs";
|
||||
import { useSshAuthProviders } from "@/hooks/useSshAuthProviders";
|
||||
import { useSshAuthEditors } from "@/plugin-host/auth-registry";
|
||||
import { useHostActions } from "./host-contributions";
|
||||
|
||||
// Core types Quick Connect draws its own fields for.
|
||||
const INLINE_AUTH_TYPES = new Set(["password", "key", "credential"]);
|
||||
|
||||
interface QuickConnectPanelProps {
|
||||
onConnect: (host: Host, type: string) => void;
|
||||
@@ -22,9 +28,8 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
||||
const [port, setPort] = useState("22");
|
||||
const [domain, setDomain] = useState("");
|
||||
const [username, setUsername] = useState("root");
|
||||
const [authType, setAuthType] = useState<"password" | "key" | "credential">(
|
||||
"password",
|
||||
);
|
||||
const [authType, setAuthType] = useState("password");
|
||||
const [authFields, setAuthFields] = useState<Record<string, unknown>>({});
|
||||
const [password, setPassword] = useState("");
|
||||
const [privateKey, setPrivateKey] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
@@ -39,6 +44,21 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Every registered SSH auth type that works for a host that is never saved.
|
||||
const { providers } = useSshAuthProviders();
|
||||
const authOptions = providers.filter(
|
||||
(option) => option.available && option.quickConnect,
|
||||
);
|
||||
const authEditor = useSshAuthEditors().find(
|
||||
(editor) => editor.id === authType,
|
||||
);
|
||||
const AuthEditor = INLINE_AUTH_TYPES.has(authType)
|
||||
? undefined
|
||||
: authEditor?.component;
|
||||
const quickActions = useHostActions().filter(
|
||||
(action) => action.quickConnect && action.tabType,
|
||||
);
|
||||
|
||||
const pluginProtocols = useHostProtocols().filter(
|
||||
(entry) => entry.quickConnect,
|
||||
);
|
||||
@@ -67,6 +87,7 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
||||
credentialId,
|
||||
protocol: selected,
|
||||
domain: domain || undefined,
|
||||
authFields: INLINE_AUTH_TYPES.has(authType) ? undefined : authFields,
|
||||
});
|
||||
const target = type ?? resolveHostTabType(hostConfig);
|
||||
if (target) onConnect(hostConfig, target);
|
||||
@@ -152,18 +173,21 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
||||
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
|
||||
{t("newUi.sidebar.quickConnect.authLabel")}
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
{(["password", "key", "credential"] as const).map((type) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{authOptions.map((option) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setAuthType(type)}
|
||||
className={`flex-1 py-1 text-[10px] font-semibold border transition-colors capitalize ${
|
||||
authType === type
|
||||
key={option.type}
|
||||
onClick={() => {
|
||||
setAuthType(option.type);
|
||||
setAuthFields({});
|
||||
}}
|
||||
className={`flex-1 py-1 px-1.5 text-[10px] font-semibold border transition-colors ${
|
||||
authType === option.type
|
||||
? "border-accent-brand/40 bg-accent-brand/10 text-accent-brand"
|
||||
: "border-border text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{type}
|
||||
{t(option.editorTitleKey ?? option.labelKey)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -257,6 +281,14 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
||||
</Select2>
|
||||
</div>
|
||||
)}
|
||||
{!isDesktop && AuthEditor && (
|
||||
<AuthEditor
|
||||
form={authFields}
|
||||
setField={(key, value) =>
|
||||
setAuthFields((current) => ({ ...current, [key]: value }))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-1.5 pt-1">
|
||||
{selected ? (
|
||||
<button
|
||||
@@ -277,13 +309,18 @@ export function QuickConnectPanel({ onConnect }: QuickConnectPanelProps) {
|
||||
<Terminal className="size-3.5" />
|
||||
{t("newUi.sidebar.quickConnect.connectToTerminal")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => connect("files")}
|
||||
className="flex items-center justify-center gap-1.5 h-7 w-full border border-accent-brand/40 bg-accent-brand/10 text-accent-brand text-xs font-semibold hover:bg-accent-brand/20 transition-colors"
|
||||
>
|
||||
<FolderSearch className="size-3.5" />
|
||||
{t("newUi.sidebar.quickConnect.connectToFiles")}
|
||||
</button>
|
||||
{quickActions.map((action) => (
|
||||
<button
|
||||
key={action.id}
|
||||
onClick={() => connect(action.tabType)}
|
||||
className="flex items-center justify-center gap-1.5 h-7 w-full border border-accent-brand/40 bg-accent-brand/10 text-accent-brand text-xs font-semibold hover:bg-accent-brand/20 transition-colors"
|
||||
>
|
||||
<action.icon className="size-3.5" />
|
||||
{t("newUi.sidebar.quickConnect.connectToAction", {
|
||||
name: t(action.titleKey),
|
||||
})}
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1256,7 +1256,7 @@ export function UserProfilePanel({
|
||||
const total =
|
||||
(s.sshHostsImported || 0) +
|
||||
(s.sshCredentialsImported || 0) +
|
||||
(s.fileManagerItemsImported || 0) +
|
||||
(s.pluginItemsImported || 0) +
|
||||
(s.dismissedAlertsImported || 0) +
|
||||
(s.settingsImported || 0);
|
||||
toast.success(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const BRANDING_LOGO_MAX_BYTES = 750 * 1024;
|
||||
export const BRANDING_LOGO_ACCEPT = "image/png,image/jpeg,image/webp";
|
||||
export const BRANDING_LOGO_ACCEPTED_TYPES = new Set([
|
||||
const BRANDING_LOGO_ACCEPTED_TYPES = new Set([
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/webp",
|
||||
|
||||
@@ -19,14 +19,6 @@ import type {
|
||||
CredentialTrayTrigger,
|
||||
} from "@/types/credential-sidebar-preferences";
|
||||
|
||||
export function credentialMatchesQuery(cred: Credential, query: string) {
|
||||
return (
|
||||
cred.name.toLowerCase().includes(query) ||
|
||||
cred.username.toLowerCase().includes(query) ||
|
||||
cred.tags?.some((t) => t.toLowerCase().includes(query))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-density layout knobs, mirroring HOST_ITEM_DENSITY_TOKENS in
|
||||
* HostItem.tsx -- one implementation with a token lookup instead of two
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { CredentialSidebarTree } from "./CredentialSidebarTree";
|
||||
export { isFolder, type CredentialFolder } from "./visible-rows";
|
||||
export { type CredentialFolder } from "./visible-rows";
|
||||
@@ -17,7 +17,7 @@ export function isFolder(
|
||||
return "children" in item;
|
||||
}
|
||||
|
||||
export function credentialMatchesQuery(cred: Credential, query: string) {
|
||||
function credentialMatchesQuery(cred: Credential, query: string) {
|
||||
return (
|
||||
cred.name.toLowerCase().includes(query) ||
|
||||
cred.username.toLowerCase().includes(query) ||
|
||||
|
||||
@@ -24,6 +24,8 @@ export interface HostActionDef {
|
||||
copyUrlView?: string;
|
||||
/** Where the dashboard's host status list sends a click. */
|
||||
overview?: boolean;
|
||||
/** Offered as a button in Quick Connect, opening tabType. */
|
||||
quickConnect?: boolean;
|
||||
order?: number;
|
||||
/** A label worked out per host, e.g. a single endpoint's name. */
|
||||
label?: (host: Host) => string | undefined;
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ExportPayload {
|
||||
hosts: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
export const FIELD_GROUP_KEYS: Record<FieldGroup, string[]> = {
|
||||
const FIELD_GROUP_KEYS: Record<FieldGroup, string[]> = {
|
||||
connection: [
|
||||
"connectionType",
|
||||
"name",
|
||||
|
||||
@@ -9,9 +9,11 @@ type QuickConnectInput = Pick<
|
||||
/** A plugin protocol; SSH when omitted. */
|
||||
protocol?: HostProtocolDef;
|
||||
domain?: string;
|
||||
/** Fields a plugin's SSH auth editor filled in. */
|
||||
authFields?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export const QUICK_CONNECT_ID_PREFIX = "quick-connect-";
|
||||
const QUICK_CONNECT_ID_PREFIX = "quick-connect-";
|
||||
|
||||
export function isQuickConnectHost(host: Pick<Host, "id">): boolean {
|
||||
return host.id.startsWith(QUICK_CONNECT_ID_PREFIX);
|
||||
@@ -60,6 +62,7 @@ export function createQuickConnectHost(input: QuickConnectInput): Host {
|
||||
quickActions: [],
|
||||
enableSsh: true,
|
||||
sshPort: input.port,
|
||||
...input.authFields,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ export interface RailItemDef {
|
||||
electronOnly?: boolean;
|
||||
/** False keeps it out of the Navigation visibility toggles. */
|
||||
hideable?: boolean;
|
||||
/** A plugin item that stays visible in the Simple preset. */
|
||||
simplePreset?: boolean;
|
||||
/** Registered but not shown, e.g. while its feature is switched off. */
|
||||
hidden?: boolean;
|
||||
/** Places a registered item after this id instead of at the end. */
|
||||
@@ -123,12 +125,6 @@ export function registerRailItem(def: RailItemDef): () => void {
|
||||
return registeredRailItems.register(def);
|
||||
}
|
||||
|
||||
export function unregisterRailItem(id: string): void {
|
||||
registeredRailItems.unregister(id);
|
||||
}
|
||||
|
||||
export const subscribeToRailItems = registeredRailItems.subscribe;
|
||||
|
||||
/** Registered items, hidden ones included. */
|
||||
export const listRegisteredRailItems = registeredRailItems.list;
|
||||
|
||||
|
||||
@@ -117,11 +117,11 @@ function actionOrder(action: HostActionDef): number {
|
||||
return action.order ?? (action.kind === "connect" ? 100 : 50);
|
||||
}
|
||||
|
||||
export async function writeClipboardText(value: string): Promise<void> {
|
||||
async function writeClipboardText(value: string): Promise<void> {
|
||||
await copyToClipboard(value);
|
||||
}
|
||||
|
||||
export function canCopyHostPassword(host: Host): boolean {
|
||||
function canCopyHostPassword(host: Host): boolean {
|
||||
return (
|
||||
host.authType === "password" ||
|
||||
host.authType === "credential" ||
|
||||
@@ -130,7 +130,7 @@ export function canCopyHostPassword(host: Host): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function canCopyHostSudoPassword(host: Host): boolean {
|
||||
function canCopyHostSudoPassword(host: Host): boolean {
|
||||
return (
|
||||
!!host.hasSudoPassword ||
|
||||
!!host.sudoPassword ||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { isFolder } from "./visible-rows";
|
||||
export { HostItem } from "./HostItem/HostItem";
|
||||
export { FolderItem } from "./FolderItem/FolderItem";
|
||||
|
||||
export { SidebarTree } from "./SidebarTree";
|
||||
Reference in new issue
Block a user