mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 00:03:09 +00:00
Gate unavailable project setup hosts in settings
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -4,6 +4,7 @@ import React, { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { toSshExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { RUNTIME_PROTOCOL_VERSION } from '../../../../shared/protocol-version'
|
||||
import type { Project, ProjectHostSetup, Repo } from '../../../../shared/types'
|
||||
import { useAppStore } from '../../store'
|
||||
import { RepositoryHostSetupsSection } from './RepositoryHostSetupsSection'
|
||||
@@ -395,6 +396,70 @@ describe('RepositoryHostSetupsSection', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('shows unsupported runtime hosts without enabling setup actions', async () => {
|
||||
const createProjectHostSetup = vi.fn()
|
||||
const setupProjectClone = vi.fn()
|
||||
const setupProjectExistingFolder = vi.fn()
|
||||
const localRepo = makeRepo({
|
||||
id: 'local-repo',
|
||||
displayName: 'Orca',
|
||||
path: '/Users/alice/orca'
|
||||
})
|
||||
useAppStore.setState({
|
||||
repos: [localRepo],
|
||||
projects: [makeProject({ id: 'github:stablyai/orca' })],
|
||||
projectHostSetups: [
|
||||
makeSetup({
|
||||
id: 'local-repo',
|
||||
projectId: 'github:stablyai/orca',
|
||||
repoId: 'local-repo',
|
||||
hostId: 'local',
|
||||
path: '/Users/alice/orca'
|
||||
})
|
||||
],
|
||||
settings: { activeRuntimeEnvironmentId: null } as never,
|
||||
runtimeStatusByEnvironmentId: new Map([
|
||||
[
|
||||
'gpu',
|
||||
{
|
||||
checkedAt: 1,
|
||||
appVersion: '1.7.0',
|
||||
status: {
|
||||
runtimeId: 'runtime-gpu',
|
||||
rendererGraphEpoch: 1,
|
||||
graphStatus: 'ready',
|
||||
authoritativeWindowId: 1,
|
||||
liveTabCount: 0,
|
||||
liveLeafCount: 0,
|
||||
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
|
||||
minCompatibleRuntimeClientVersion: 1,
|
||||
capabilities: []
|
||||
}
|
||||
}
|
||||
]
|
||||
]),
|
||||
createProjectHostSetup,
|
||||
setupProjectClone,
|
||||
setupProjectExistingFolder
|
||||
})
|
||||
|
||||
renderSection(localRepo)
|
||||
|
||||
expect(container.textContent).toContain('Update Orca on this host to set up projects')
|
||||
const trackButton = Array.from(container.querySelectorAll('button')).find(
|
||||
(button) => button.textContent === 'Track setup'
|
||||
)
|
||||
expect(trackButton?.disabled).toBe(true)
|
||||
|
||||
await act(async () => {
|
||||
trackButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
})
|
||||
|
||||
expect(createProjectHostSetup).not.toHaveBeenCalled()
|
||||
expect(setupProjectClone).not.toHaveBeenCalled()
|
||||
expect(setupProjectExistingFolder).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('offers inactive runtime hosts discovered from hydrated runtime status', async () => {
|
||||
const createProjectHostSetup = vi.fn().mockResolvedValue({
|
||||
project: makeProject({ id: 'github:stablyai/orca' }),
|
||||
@@ -439,7 +504,7 @@ describe('RepositoryHostSetupsSection', () => {
|
||||
authoritativeWindowId: 1,
|
||||
liveTabCount: 0,
|
||||
liveLeafCount: 0,
|
||||
runtimeProtocolVersion: 1,
|
||||
runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION,
|
||||
minCompatibleRuntimeClientVersion: 1,
|
||||
capabilities: ['project-host-setup.v1']
|
||||
}
|
||||
|
||||
@@ -86,7 +86,12 @@ export function RepositoryHostSetupsSection({
|
||||
const [isCloning, setIsCloning] = useState(false)
|
||||
const [isCreatingPendingSetup, setIsCreatingPendingSetup] = useState(false)
|
||||
const [deletingSetupId, setDeletingSetupId] = useState<string | null>(null)
|
||||
const setupTargetHostId = selectedSetupHostId ?? setupHostOptions[0]?.id ?? null
|
||||
const defaultSetupHostOption =
|
||||
setupHostOptions.find((option) => option.isAvailable) ?? setupHostOptions[0] ?? null
|
||||
const setupTargetHostId = selectedSetupHostId ?? defaultSetupHostOption?.id ?? null
|
||||
const setupTargetHostOption =
|
||||
setupHostOptions.find((option) => option.id === setupTargetHostId) ?? null
|
||||
const canUseSetupTargetHost = setupTargetHostOption?.isAvailable ?? false
|
||||
|
||||
if (
|
||||
(projectHostSetups.length <= 1 && setupHostOptions.length === 0) ||
|
||||
@@ -209,8 +214,15 @@ export function RepositoryHostSetupsSection({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{setupHostOptions.map((option) => (
|
||||
<SelectItem key={option.id} value={option.id}>
|
||||
{option.label}
|
||||
<SelectItem key={option.id} value={option.id} disabled={!option.isAvailable}>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate">{option.label}</span>
|
||||
{!option.isAvailable ? (
|
||||
<span className="block truncate text-[11px] text-muted-foreground">
|
||||
{option.detail}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -245,9 +257,14 @@ export function RepositoryHostSetupsSection({
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={!setupTargetHostId || !setupPath.trim() || isSettingUp}
|
||||
disabled={!canUseSetupTargetHost || !setupPath.trim() || isSettingUp}
|
||||
onClick={async () => {
|
||||
if (!setupTargetHostId || !selectedProjectHostSetup || !setupPath.trim()) {
|
||||
if (
|
||||
!setupTargetHostId ||
|
||||
!canUseSetupTargetHost ||
|
||||
!selectedProjectHostSetup ||
|
||||
!setupPath.trim()
|
||||
) {
|
||||
return
|
||||
}
|
||||
setIsSettingUp(true)
|
||||
@@ -295,11 +312,12 @@ export function RepositoryHostSetupsSection({
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={
|
||||
!setupTargetHostId || !cloneUrl.trim() || !cloneDestination.trim() || isCloning
|
||||
!canUseSetupTargetHost || !cloneUrl.trim() || !cloneDestination.trim() || isCloning
|
||||
}
|
||||
onClick={async () => {
|
||||
if (
|
||||
!setupTargetHostId ||
|
||||
!canUseSetupTargetHost ||
|
||||
!selectedProjectHostSetup ||
|
||||
!cloneUrl.trim() ||
|
||||
!cloneDestination.trim()
|
||||
@@ -334,9 +352,9 @@ export function RepositoryHostSetupsSection({
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!setupTargetHostId || isCreatingPendingSetup}
|
||||
disabled={!canUseSetupTargetHost || isCreatingPendingSetup}
|
||||
onClick={async () => {
|
||||
if (!setupTargetHostId || !selectedProjectHostSetup) {
|
||||
if (!setupTargetHostId || !canUseSetupTargetHost || !selectedProjectHostSetup) {
|
||||
return
|
||||
}
|
||||
setIsCreatingPendingSetup(true)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { getExecutionHostLabel, 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, ProjectHostSetupState } from '../../../../shared/types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
export type SetupHostOption = {
|
||||
id: ExecutionHostId
|
||||
label: string
|
||||
detail: string
|
||||
isAvailable: boolean
|
||||
}
|
||||
|
||||
export function getSetupStateLabel(setupState: ProjectHostSetupState): string {
|
||||
@@ -42,8 +45,45 @@ export function buildSetupHostOptions({
|
||||
const setupHostIds = new Set(projectHostSetups.map((setup) => setup.hostId))
|
||||
return hostOptions
|
||||
.filter((host) => !setupHostIds.has(host.id))
|
||||
.map((host) => ({
|
||||
id: host.id,
|
||||
label: host.label || getExecutionHostLabel(host.id)
|
||||
}))
|
||||
.map((host) => {
|
||||
const availability = getHostSetupAvailability(host)
|
||||
return {
|
||||
id: host.id,
|
||||
label: host.label || getExecutionHostLabel(host.id),
|
||||
detail: availability.detail,
|
||||
isAvailable: availability.isAvailable
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getHostSetupAvailability(host: ExecutionHostRegistryEntry): {
|
||||
isAvailable: boolean
|
||||
detail: string
|
||||
} {
|
||||
if (host.health === 'blocked') {
|
||||
return {
|
||||
isAvailable: false,
|
||||
detail: translate(
|
||||
'auto.components.settings.RepositoryPane.hostSetupBlockedVersion',
|
||||
'Orca server version is incompatible'
|
||||
)
|
||||
}
|
||||
}
|
||||
if (
|
||||
host.kind === 'runtime' &&
|
||||
host.capabilities &&
|
||||
!host.capabilities?.includes(PROJECT_HOST_SETUP_RUNTIME_CAPABILITY)
|
||||
) {
|
||||
return {
|
||||
isAvailable: false,
|
||||
detail: translate(
|
||||
'auto.components.settings.RepositoryPane.hostSetupMissingCapability',
|
||||
'Update Orca on this host to set up projects'
|
||||
)
|
||||
}
|
||||
}
|
||||
return {
|
||||
isAvailable: true,
|
||||
detail: host.detail
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5058,6 +5058,8 @@
|
||||
"setupPathPending": "Path pending",
|
||||
"openSetup": "Open",
|
||||
"removeSetup": "Remove",
|
||||
"hostSetupBlockedVersion": "Orca server version is incompatible",
|
||||
"hostSetupMissingCapability": "Update Orca on this host to set up projects",
|
||||
"setupProjectOnHost": "Set up on another host",
|
||||
"setupProjectOnHostHelp": "Choose a host, then import an existing checkout, clone the repository there, or track a setup that will be provisioned later.",
|
||||
"setupExistingFolder": "Import existing folder",
|
||||
|
||||
@@ -5038,6 +5038,8 @@
|
||||
"setupPathPending": "パス保留中",
|
||||
"openSetup": "開く",
|
||||
"removeSetup": "削除",
|
||||
"hostSetupBlockedVersion": "Orca サーバーのバージョンに互換性がありません",
|
||||
"hostSetupMissingCapability": "プロジェクトをセットアップするには、このホストの Orca を更新してください",
|
||||
"setupProjectOnHost": "別のホストにセットアップ",
|
||||
"setupProjectOnHostHelp": "ホストを選択し、既存のチェックアウトをインポートするか、そこでリポジトリをクローンするか、後でプロビジョニングされるセットアップを追跡します。",
|
||||
"setupExistingFolder": "既存フォルダーをインポート",
|
||||
|
||||
@@ -5038,6 +5038,8 @@
|
||||
"setupPathPending": "경로 대기 중",
|
||||
"openSetup": "열기",
|
||||
"removeSetup": "제거",
|
||||
"hostSetupBlockedVersion": "Orca 서버 버전이 호환되지 않습니다",
|
||||
"hostSetupMissingCapability": "프로젝트를 설정하려면 이 호스트의 Orca를 업데이트하세요",
|
||||
"setupProjectOnHost": "다른 호스트에 설정",
|
||||
"setupProjectOnHostHelp": "호스트를 선택한 다음 기존 체크아웃을 가져오거나, 그곳에 저장소를 클론하거나, 나중에 프로비저닝될 설정을 추적합니다.",
|
||||
"setupExistingFolder": "기존 폴더 가져오기",
|
||||
|
||||
@@ -5038,6 +5038,8 @@
|
||||
"setupPathPending": "路径待定",
|
||||
"openSetup": "打开",
|
||||
"removeSetup": "移除",
|
||||
"hostSetupBlockedVersion": "Orca 服务器版本不兼容",
|
||||
"hostSetupMissingCapability": "请更新此主机上的 Orca 以设置项目",
|
||||
"setupProjectOnHost": "在另一台主机上设置",
|
||||
"setupProjectOnHostHelp": "选择一台主机,然后导入现有检出目录、在该主机上克隆仓库,或跟踪稍后要预配的设置。",
|
||||
"setupExistingFolder": "导入现有文件夹",
|
||||
|
||||
@@ -155,6 +155,7 @@ function getHostSetupAvailability(host: ExecutionHostRegistryEntry): {
|
||||
}
|
||||
if (
|
||||
host.kind === 'runtime' &&
|
||||
host.capabilities &&
|
||||
!host.capabilities?.includes(PROJECT_HOST_SETUP_RUNTIME_CAPABILITY)
|
||||
) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user