From 1eac0f54b0e9ab5557033e1943e7785700dd6640 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Wed, 23 Sep 2026 08:59:06 -0500 Subject: [PATCH] feat: point core frontend snippets pickers at the plugin (B2) --- src/ui/AppShell.tsx | 24 - src/ui/api/admin-user-data-api.ts | 16 +- src/ui/api/open-tabs-api.ts | 1 - src/ui/api/rbac-api.ts | 86 - src/ui/api/snippets-api.ts | 171 +- src/ui/features/terminal/Terminal.tsx | 50 +- src/ui/hooks/use-snippet-runner.tsx | 96 +- src/ui/lib/keybinding-dispatch.ts | 41 +- src/ui/locales/en.json | 4 - src/ui/main-axios.ts | 20 +- src/ui/onboarding/steps/FeaturesStep.tsx | 3 +- src/ui/plugin-host/bridge.ts | 5 +- src/ui/shell/shell-layout.ts | 2 - src/ui/shell/tabUtils.tsx | 17 - src/ui/sidebar/AdminUserManagePanel.tsx | 8 +- src/ui/sidebar/SnippetsPanel.tsx | 2403 ----------------- src/ui/sidebar/UserProfilePanel.tsx | 34 +- src/ui/sidebar/rail-items.ts | 10 - src/ui/tests/lib/keybinding-dispatch.test.ts | 51 +- src/ui/tests/lib/snippet-variables.test.ts | 79 - .../tests/onboarding/onboarding-steps.test.ts | 2 +- src/ui/tests/sidebar/rail-items.test.ts | 15 +- 22 files changed, 170 insertions(+), 2968 deletions(-) delete mode 100644 src/ui/sidebar/SnippetsPanel.tsx delete mode 100644 src/ui/tests/lib/snippet-variables.test.ts diff --git a/src/ui/AppShell.tsx b/src/ui/AppShell.tsx index 2df54e6be..1d8d76ba9 100644 --- a/src/ui/AppShell.tsx +++ b/src/ui/AppShell.tsx @@ -86,9 +86,6 @@ const SshToolsPanel = lazy(() => const CollabPanel = lazy(() => import("@/sidebar/CollabPanel").then((m) => ({ default: m.CollabPanel })), ); -const SnippetsPanel = lazy(() => - import("@/sidebar/SnippetsPanel").then((m) => ({ default: m.SnippetsPanel })), -); const MacrosPanel = lazy(() => import("@/sidebar/MacrosPanel").then((m) => ({ default: m.MacrosPanel })), ); @@ -1018,7 +1015,6 @@ export function AppShell({ "hostTrayOnClick", "pinAppRail", "expandAppRailOnHover", - "defaultSnippetFoldersCollapsed", "confirmSnippetExecution", "disableUpdateCheck", "confirmTabClose", @@ -1086,14 +1082,6 @@ export function AppShell({ ); window.dispatchEvent(new Event("expandAppRailOnHoverChanged")); } - if ( - prefs.foldersCollapsed !== null && - prefs.foldersCollapsed !== undefined - ) - localStorage.setItem( - "defaultSnippetFoldersCollapsed", - String(prefs.foldersCollapsed), - ); if ( prefs.confirmSnippetExecution !== null && prefs.confirmSnippetExecution !== undefined @@ -2570,18 +2558,6 @@ export function AppShell({ )} - {railView === "snippets" && ( -
- -
- )} - {railView === "macros" && (
> { try { - const response = await authApi.get("/snippets", { + const response = await authApi.get("/plugin-api/snippets", { headers: adminHeaders(targetUserId), }); return response.data; @@ -209,7 +209,7 @@ export async function adminCreateUserSnippet( snippetData: Record, ): Promise> { try { - const response = await authApi.post("/snippets", snippetData, { + const response = await authApi.post("/plugin-api/snippets", snippetData, { headers: adminHeaders(targetUserId), }); return response.data; @@ -224,9 +224,13 @@ export async function adminUpdateUserSnippet( snippetData: Record, ): Promise> { try { - const response = await authApi.put(`/snippets/${snippetId}`, snippetData, { - headers: adminHeaders(targetUserId), - }); + const response = await authApi.put( + `/plugin-api/snippets/${snippetId}`, + snippetData, + { + headers: adminHeaders(targetUserId), + }, + ); return response.data; } catch (error) { throw handleApiError(error, "update user's snippet"); @@ -238,7 +242,7 @@ export async function adminDeleteUserSnippet( snippetId: number, ): Promise> { try { - const response = await authApi.delete(`/snippets/${snippetId}`, { + const response = await authApi.delete(`/plugin-api/snippets/${snippetId}`, { headers: adminHeaders(targetUserId), }); return response.data; diff --git a/src/ui/api/open-tabs-api.ts b/src/ui/api/open-tabs-api.ts index 4320ab7fc..c4615b4b4 100644 --- a/src/ui/api/open-tabs-api.ts +++ b/src/ui/api/open-tabs-api.ts @@ -115,7 +115,6 @@ export interface UserPreferences { pinAppRail?: boolean | null; expandAppRailOnHover?: boolean | null; showPinAppRailButton?: boolean | null; - foldersCollapsed?: boolean | null; confirmSnippetExecution?: boolean | null; disableUpdateCheck?: boolean | null; confirmTabClose?: boolean | null; diff --git a/src/ui/api/rbac-api.ts b/src/ui/api/rbac-api.ts index 77e661cd8..1de0286b2 100644 --- a/src/ui/api/rbac-api.ts +++ b/src/ui/api/rbac-api.ts @@ -397,47 +397,6 @@ export async function setHostAuthOverride( } } -// ============================================================================ -// SNIPPET SHARING -// ============================================================================ - -export async function shareSnippet( - snippetId: number, - shareData: { - targetType: "user" | "role"; - targetUserId?: string; - targetRoleId?: number; - durationHours?: number; - }, -): Promise<{ success: boolean }> { - try { - const response = await rbacApi.post( - `/rbac/snippet/${snippetId}/share`, - shareData, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "share snippet"); - } -} - -export async function shareSnippetFolder( - folder: string, - targets: ShareTarget[], - durationHours?: number, -): Promise<{ success: boolean; snippetsShared: number }> { - try { - const response = await rbacApi.post("/rbac/snippet-folder/share", { - folder, - targets, - durationHours, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "share snippet folder"); - } -} - export type CredentialPermissionLevel = "use" | "manage"; export async function shareCredential( @@ -480,48 +439,3 @@ export async function revokeCredentialAccess( throw handleApiError(error, "revoke credential access"); } } - -export async function getSnippetAccess( - snippetId: number, -): Promise<{ accessList: AccessRecord[] }> { - try { - const response = await rbacApi.get(`/rbac/snippet/${snippetId}/access`); - return response.data; - } catch (error) { - throw handleApiError(error, "fetch snippet access"); - } -} - -export async function revokeSnippetAccess( - snippetId: number, - accessId: number, -): Promise<{ success: boolean }> { - try { - const response = await rbacApi.delete( - `/rbac/snippet/${snippetId}/access/${accessId}`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "revoke snippet access"); - } -} - -export async function getSharedSnippets(): Promise<{ - sharedSnippets: Array<{ - id: number; - name: string; - content: string; - description: string | null; - folder: string | null; - ownerUsername: string; - permissionLevel: string; - expiresAt: string | null; - }>; -}> { - try { - const response = await rbacApi.get("/rbac/shared-snippets"); - return response.data; - } catch (error) { - handleApiError(error, "fetch shared snippets"); - } -} diff --git a/src/ui/api/snippets-api.ts b/src/ui/api/snippets-api.ts index 3eb043032..155fe49e9 100644 --- a/src/ui/api/snippets-api.ts +++ b/src/ui/api/snippets-api.ts @@ -3,6 +3,12 @@ import { authApi, handleApiError } from "@/main-axios"; /** * A snippet row as the list endpoint returns it. Callers that need the full * shape narrow it themselves; only the id is relied on across the app. + * + * Snippets themselves are owned by the snippets plugin + * (/plugin-api/snippets); this is the thin read/run surface used by a few + * core and plugin callers (the host editor's startup snippet, the command + * palette, custom keybindings, host-metrics quick actions) that cannot + * reach into the snippets plugin's own frontend code. */ export interface SnippetRow { id: number; @@ -11,54 +17,20 @@ export interface SnippetRow { export async function getSnippets(): Promise { try { - const response = await authApi.get("/snippets"); + const response = await authApi.get("/plugin-api/snippets"); return response.data; } catch (error) { throw handleApiError(error, "fetch snippets"); } } -export async function createSnippet( - snippetData: Record, -): Promise> { - try { - const response = await authApi.post("/snippets", snippetData); - return response.data; - } catch (error) { - throw handleApiError(error, "create snippet"); - } -} - -export async function updateSnippet( - snippetId: number, - snippetData: Record, -): Promise> { - try { - const response = await authApi.put(`/snippets/${snippetId}`, snippetData); - return response.data; - } catch (error) { - throw handleApiError(error, "update snippet"); - } -} - -export async function deleteSnippet( - snippetId: number, -): Promise> { - try { - const response = await authApi.delete(`/snippets/${snippetId}`); - return response.data; - } catch (error) { - throw handleApiError(error, "delete snippet"); - } -} - export async function executeSnippet( snippetId: number, hostId: number, inputValues?: Record, ): Promise<{ success: boolean; output: string; error?: string }> { try { - const response = await authApi.post("/snippets/execute", { + const response = await authApi.post("/plugin-api/snippets/execute", { snippetId, hostId, ...(inputValues ? { inputValues } : {}), @@ -68,130 +40,3 @@ export async function executeSnippet( throw handleApiError(error, "execute snippet"); } } - -export async function getSnippetFolders(): Promise> { - try { - const response = await authApi.get("/snippets/folders"); - return response.data; - } catch (error) { - throw handleApiError(error, "fetch snippet folders"); - } -} - -export async function createSnippetFolder(folderData: { - name: string; - color?: string; - icon?: string; -}): Promise> { - try { - const response = await authApi.post("/snippets/folders", folderData); - return response.data; - } catch (error) { - throw handleApiError(error, "create snippet folder"); - } -} - -export async function updateSnippetFolderMetadata( - folderName: string, - metadata: { color?: string; icon?: string }, -): Promise> { - try { - const response = await authApi.put( - `/snippets/folders/${encodeURIComponent(folderName)}/metadata`, - metadata, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "update snippet folder metadata"); - } -} - -export async function renameSnippetFolder( - oldName: string, - newName: string, -): Promise<{ success: boolean; oldName: string; newName: string }> { - try { - const response = await authApi.put("/snippets/folders/rename", { - oldName, - newName, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "rename snippet folder"); - } -} - -export async function deleteSnippetFolder( - folderName: string, -): Promise<{ success: boolean }> { - try { - const response = await authApi.delete( - `/snippets/folders/${encodeURIComponent(folderName)}`, - ); - return response.data; - } catch (error) { - throw handleApiError(error, "delete snippet folder"); - } -} - -export async function reorderSnippets( - updates: Array<{ id: number; order: number; folder?: string }>, -): Promise<{ success: boolean }> { - try { - const response = await authApi.put("/snippets/reorder", { - snippets: updates, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "reorder snippets"); - } -} - -export interface SnippetExportData { - snippets: Array<{ - name: string; - content: string; - description?: string | null; - folder?: string | null; - order?: number; - hostFilter?: string | null; - }>; - folders: Array<{ - name: string; - color?: string | null; - icon?: string | null; - }>; -} - -export async function exportSnippets(): Promise { - try { - const response = await authApi.get("/snippets/export"); - return response.data; - } catch (error) { - throw handleApiError(error, "export snippets"); - } -} - -export async function importSnippets( - data: SnippetExportData, - overwrite: boolean, -): Promise<{ - success: boolean; - snippetsImported: number; - snippetsSkipped: number; - snippetsUpdated: number; - foldersImported: number; - foldersSkipped: number; - failed: number; - errors: string[]; -}> { - try { - const response = await authApi.post("/snippets/bulk-import", { - ...data, - overwrite, - }); - return response.data; - } catch (error) { - throw handleApiError(error, "import snippets"); - } -} diff --git a/src/ui/features/terminal/Terminal.tsx b/src/ui/features/terminal/Terminal.tsx index 942147ca2..dd2267959 100644 --- a/src/ui/features/terminal/Terminal.tsx +++ b/src/ui/features/terminal/Terminal.tsx @@ -34,11 +34,11 @@ import { getCookie, isElectron, logActivity, - getSnippets, deleteCommandFromHistory, getCommandHistory, getHostPassword, } from "@/main-axios.ts"; +import { invokeAction } from "@/shell/action-registry"; import { TOTPDialog } from "@/ssh/dialogs/TOTPDialog.tsx"; import { SSHAuthDialog } from "@/ssh/dialogs/SSHAuthDialog.tsx"; import { PassphraseDialog } from "@/ssh/dialogs/PassphraseDialog.tsx"; @@ -272,9 +272,6 @@ const TerminalInner = forwardRef( const terminalInputDisposableRef = useRef<{ dispose(): void } | null>(null); const localEchoRef = useRef(null); const customKeybindingsRef = useRef([]); - const cachedSnippetsRef = useRef<{ id: number; content: string }[] | null>( - null, - ); const [pendingKeybindingSnippet, setPendingKeybindingSnippet] = useState<{ id: string; content: string; @@ -1961,16 +1958,28 @@ const TerminalInner = forwardRef( if (terminalConfig.startupSnippetId) { try { - const snippets = await getSnippets(); - const snippet = snippets.find( - (s: { id: number }) => - s.id === terminalConfig.startupSnippetId, - ); - if (snippet && ws.readyState === 1) { + const resolved = (await invokeAction( + "snippets.resolveForTerminal", + terminalConfig.startupSnippetId, + { + ip: hostConfig.ip, + username: hostConfig.username, + port: hostConfig.port, + name: hostConfig.name, + }, + )) as + | { needsInputs: boolean; content: string } + | null + | undefined; + if ( + resolved && + !resolved.needsInputs && + ws.readyState === 1 + ) { ws.send( JSON.stringify({ type: "input", - data: snippet.content + "\n", + data: resolved.content + "\n", }), ); } @@ -3141,7 +3150,6 @@ const TerminalInner = forwardRef( useEffect(() => { let cancelled = false; const loadKeybindings = () => { - cachedSnippetsRef.current = null; getUserPreferences() .then((prefs) => { if (!cancelled) { @@ -3165,23 +3173,6 @@ const TerminalInner = forwardRef( useEffect(() => { if (!terminal) return; - const getSnippetById = async ( - id: string, - ): Promise<{ content: string } | undefined> => { - try { - if (!cachedSnippetsRef.current) { - cachedSnippetsRef.current = (await getSnippets()) as unknown as { - id: number; - content: string; - }[]; - } - const numericId = Number(id); - return cachedSnippetsRef.current?.find((s) => s.id === numericId); - } catch { - return undefined; - } - }; - const handleCustomKey = (e: KeyboardEvent): boolean => { if (e.type !== "keydown") { return true; @@ -3202,7 +3193,6 @@ const TerminalInner = forwardRef( webSocketRef, writeTextToClipboard, readTextFromClipboard, - getSnippetById, hostContext: { ip: hostConfig.ip, username: hostConfig.username, diff --git a/src/ui/hooks/use-snippet-runner.tsx b/src/ui/hooks/use-snippet-runner.tsx index 5bf924a17..4e1e0b2ca 100644 --- a/src/ui/hooks/use-snippet-runner.tsx +++ b/src/ui/hooks/use-snippet-runner.tsx @@ -2,28 +2,30 @@ import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { useConfirmation } from "@/hooks/use-confirmation.ts"; -import { - hasSnippetInputs, - resolveSnippetContent, - type SnippetHostContext, -} from "@/lib/snippet-variables"; +import { invokeAction } from "@/shell/action-registry"; import { SnippetVariablesDialog } from "@/components/SnippetVariablesDialog"; import type { Snippet, Tab } from "@/types/ui-types"; +interface ResolvedForTerminal { + needsInputs: boolean; + content: string; + isNote: boolean; +} + /** - * Shared "run this snippet against these terminal tabs" flow: resolves - * $HOST-style vars per target, prompts once for $INPUT_n placeholders when - * present, and gates on the confirm-before-running setting. Used by both the - * Snippets sidebar panel and the command palette's snippet entries so the - * behavior (and the dialog) is identical everywhere a snippet gets sent to a - * terminal. + * Shared "run this snippet against these terminal tabs" flow, for the few + * core surfaces (the command palette) that still reach for a snippet + * directly rather than through the snippets plugin's own panel. Variable + * resolution runs through the snippets plugin's "snippets.resolveForTerminal" + * action, and sending runs through ssh-terminal's "terminal.sendToSession", + * since core no longer owns snippet content or a terminal ref. */ export function useSnippetRunner() { const { t } = useTranslation(); const { confirmWithToast } = useConfirmation(); const [runningSnippet, setRunningSnippet] = useState<{ snippet: Snippet; - host: SnippetHostContext | null; + host: Tab["host"] | null; onConfirm: ( resolvedContent: string, inputValues: Record, @@ -49,38 +51,41 @@ export function useSnippetRunner() { [confirmWithToast, t], ); - function sendResolvedToTerminal( + async function sendResolvedToTerminal( tab: Tab, snippet: Snippet, inputValues: Record, ) { - const content = resolveSnippetContent( - snippet.content, + const resolved = (await invokeAction( + "snippets.resolveForTerminal", + snippet.id, tab.host ?? null, inputValues, - ); - if (snippet.isNote) { - tab.terminalRef?.current?.paste?.(content); - } else { - tab.terminalRef?.current?.sendInput?.(content + "\r"); - } + )) as ResolvedForTerminal | null | undefined; + if (!resolved || resolved.needsInputs) return; + await invokeAction("terminal.sendToSession", tab.id, resolved.content, { + run: !resolved.isNote, + }); } const runSnippet = useCallback( (snippet: Snippet, targets: Tab[]) => { const runWithInputs = (inputValues: Record) => { const doSend = () => { - targets.forEach((tab) => - sendResolvedToTerminal(tab, snippet, inputValues), - ); - toast.success( - t( - snippet.isNote - ? "newUi.sidebar.snippets.pasteSuccess" - : "newUi.sidebar.snippets.runSuccess", - { name: snippet.name, count: targets.length }, + void Promise.all( + targets.map((tab) => + sendResolvedToTerminal(tab, snippet, inputValues), ), - ); + ).then(() => { + toast.success( + t( + snippet.isNote + ? "newUi.sidebar.snippets.pasteSuccess" + : "newUi.sidebar.snippets.runSuccess", + { name: snippet.name, count: targets.length }, + ), + ); + }); }; if (snippet.isNote) { doSend(); @@ -89,18 +94,25 @@ export function useSnippetRunner() { } }; - if (hasSnippetInputs(snippet.content)) { - setRunningSnippet({ - snippet, - host: targets[0]?.host ?? null, - onConfirm: (_resolvedContent, inputValues) => { - setRunningSnippet(null); - runWithInputs(inputValues); - }, - }); - } else { + void invokeAction( + "snippets.resolveForTerminal", + snippet.id, + targets[0]?.host ?? null, + ).then((result) => { + const resolved = result as ResolvedForTerminal | null | undefined; + if (resolved?.needsInputs) { + setRunningSnippet({ + snippet, + host: targets[0]?.host ?? null, + onConfirm: (_resolvedContent, inputValues) => { + setRunningSnippet(null); + runWithInputs(inputValues); + }, + }); + return; + } runWithInputs({}); - } + }); }, [handleConfirmRun, t], ); diff --git a/src/ui/lib/keybinding-dispatch.ts b/src/ui/lib/keybinding-dispatch.ts index 3e7180142..6d7650f02 100644 --- a/src/ui/lib/keybinding-dispatch.ts +++ b/src/ui/lib/keybinding-dispatch.ts @@ -1,18 +1,20 @@ import type { Terminal } from "@xterm/xterm"; import type { KeybindingAction } from "@/types/keybindings"; -import { - hasSnippetInputs, - resolveSnippetContent, - type SnippetHostContext, -} from "@/lib/snippet-variables"; +import { invokeAction } from "@/shell/action-registry"; + +export interface SnippetHostContext { + ip?: string; + username?: string; + port?: number | string; + name?: string; +} export interface KeybindingDispatchContext { terminal: Terminal; webSocketRef: React.MutableRefObject; writeTextToClipboard: (text: string) => Promise; readTextFromClipboard: () => Promise; - getSnippetById: (id: string) => Promise<{ content: string } | undefined>; - /** Host context used to silently resolve $HOST/$USER/$PORT/$NAME in runSnippet actions. */ + /** Host context used to resolve $HOST/$USER/$PORT/$NAME in runSnippet actions. */ hostContext?: SnippetHostContext | null; /** * Called instead of sending the snippet directly when its content still has @@ -64,21 +66,26 @@ export function dispatchKeybindingAction( } case "runSnippet": { if (!action.snippetId) return; - ctx.getSnippetById(action.snippetId).then((snippet) => { - if (!snippet) return; - if (hasSnippetInputs(snippet.content)) { + const snippetId = Number(action.snippetId); + if (!Number.isFinite(snippetId)) return; + // Resolving with no inputValues first tells us whether the snippet + // still needs them; the plugin decides and reports back. + void invokeAction( + "snippets.resolveForTerminal", + snippetId, + ctx.hostContext ?? null, + ).then((result) => { + const resolved = result as + { needsInputs: boolean; content: string } | null | undefined; + if (!resolved) return; + if (resolved.needsInputs) { ctx.onSnippetNeedsInputs?.({ id: action.snippetId!, - content: snippet.content, + content: resolved.content, }); return; } - const resolved = resolveSnippetContent( - snippet.content, - ctx.hostContext ?? null, - {}, - ); - sendRaw(resolved + (action.appendEnter !== false ? "\r" : "")); + sendRaw(resolved.content + (action.appendEnter !== false ? "\r" : "")); }); return; } diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 2c8e79be7..93dde68d0 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -624,8 +624,6 @@ "feature_files_desc": "Browse, upload, download and edit files over SFTP without leaving the app.", "feature_tunnels": "Tunnels", "feature_tunnels_desc": "Forward ports through a server to reach services that are not exposed.", - "feature_snippets": "Snippets", - "feature_snippets_desc": "Save commands you run often and send them to a terminal in one click.", "featuresRailHint": "Do not need one of these? Hide any icon from the sidebar in Settings, under Navigation.", "workflowTitle": "Move around faster", "workflowIntro": "A few habits that save the most time once you have a couple of servers.", @@ -710,7 +708,6 @@ "multiPanelHintDockHow": "Right-click its rail icon, or use the panel icon in the tab bar.", "sidebarSettings": "Sidebar Settings...", "hosts": "Hosts", - "snippets": "Snippets", "macros": "Macros", "hostManager": "Host Manager", "credentials": "Credentials", @@ -3201,7 +3198,6 @@ "pluginDisabled": "Plugin disabled", "groups": { "hosts": "Hosts", - "snippets": "Snippets", "credentials": "Credentials", "admin": "Administration" } diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index f4befa6ca..274eb4600 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -2150,20 +2150,10 @@ export { // ============================================================================ // SNIPPETS API // ============================================================================ +// Snippets themselves are owned by the snippets plugin. This is the thin +// list read a few core pickers still need (see src/ui/api/snippets-api.ts). -export { - getSnippets, - createSnippet, - updateSnippet, - deleteSnippet, - executeSnippet, - getSnippetFolders, - createSnippetFolder, - updateSnippetFolderMetadata, - renameSnippetFolder, - deleteSnippetFolder, - reorderSnippets, -} from "@/api/snippets-api"; +export { getSnippets, executeSnippet } from "@/api/snippets-api"; // ============================================================================ export type { @@ -2216,10 +2206,6 @@ export { setHostAuthOverride, getPermissionsCatalog, getSharedHosts, - shareSnippet, - getSnippetAccess, - revokeSnippetAccess, - getSharedSnippets, } from "@/api/rbac-api"; export type { SharePermissionLevel, diff --git a/src/ui/onboarding/steps/FeaturesStep.tsx b/src/ui/onboarding/steps/FeaturesStep.tsx index 4435b421b..63e89cb04 100644 --- a/src/ui/onboarding/steps/FeaturesStep.tsx +++ b/src/ui/onboarding/steps/FeaturesStep.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { FolderTree, Play, Plug, type LucideIcon } from "lucide-react"; +import { FolderTree, Plug, type LucideIcon } from "lucide-react"; import { useActionSlot } from "@/hooks/use-action-slot"; /** @@ -11,7 +11,6 @@ import { useActionSlot } from "@/hooks/use-action-slot"; const FEATURES: { icon: LucideIcon; key: string }[] = [ { icon: FolderTree, key: "files" }, { icon: Plug, key: "tunnels" }, - { icon: Play, key: "snippets" }, ]; export function FeaturesStep() { diff --git a/src/ui/plugin-host/bridge.ts b/src/ui/plugin-host/bridge.ts index 24eed84c1..7c1ec2332 100644 --- a/src/ui/plugin-host/bridge.ts +++ b/src/ui/plugin-host/bridge.ts @@ -29,9 +29,10 @@ import { useSshAuthProviders } from "@/hooks/useSshAuthProviders"; /** * Core permission groups, mirroring RESERVED_PERMISSION_PREFIXES in the SDK * manifest module, which is not imported here because it pulls semver into - * the browser bundle. + * the browser bundle. A group that is itself a plugin id (snippets, once) + * does not need an entry here: knownPluginIds() below already covers it. */ -const CORE_PERMISSION_GROUPS = ["hosts", "snippets", "credentials", "admin"]; +const CORE_PERMISSION_GROUPS = ["hosts", "credentials", "admin"]; /** * Resolves a permission the way the backend's ctx.rbac does: a short name diff --git a/src/ui/shell/shell-layout.ts b/src/ui/shell/shell-layout.ts index 7553ac25f..f130ca01a 100644 --- a/src/ui/shell/shell-layout.ts +++ b/src/ui/shell/shell-layout.ts @@ -26,7 +26,6 @@ const CORE_CAPTURABLE = new Set([ "homepage", "termix-id", "session-logs", - "snippets", "macros", "history", "ssh-tools", @@ -39,7 +38,6 @@ const CORE_SINGLETON = new Set([ "homepage", "termix-id", "session-logs", - "snippets", "macros", "history", "ssh-tools", diff --git a/src/ui/shell/tabUtils.tsx b/src/ui/shell/tabUtils.tsx index ad43de344..f08446cae 100644 --- a/src/ui/shell/tabUtils.tsx +++ b/src/ui/shell/tabUtils.tsx @@ -16,7 +16,6 @@ import { Clock, Fingerprint, Hammer, - Play, ScrollText, Presentation, } from "lucide-react"; @@ -91,9 +90,6 @@ const SessionLogsPanel = lazy(() => default: m.SessionLogsPanel, })), ); -const SnippetsPanel = lazy(() => - import("@/sidebar/SnippetsPanel").then((m) => ({ default: m.SnippetsPanel })), -); const MacrosPanel = lazy(() => import("@/sidebar/MacrosPanel").then((m) => ({ default: m.MacrosPanel })), ); @@ -197,8 +193,6 @@ export function tabIcon(type: TabType) { return ; case "session-logs": return ; - case "snippets": - return ; case "macros": return ; case "history": @@ -371,17 +365,6 @@ export function renderTabContent(tab: Tab, context: TabRenderContext) { case "split-screen": return null; - case "snippets": - return withTabSuspense( - - - , - ); - case "macros": return withTabSuspense( diff --git a/src/ui/sidebar/AdminUserManagePanel.tsx b/src/ui/sidebar/AdminUserManagePanel.tsx index 2fa74a7af..1627924f2 100644 --- a/src/ui/sidebar/AdminUserManagePanel.tsx +++ b/src/ui/sidebar/AdminUserManagePanel.tsx @@ -66,8 +66,10 @@ 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" | "snippets" | "sessions" | "danger"; + "account" | "hosts" | "credentials" | "user-snippets" | "sessions" | "danger"; type ManagedSnippet = { id: number; @@ -253,7 +255,7 @@ export function AdminUserManagePanel({ { id: "account", label: t("admin.manageTabAccount") }, { id: "hosts", label: t("admin.manageTabHosts") }, { id: "credentials", label: t("admin.manageTabCredentials") }, - { id: "snippets", label: t("admin.manageTabSnippets") }, + { id: "user-snippets", label: t("admin.manageTabSnippets") }, { id: "sessions", label: t("admin.manageTabSessions") }, { id: "danger", label: t("admin.manageTabDanger") }, ]; @@ -996,7 +998,7 @@ export function AdminUserManagePanel({
))} - {activeTab === "snippets" && + {activeTab === "user-snippets" && (!dataUnlocked ? ( lockedNotice ) : ( diff --git a/src/ui/sidebar/SnippetsPanel.tsx b/src/ui/sidebar/SnippetsPanel.tsx deleted file mode 100644 index bf1488250..000000000 --- a/src/ui/sidebar/SnippetsPanel.tsx +++ /dev/null @@ -1,2403 +0,0 @@ -import { getErrorMessage } from "../lib/error-message.js"; -import { useState, useEffect, useCallback, useMemo, useRef } from "react"; -import { useTranslation } from "react-i18next"; -import { copyToClipboard } from "@/lib/clipboard"; -import { useSnippetRunner } from "@/hooks/use-snippet-runner.tsx"; -import { - getSnippets, - createSnippet as apiCreateSnippet, - updateSnippet as apiUpdateSnippet, - deleteSnippet as apiDeleteSnippet, - getSnippetFolders, - createSnippetFolder as apiCreateSnippetFolder, - deleteSnippetFolder as apiDeleteSnippetFolder, - renameSnippetFolder as apiRenameSnippetFolder, - updateSnippetFolderMetadata as apiUpdateSnippetFolderMetadata, - shareSnippet as apiShareSnippet, - getSnippetAccess, - revokeSnippetAccess, - getUserList, - getRoles, - reorderSnippets, - saveUserPreferences, -} from "@/main-axios"; -import { - hasSnippetInputs, - type SnippetHostContext, -} from "@/lib/snippet-variables"; -import { SnippetVariablesDialog } from "@/components/SnippetVariablesDialog"; -import { - exportSnippets, - importSnippets, - type SnippetExportData, - executeSnippet as apiExecuteSnippet, -} from "@/api/snippets-api"; -import { getSSHHosts } from "@/api/ssh-host-management-api"; -import type { SSHHost } from "@/types/index"; -import { Button } from "@/components/button"; -import { Input } from "@/components/input"; -import { FolderPathPicker } from "./FolderPathPicker"; -import { Separator } from "@/components/separator"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, -} from "@/components/dialog"; -import { - Box, - ChevronDown, - ChevronsDownUp, - ChevronsUpDown, - Clipboard, - Copy, - Cpu, - Database, - Download, - Folder, - Globe, - GripVertical, - Network, - Pencil, - Play, - Plus, - Search, - Server, - Settings, - Share2, - Terminal, - Trash2, - Upload, - UserPlus, - X, - MoreHorizontal, - Zap, -} from "lucide-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/dropdown-menu"; -import { SettingRow, FakeSwitch } from "@/components/section-card"; -import { toast } from "sonner"; -import { FOLDER_COLORS } from "@/lib/theme"; -import { FOLDER_ICONS } from "@/types/ui-types"; -import type { - Snippet, - SnippetFolder, - FolderIconId, - Tab, -} from "@/types/ui-types"; -import { Select2 } from "@/components/select2"; - -function FolderIconEl({ - icon, - className, - style, -}: { - icon: FolderIconId; - className?: string; - style?: React.CSSProperties; -}) { - const props = { className, style }; - switch (icon) { - case "folder": - return ; - case "server": - return ; - case "cloud": - return ( -
- - - -
- ); - case "database": - return ; - case "box": - return ; - case "network": - return ; - case "copy": - return ; - case "settings": - return ; - case "cpu": - return ; - case "globe": - return ; - } -} - -function SnippetFormDialog({ - open, - onOpenChange, - folders, - snippet, - onSave, - availableHosts, -}: { - open: boolean; - onOpenChange: (v: boolean) => void; - folders: SnippetFolder[]; - snippet: Snippet | null; - onSave: (data: Omit, id?: number) => void; - availableHosts: SSHHost[]; -}) { - const { t } = useTranslation(); - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [folder, setFolder] = useState(null); - const [content, setContent] = useState(""); - const [isNote, setIsNote] = useState(false); - const [selectedHostIds, setSelectedHostIds] = useState>( - new Set(), - ); - - const folderMeta = useMemo(() => { - const map = new Map(); - for (const f of folders) { - map.set(f.name, { color: f.color ?? undefined, icon: f.icon }); - } - return map; - }, [folders]); - - useEffect(() => { - if (open) { - setName(snippet?.name ?? ""); - setDescription(snippet?.description ?? ""); - setFolder(snippet?.folder ?? null); - setContent(snippet?.content ?? ""); - setIsNote(snippet?.isNote ?? false); - setSelectedHostIds(new Set(snippet?.hostIds ?? [])); - } - }, [open, snippet]); - - function toggleHost(id: number) { - setSelectedHostIds((prev) => { - const next = new Set(prev); - if (next.has(id)) { - next.delete(id); - } else { - next.add(id); - } - return next; - }); - } - - function handleSave() { - if (!name.trim() || !content.trim()) return; - onSave( - { - name: name.trim(), - description: description.trim() || undefined, - content: content.trim(), - folder, - isNote, - hostIds: - !isNote && selectedHostIds.size > 0 - ? Array.from(selectedHostIds) - : undefined, - }, - snippet?.id, - ); - onOpenChange(false); - } - - const isEdit = snippet !== null; - - return ( - - - - - {isEdit - ? t("newUi.sidebar.snippets.editSnippetTitle") - : t("newUi.sidebar.snippets.createSnippetTitle")} - - - {isEdit - ? t("newUi.sidebar.snippets.editSnippetDescription") - : t("newUi.sidebar.snippets.createSnippetDescription")} - - -
-
- - setName(e.target.value)} - /> -
-
- - setDescription(e.target.value)} - /> -
-
- - setFolder(path === "" ? null : path)} - folderPaths={folders.map((f) => f.name)} - folderMeta={folderMeta} - /> -
-
- -
- - -
-
-
- -