mirror of
https://github.com/stablyai/orca.git
synced 2026-11-06 03:57:35 +00:00
fix(ports): show the Ports panel for folder workspaces on a managed SSH server (#29908)
A folder workspace has no repo row, only a synthetic repoId, so the panel's repo lookup came back empty and it said "No workspace selected", hiding the SSH forwards and the server's listeners.
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import type { FolderWorkspace } from '../../../../shared/folder-workspace-types'
|
||||
import type { WorkspacePortScanResult } from '../../../../shared/workspace-ports'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { useAppStore } from '@/store'
|
||||
import { workspacePortRuntimeTargetKey } from '@/lib/workspace-port-actions'
|
||||
import PortsPanel from './PortsPanel'
|
||||
|
||||
const initialState = useAppStore.getInitialState()
|
||||
|
||||
const folderWorkspace: FolderWorkspace = {
|
||||
id: 'f1',
|
||||
projectGroupId: 'g1',
|
||||
name: 'notes',
|
||||
folderPath: '/home/me/notes',
|
||||
connectionId: null,
|
||||
executionHostId: 'runtime:env-a',
|
||||
linkedTask: null,
|
||||
comment: '',
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0,
|
||||
createdAt: 0,
|
||||
updatedAt: 0
|
||||
}
|
||||
|
||||
const scan: WorkspacePortScanResult = {
|
||||
platform: 'linux',
|
||||
scannedAt: 1,
|
||||
ports: [
|
||||
{
|
||||
id: '127.0.0.1:5173:42',
|
||||
bindHost: '127.0.0.1',
|
||||
connectHost: '127.0.0.1',
|
||||
port: 5173,
|
||||
pid: 42,
|
||||
processName: 'vite',
|
||||
protocol: 'http',
|
||||
kind: 'external'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
useAppStore.setState(initialState, true)
|
||||
})
|
||||
|
||||
describe('Ports panel for a folder workspace on a managed SSH server', () => {
|
||||
it('shows the SSH forwards and the server listeners, not "No workspace selected"', () => {
|
||||
const scanKey = `${workspacePortRuntimeTargetKey({ kind: 'environment', environmentId: 'env-a' })}:all`
|
||||
useAppStore.setState({
|
||||
folderWorkspaces: [folderWorkspace],
|
||||
activeWorktreeId: 'folder:f1',
|
||||
activeWorkspaceExecutionHostId: 'runtime:env-a',
|
||||
runtimeEnvironments: [
|
||||
{
|
||||
id: 'env-a',
|
||||
name: 'Box',
|
||||
createdAt: 0,
|
||||
updatedAt: 0,
|
||||
lastUsedAt: null,
|
||||
runtimeId: null,
|
||||
preferredEndpointId: 'ep',
|
||||
endpoints: [],
|
||||
orcadDeployment: {
|
||||
sshTargetId: 'box',
|
||||
sshTargetGeneration: 1,
|
||||
localPort: 7001,
|
||||
remotePort: 7002
|
||||
}
|
||||
}
|
||||
],
|
||||
sshConnectionStates: new Map([
|
||||
['box', { targetId: 'box', status: 'connected', error: null, reconnectAttempt: 0 }]
|
||||
]),
|
||||
workspacePortScansByKey: { [scanKey]: scan }
|
||||
})
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<PortsPanel isVisible />
|
||||
</TooltipProvider>
|
||||
)
|
||||
expect(screen.queryByText('No workspace selected')).toBeNull()
|
||||
expect(screen.getByRole('button', { name: /Add/ })).toBeTruthy()
|
||||
// The server's listener reaches the (collapsed) External section.
|
||||
expect(screen.getByRole('button', { name: /External\s*1/ })).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { RefreshCw, Server } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useActiveWorktree, useRepoById } from '@/store/selectors'
|
||||
import { useActiveWorktree } from '@/store/selectors'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
|
||||
import {
|
||||
@@ -40,7 +40,8 @@ export function LocalWorkspacePortsPanel({
|
||||
sshForwardTargetId?: string | null
|
||||
}): React.JSX.Element {
|
||||
const activeWorktree = useActiveWorktree()
|
||||
const activeRepo = useRepoById(activeWorktree?.repoId ?? null)
|
||||
// Why the workspace's repoId, not a repo row: a folder workspace has no repo, only a synthetic id.
|
||||
const activeRepoId = activeWorktree?.repoId ?? null
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const createBrowserTab = useAppStore((s) => s.createBrowserTab)
|
||||
const setRemoteBrowserPageHandle = useAppStore((s) => s.setRemoteBrowserPageHandle)
|
||||
@@ -57,7 +58,7 @@ export function LocalWorkspacePortsPanel({
|
||||
const scanKey = runtimeTarget ? `${workspacePortRuntimeTargetKey(runtimeTarget)}:all` : null
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
if (!activeRepo || !runtimeTarget || !scanKey) {
|
||||
if (!activeRepoId || !runtimeTarget || !scanKey) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
setWorkspacePortScanRefreshing(true)
|
||||
@@ -92,7 +93,7 @@ export function LocalWorkspacePortsPanel({
|
||||
})
|
||||
return promise
|
||||
}, [
|
||||
activeRepo,
|
||||
activeRepoId,
|
||||
runtimeTarget,
|
||||
scanKey,
|
||||
replaceWorkspacePortScans,
|
||||
@@ -109,11 +110,11 @@ export function LocalWorkspacePortsPanel({
|
||||
|
||||
const handleStopPort = useCallback(
|
||||
async (port: WorkspacePort) => {
|
||||
if (!activeRepo || !port.pid) {
|
||||
if (!activeRepoId || !port.pid) {
|
||||
return
|
||||
}
|
||||
const result = await killWorkspacePortForTarget(runtimeTarget, {
|
||||
repoId: activeRepo.id,
|
||||
repoId: activeRepoId,
|
||||
pid: port.pid,
|
||||
port: port.port
|
||||
})
|
||||
@@ -146,7 +147,7 @@ export function LocalWorkspacePortsPanel({
|
||||
)
|
||||
}
|
||||
},
|
||||
[activeRepo, runtimeTarget, replaceWorkspacePortScans, setWorkspacePortScanRefreshing]
|
||||
[activeRepoId, runtimeTarget, replaceWorkspacePortScans, setWorkspacePortScanRefreshing]
|
||||
)
|
||||
|
||||
const handleOpenPortInBrowser = useCallback(
|
||||
@@ -178,8 +179,8 @@ export function LocalWorkspacePortsPanel({
|
||||
)
|
||||
|
||||
const { activePorts, otherWorkspacePorts, externalPorts } = useMemo(
|
||||
() => getLocalWorkspacePortSections(displayScan, activeRepo?.id, activeWorktree?.id),
|
||||
[activeRepo?.id, activeWorktree?.id, displayScan]
|
||||
() => getLocalWorkspacePortSections(displayScan, activeRepoId, activeWorktree?.id),
|
||||
[activeRepoId, activeWorktree?.id, displayScan]
|
||||
)
|
||||
|
||||
const showPortSections = shouldShowLocalWorkspacePortSections(displayScan, {
|
||||
@@ -188,7 +189,7 @@ export function LocalWorkspacePortsPanel({
|
||||
externalPorts
|
||||
})
|
||||
|
||||
if (!activeRepo) {
|
||||
if (!activeRepoId) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full px-4 text-center text-muted-foreground">
|
||||
<Server size={32} className="mb-3 opacity-50" />
|
||||
|
||||
Reference in New Issue
Block a user