chore: lock the plugin boundary and remove 2.9.0 leftovers

This commit is contained in:
LukeGus committed 2026-09-25 07:24:46 -05:00
1 parent 6489b62ece
commit 2961297989
399 files changed
+3964 -5627

No files matched your search

+1 -1
View File
@@ -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(
+18 -233
View File
@@ -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" && (
+2 -2
View File
@@ -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)}
/>
)}
+6 -8
View File
@@ -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)}
/>
)}
+1 -7
View File
@@ -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}`),
+3 -2
View File
@@ -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(
+55 -18
View File
@@ -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>
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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) ||
+2
View File
@@ -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;
+1 -1
View File
@@ -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",
+4 -1
View File
@@ -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,
};
}
+2 -6
View File
@@ -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;
+3 -3
View File
@@ -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 -2
View File
@@ -1,4 +1,3 @@
export { isFolder } from "./visible-rows";
export { HostItem } from "./HostItem/HostItem";
export { FolderItem } from "./FolderItem/FolderItem";
export { SidebarTree } from "./SidebarTree";