mirror of
https://github.com/stablyai/orca.git
synced 2026-10-15 16:02:49 +00:00
Gate unavailable project host setup options
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -21,14 +21,21 @@ vi.mock('@/components/ui/command', () => ({
|
||||
CommandList: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
CommandItem: ({
|
||||
children,
|
||||
disabled,
|
||||
onSelect,
|
||||
value
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
disabled?: boolean
|
||||
onSelect?: (value: string) => void
|
||||
value: string
|
||||
}) => (
|
||||
<button type="button" data-command-value={value} onClick={() => onSelect?.(value)}>
|
||||
<button
|
||||
type="button"
|
||||
data-command-value={value}
|
||||
disabled={disabled}
|
||||
onClick={() => onSelect?.(value)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
@@ -54,7 +61,18 @@ const needsSetupOption: NeedsSetupProjectHostOption = {
|
||||
projectId: 'project-1',
|
||||
hostId: 'ssh:builder',
|
||||
label: 'Builder',
|
||||
detail: 'Project not set up on this host'
|
||||
detail: 'Project not set up on this host',
|
||||
isAvailable: true
|
||||
}
|
||||
|
||||
const unavailableOption: NeedsSetupProjectHostOption = {
|
||||
id: 'needs-setup:runtime:old',
|
||||
kind: 'needs-setup',
|
||||
projectId: 'project-1',
|
||||
hostId: 'runtime:old',
|
||||
label: 'Old server',
|
||||
detail: 'Update Orca on this host to set up projects',
|
||||
isAvailable: false
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -121,4 +139,33 @@ describe('ProjectHostSetupCombobox', () => {
|
||||
expect(onValueChange).not.toHaveBeenCalled()
|
||||
expect(onNeedsSetupHostSelect).toHaveBeenCalledWith(needsSetupOption)
|
||||
})
|
||||
|
||||
it('keeps unavailable setup rows visible but not selectable', () => {
|
||||
const onValueChange = vi.fn()
|
||||
const onNeedsSetupHostSelect = vi.fn()
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<ProjectHostSetupCombobox
|
||||
options={[readyOption, unavailableOption]}
|
||||
value={readyOption.id}
|
||||
onValueChange={onValueChange}
|
||||
onNeedsSetupHostSelect={onNeedsSetupHostSelect}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const unavailableButton = container.querySelector<HTMLButtonElement>(
|
||||
'[data-command-value="needs-setup:runtime:old"]'
|
||||
)
|
||||
expect(unavailableButton?.textContent).toContain('Update Orca on this host')
|
||||
expect(unavailableButton?.disabled).toBe(true)
|
||||
|
||||
act(() => {
|
||||
unavailableButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
})
|
||||
|
||||
expect(onValueChange).not.toHaveBeenCalled()
|
||||
expect(onNeedsSetupHostSelect).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,6 +34,9 @@ export default function ProjectHostSetupCombobox({
|
||||
return
|
||||
}
|
||||
if (option.kind === 'needs-setup') {
|
||||
if (!option.isAvailable) {
|
||||
return
|
||||
}
|
||||
onNeedsSetupHostSelect?.(option)
|
||||
setOpen(false)
|
||||
return
|
||||
@@ -90,6 +93,7 @@ export default function ProjectHostSetupCombobox({
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
onSelect={() => handleSelect(option.id)}
|
||||
disabled={option.kind === 'needs-setup' && !option.isAvailable}
|
||||
className="items-center gap-2 px-3 py-2"
|
||||
>
|
||||
<Check
|
||||
|
||||
@@ -96,7 +96,8 @@ describe('buildProjectHostSetupOptions', () => {
|
||||
id: 'needs-setup:ssh:builder',
|
||||
kind: 'needs-setup',
|
||||
label: 'Builder',
|
||||
detail: 'Project not set up on this host'
|
||||
detail: 'Project not set up on this host',
|
||||
isAvailable: true
|
||||
})
|
||||
])
|
||||
})
|
||||
@@ -105,7 +106,10 @@ describe('buildProjectHostSetupOptions', () => {
|
||||
const options = buildProjectHostSetupOptions({
|
||||
projectId: 'project-1',
|
||||
eligibleRepos: [repo('local-repo')],
|
||||
hosts: [host('local'), host('runtime:gpu', { label: 'GPU VM' })],
|
||||
hosts: [
|
||||
host('local'),
|
||||
host('runtime:gpu', { label: 'GPU VM', capabilities: ['project-host-setup.v1'] })
|
||||
],
|
||||
projectHostSetups: [
|
||||
setup('local', 'project-1', 'local', 'local-repo'),
|
||||
setup('gpu-pending', 'project-1', 'runtime:gpu', '', {
|
||||
@@ -122,7 +126,8 @@ describe('buildProjectHostSetupOptions', () => {
|
||||
id: 'needs-setup:runtime:gpu',
|
||||
kind: 'needs-setup',
|
||||
label: 'GPU VM',
|
||||
detail: 'Project setup is in progress'
|
||||
detail: 'Project setup is in progress',
|
||||
isAvailable: true
|
||||
})
|
||||
])
|
||||
})
|
||||
@@ -137,7 +142,7 @@ describe('buildProjectHostSetupOptions', () => {
|
||||
expect(
|
||||
buildProjectHostSetupOptions({
|
||||
...base,
|
||||
hosts: [host('runtime:gpu', { label: 'GPU VM' })],
|
||||
hosts: [host('runtime:gpu', { label: 'GPU VM', capabilities: ['project-host-setup.v1'] })],
|
||||
projectHostSetups: [
|
||||
...base.projectHostSetups,
|
||||
setup('gpu-pending', 'project-1', 'runtime:gpu', '', {
|
||||
@@ -152,7 +157,7 @@ describe('buildProjectHostSetupOptions', () => {
|
||||
expect(
|
||||
buildProjectHostSetupOptions({
|
||||
...base,
|
||||
hosts: [host('runtime:gpu', { label: 'GPU VM' })],
|
||||
hosts: [host('runtime:gpu', { label: 'GPU VM', capabilities: ['project-host-setup.v1'] })],
|
||||
projectHostSetups: [
|
||||
...base.projectHostSetups,
|
||||
setup('gpu-pending', 'project-1', 'runtime:gpu', '', {
|
||||
@@ -167,7 +172,7 @@ describe('buildProjectHostSetupOptions', () => {
|
||||
expect(
|
||||
buildProjectHostSetupOptions({
|
||||
...base,
|
||||
hosts: [host('runtime:gpu', { label: 'GPU VM' })],
|
||||
hosts: [host('runtime:gpu', { label: 'GPU VM', capabilities: ['project-host-setup.v1'] })],
|
||||
projectHostSetups: [
|
||||
...base.projectHostSetups,
|
||||
setup('gpu-pending', 'project-1', 'runtime:gpu', '', {
|
||||
@@ -179,4 +184,47 @@ describe('buildProjectHostSetupOptions', () => {
|
||||
}).at(-1)
|
||||
).toMatchObject({ detail: 'Project is unsupported on this host' })
|
||||
})
|
||||
|
||||
it('marks incompatible runtime hosts as visible but unavailable', () => {
|
||||
const options = buildProjectHostSetupOptions({
|
||||
projectId: 'project-1',
|
||||
eligibleRepos: [repo('local-repo')],
|
||||
hosts: [
|
||||
host('local'),
|
||||
host('runtime:gpu', {
|
||||
label: 'GPU VM',
|
||||
health: 'blocked',
|
||||
capabilities: ['project-host-setup.v1']
|
||||
})
|
||||
],
|
||||
projectHostSetups: [setup('local', 'project-1', 'local', 'local-repo')]
|
||||
})
|
||||
|
||||
expect(options).toEqual([
|
||||
expect.objectContaining({ id: 'local', kind: 'ready', label: 'Local Mac' }),
|
||||
expect.objectContaining({
|
||||
id: 'needs-setup:runtime:gpu',
|
||||
kind: 'needs-setup',
|
||||
label: 'GPU VM',
|
||||
detail: 'Orca server version is incompatible',
|
||||
isAvailable: false
|
||||
})
|
||||
])
|
||||
})
|
||||
|
||||
it('marks runtime hosts without project setup capability as unavailable', () => {
|
||||
const options = buildProjectHostSetupOptions({
|
||||
projectId: 'project-1',
|
||||
eligibleRepos: [repo('local-repo')],
|
||||
hosts: [host('local'), host('runtime:gpu', { label: 'GPU VM', capabilities: [] })],
|
||||
projectHostSetups: [setup('local', 'project-1', 'local', 'local-repo')]
|
||||
})
|
||||
|
||||
expect(options.at(-1)).toMatchObject({
|
||||
id: 'needs-setup:runtime:gpu',
|
||||
kind: 'needs-setup',
|
||||
detail: 'Update Orca on this host to set up projects',
|
||||
isAvailable: false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type ExecutionHostId
|
||||
} from '../../../shared/execution-host'
|
||||
import type { ExecutionHostRegistryEntry } from '../../../shared/execution-host-registry'
|
||||
import { PROJECT_HOST_SETUP_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
|
||||
import type { ProjectHostSetup, Repo } from '../../../shared/types'
|
||||
|
||||
export type ProjectHostSetupOption =
|
||||
@@ -24,6 +25,7 @@ export type ProjectHostSetupOption =
|
||||
hostId: ExecutionHostId
|
||||
label: string
|
||||
detail: string
|
||||
isAvailable: boolean
|
||||
}
|
||||
|
||||
export type ReadyProjectHostSetupOption = Extract<ProjectHostSetupOption, { kind: 'ready' }>
|
||||
@@ -124,19 +126,48 @@ function buildNeedsSetupOptions({
|
||||
.filter((host) => !readySetupByHost.has(host.id))
|
||||
.map((host) => {
|
||||
const pendingSetup = pendingSetupByHost.get(host.id)
|
||||
const availability = getHostSetupAvailability(host)
|
||||
return {
|
||||
id: `needs-setup:${host.id}`,
|
||||
kind: 'needs-setup' as const,
|
||||
projectId,
|
||||
hostId: host.id,
|
||||
label: host.label || getExecutionHostLabel(host.id),
|
||||
detail: pendingSetup
|
||||
? getPendingSetupDetail(pendingSetup)
|
||||
: 'Project not set up on this host'
|
||||
detail: availability.isAvailable
|
||||
? pendingSetup
|
||||
? getPendingSetupDetail(pendingSetup)
|
||||
: 'Project not set up on this host'
|
||||
: availability.detail,
|
||||
isAvailable: availability.isAvailable
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getHostSetupAvailability(host: ExecutionHostRegistryEntry): {
|
||||
isAvailable: boolean
|
||||
detail: string
|
||||
} {
|
||||
if (host.health === 'blocked') {
|
||||
return {
|
||||
isAvailable: false,
|
||||
detail: 'Orca server version is incompatible'
|
||||
}
|
||||
}
|
||||
if (
|
||||
host.kind === 'runtime' &&
|
||||
!host.capabilities?.includes(PROJECT_HOST_SETUP_RUNTIME_CAPABILITY)
|
||||
) {
|
||||
return {
|
||||
isAvailable: false,
|
||||
detail: 'Update Orca on this host to set up projects'
|
||||
}
|
||||
}
|
||||
return {
|
||||
isAvailable: true,
|
||||
detail: ''
|
||||
}
|
||||
}
|
||||
|
||||
function getPendingSetupDetail(setup: ProjectHostSetup): string {
|
||||
switch (setup.setupState) {
|
||||
case 'not-set-up':
|
||||
|
||||
Reference in New Issue
Block a user