Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/components/layout/AppShell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const items: NavItem[] = [
{ label: 'Workspace', to: '/workspace', minRole: 'viewer', icon: 'workspace' },
{ label: 'Check', to: '/check', minRole: 'viewer', icon: 'check' },
{ label: 'Gradient Lab', to: '/lab', minRole: 'viewer', icon: 'lab' },
{ label: 'Lab Envs', to: '/lab/envs', minRole: 'viewer', icon: 'lab' },
{ label: 'Teams', to: '/teams', minRole: 'admin', icon: 'teams' },
{ label: 'Users', to: '/users', minRole: 'admin', icon: 'users' },
{ label: 'System', to: '/system', minRole: 'admin', icon: 'system' },
Expand Down
78 changes: 78 additions & 0 deletions frontend/src/lib/labEnvs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { api } from './api'
import type { LabEnv, LabEnvsResponse, LabStorage } from '../types'

export async function fetchLabEnvs(): Promise<LabEnvsResponse> {
return api<LabEnvsResponse>('/api/v1/lab/envs')
}

export interface LaunchEnvPayload {
image: string
display_name?: string
driver?: string
gpus?: number
cpu_request?: string
mem_request?: string
ttl_seconds: number
}

export async function launchLabEnv(payload: LaunchEnvPayload): Promise<LabEnv> {
return api<LabEnv>('/api/v1/lab/envs', {
method: 'POST',
body: JSON.stringify(payload),
})
}

export async function extendLabEnv(id: string, extendSeconds: number): Promise<LabEnv> {
return api<LabEnv>(`/api/v1/lab/envs/${encodeURIComponent(id)}/extend`, {
method: 'POST',
body: JSON.stringify({ extend_seconds: extendSeconds }),
})
}

export async function archiveLabEnv(id: string): Promise<LabEnv> {
return api<LabEnv>(`/api/v1/lab/envs/${encodeURIComponent(id)}/archive`, {
method: 'POST',
})
}

export async function deleteLabEnv(id: string): Promise<LabEnv> {
return api<LabEnv>(`/api/v1/lab/envs/${encodeURIComponent(id)}`, { method: 'DELETE' })
}

export async function updateLabStorage(next: LabStorage): Promise<LabStorage> {
return api<LabStorage>('/api/v1/lab/storage', {
method: 'PUT',
body: JSON.stringify(next),
})
}

export async function setActiveDriver(name: string): Promise<{ active: string }> {
return api<{ active: string }>('/api/v1/lab/drivers', {
method: 'PUT',
body: JSON.stringify({ driver: name }),
})
}

export function remainingSeconds(expiresAt: string, now: Date = new Date()): number {
const expiry = Date.parse(expiresAt)
if (Number.isNaN(expiry)) {
return 0
}
return Math.max(0, Math.floor((expiry - now.getTime()) / 1000))
}

export function formatCountdown(seconds: number): string {
if (seconds <= 0) {
return 'expired'
}
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
const s = seconds % 60
if (h > 0) {
return `${h}h ${m.toString().padStart(2, '0')}m`
}
if (m > 0) {
return `${m}m ${s.toString().padStart(2, '0')}s`
}
return `${s}s`
}
2 changes: 2 additions & 0 deletions frontend/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import EnvironmentView from '../views/EnvironmentView.vue'
import FleetView from '../views/FleetView.vue'
import CheckView from '../views/CheckView.vue'
import LabView from '../views/LabView.vue'
import LabEnvsView from '../views/LabEnvsView.vue'
import LoginView from '../views/LoginView.vue'
import LogsView from '../views/LogsView.vue'
import SettingsView from '../views/SettingsView.vue'
Expand Down Expand Up @@ -40,6 +41,7 @@ export const router = createRouter({
{ path: '/check', component: CheckView, meta: { minRole: 'viewer', title: 'Check' } },
{ path: '/doctor', redirect: '/check' },
{ path: '/lab', component: LabView, meta: { minRole: 'viewer', title: 'Gradient Lab' } },
{ path: '/lab/envs', component: LabEnvsView, meta: { minRole: 'viewer', title: 'Lab Envs' } },
{ path: '/teams', component: TeamsView, meta: { minRole: 'admin', title: 'Teams' } },
{ path: '/users', component: UsersView, meta: { minRole: 'admin', title: 'Users' } },
{ path: '/system', component: SystemView, meta: { minRole: 'admin', title: 'System' } },
Expand Down
42 changes: 42 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
export type Role = 'viewer' | 'developer' | 'operator' | 'admin'

export type LabEnvStatus =
| 'pending'
| 'running'
| 'expiring'
| 'archiving'
| 'archived'
| 'failed'

export interface LabEnv {
id: string
driver: string
owner: string
image: string
display_name?: string
status: LabEnvStatus
container_id?: string
jupyter_url?: string
token?: string
gpus: number
cpu_request?: string
mem_request?: string
hot_tier_path: string
cold_tier_path: string
archive_ref?: string
created_at: string
expires_at: string
archived_at?: string
last_error?: string
}

export interface LabStorage {
hot_tier: string
cold_tier: string
}

export interface LabEnvsResponse {
envs: LabEnv[]
storage: LabStorage
drivers: string[]
active: string
}

export interface Session {
username: string
role: Role
Expand Down
Loading