show ssh commands

This commit is contained in:
miloschwartz
2026-09-11 15:23:01 -04:00
parent 110b6d19a5
commit 92ebbc9ac7
2 changed files with 165 additions and 74 deletions
+2
View File
@@ -4279,6 +4279,8 @@
"resourceLauncherSitesDescription": "The resource is accessible via the following sites.", "resourceLauncherSitesDescription": "The resource is accessible via the following sites.",
"resourceLauncherViewSiteAsAdmin": "View Site as Admin", "resourceLauncherViewSiteAsAdmin": "View Site as Admin",
"resourceLauncherFilterBySite": "Filter by Site", "resourceLauncherFilterBySite": "Filter by Site",
"resourceLauncherSshCommand": "SSH Command",
"resourceLauncherSshCommandDescription": "Use the Pangolin CLI to open an SSH session to this resource.",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.", "resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.", "resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required", "resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
@@ -1,6 +1,8 @@
"use client"; "use client";
import { AiClientConfigSection } from "@app/components/ai-client-config/AiClientConfigSection"; import { AiClientConfigSection } from "@app/components/ai-client-config/AiClientConfigSection";
import { AiConfigBlocks } from "@app/components/ai-client-config/AiConfigBlocks";
import CopyTextBox from "@app/components/CopyTextBox";
import CopyToClipboard from "@app/components/CopyToClipboard"; import CopyToClipboard from "@app/components/CopyToClipboard";
import { import {
InfoSection, InfoSection,
@@ -43,6 +45,7 @@ import {
TableRow TableRow
} from "@app/components/ui/table"; } from "@app/components/ui/table";
import { useMyVirtualApiKeySecret } from "@app/hooks/useMyVirtualApiKeySecret"; import { useMyVirtualApiKeySecret } from "@app/hooks/useMyVirtualApiKeySecret";
import type { AiConfigBlock } from "@app/lib/aiClientConfig";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { import {
derivePublicAuthState, derivePublicAuthState,
@@ -259,10 +262,11 @@ function LauncherResourceSitesSection({
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<Table> <div className="min-w-0 overflow-x-auto overflow-y-hidden">
<Table sticky>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead> <TableHead className="whitespace-nowrap">
<Button <Button
variant="ghost" variant="ghost"
className="h-8 px-3" className="h-8 px-3"
@@ -272,7 +276,7 @@ function LauncherResourceSitesSection({
<ChevronsUpDown className="ml-2 size-4" /> <ChevronsUpDown className="ml-2 size-4" />
</Button> </Button>
</TableHead> </TableHead>
<TableHead> <TableHead className="whitespace-nowrap">
<Button <Button
variant="ghost" variant="ghost"
className="h-8 px-3" className="h-8 px-3"
@@ -282,19 +286,35 @@ function LauncherResourceSitesSection({
<ChevronsUpDown className="ml-2 size-4" /> <ChevronsUpDown className="ml-2 size-4" />
</Button> </Button>
</TableHead> </TableHead>
<TableHead className="w-12"> <TableHead
<span className="sr-only">{t("actions")}</span> className={cn(
"whitespace-nowrap text-right",
"sticky right-0 z-10 w-auto min-w-fit bg-card",
"[mask-image:linear-gradient(to_right,transparent_0%,black_20px)]"
)}
>
<span className="sr-only">
{t("actions")}
</span>
</TableHead> </TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{sortedSites.map((site) => ( {sortedSites.map((site) => (
<TableRow key={site.siteId}> <TableRow key={site.siteId}>
<TableCell>{site.name}</TableCell> <TableCell className="whitespace-nowrap">
<TableCell> {site.name}
</TableCell>
<TableCell className="whitespace-nowrap">
<SiteStatusCell site={site} /> <SiteStatusCell site={site} />
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell
className={cn(
"whitespace-nowrap text-right",
"sticky right-0 z-10 w-auto min-w-fit bg-card",
"[mask-image:linear-gradient(to_right,transparent_0%,black_20px)]"
)}
>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
@@ -335,6 +355,67 @@ function LauncherResourceSitesSection({
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</div>
</SettingsSectionBody>
</SettingsSection>
);
}
function LauncherPrivateSshCommandSection({
alias,
niceId,
pamMode
}: {
alias?: string | null;
niceId: string;
pamMode?: string | null;
}) {
const t = useTranslations();
const trimmedAlias = alias?.trim() || null;
const hasDistinctAlias = Boolean(trimmedAlias && trimmedAlias !== niceId);
const useUserPrefix = pamMode !== "push";
function formatSshTarget(target: string) {
return useUserPrefix ? `user@${target}` : target;
}
const blocks: AiConfigBlock[] = hasDistinctAlias
? [
{
id: "alias",
label: t("editInternalResourceDialogAlias"),
displayText: `pangolin ssh ${formatSshTarget(trimmedAlias!)}`
},
{
id: "niceId",
label: t("identifier"),
displayText: `pangolin ssh ${formatSshTarget(niceId)}`
}
]
: [
{
id: "niceId",
label: t("identifier"),
displayText: `pangolin ssh ${formatSshTarget(niceId)}`
}
];
return (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("resourceLauncherSshCommand")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("resourceLauncherSshCommandDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
{hasDistinctAlias ? (
<AiConfigBlocks blocks={blocks} relation="options" />
) : (
<CopyTextBox text={blocks[0].displayText} />
)}
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
); );
@@ -572,6 +653,7 @@ function PrivateResourceDetails({
}) { }) {
const t = useTranslations(); const t = useTranslations();
const isInference = resource.mode === "inference"; const isInference = resource.mode === "inference";
const isSsh = resource.mode === "ssh";
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -624,6 +706,13 @@ function PrivateResourceDetails({
isAdmin={isAdmin} isAdmin={isAdmin}
onFilterBySite={onFilterBySite} onFilterBySite={onFilterBySite}
/> />
{isSsh ? (
<LauncherPrivateSshCommandSection
alias={resource.alias}
niceId={resource.niceId}
pamMode={resource.pamMode}
/>
) : null}
{isInference ? ( {isInference ? (
<> <>
<LauncherInferenceModelsSection <LauncherInferenceModelsSection