Populate the program stack select from castle-api
The dashboard's stack dropdown hardcoded its options (python-cli/fastapi/ react-vite), so a supabase program showed "(none)" — the value had no matching option. Make the backend the single source of truth instead: - core: available_stacks() returns sorted(HANDLERS) — the stacks castle has handlers for. - castle-api: GET /stacks exposes it. - cli: --stack choices derive from available_stacks() (drops the duplicated list). - app: ProgramFields fetches /stacks (useStacks) and renders options from it, always including the current value so it never silently blanks; StackBadge gets a Supabase label. A new backend stack now appears everywhere without a frontend edit.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import type { ProgramDetail } from "@/types"
|
import type { ProgramDetail } from "@/types"
|
||||||
|
import { useStacks } from "@/services/api/hooks"
|
||||||
import { Field, TextField, FormFooter } from "./fields"
|
import { Field, TextField, FormFooter } from "./fields"
|
||||||
|
|
||||||
const SELECT =
|
const SELECT =
|
||||||
@@ -21,6 +22,7 @@ type Obj = Record<string, unknown>
|
|||||||
/** Edit a program's catalog config (its source identity), not how it's deployed. */
|
/** Edit a program's catalog config (its source identity), not how it's deployed. */
|
||||||
export function ProgramFields({ program, onSave, onDelete }: Props) {
|
export function ProgramFields({ program, onSave, onDelete }: Props) {
|
||||||
const m = program.manifest
|
const m = program.manifest
|
||||||
|
const { data: stacks = [] } = useStacks()
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [saved, setSaved] = useState(false)
|
const [saved, setSaved] = useState(false)
|
||||||
|
|
||||||
@@ -114,9 +116,13 @@ export function ProgramFields({ program, onSave, onDelete }: Props) {
|
|||||||
>
|
>
|
||||||
<select value={stack} onChange={(e) => setStack(e.target.value)} className={`w-48 ${SELECT}`}>
|
<select value={stack} onChange={(e) => setStack(e.target.value)} className={`w-48 ${SELECT}`}>
|
||||||
<option value="">(none)</option>
|
<option value="">(none)</option>
|
||||||
<option value="python-cli">python-cli</option>
|
{/* Options come from the backend (GET /stacks); include the current
|
||||||
<option value="python-fastapi">python-fastapi</option>
|
value even if unknown so it never silently blanks. */}
|
||||||
<option value="react-vite">react-vite</option>
|
{Array.from(new Set([...stacks, ...(stack ? [stack] : [])])).map((s) => (
|
||||||
|
<option key={s} value={s}>
|
||||||
|
{s}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
<TextField label="Version" value={version} onChange={setVersion} width="w-32" hint="Optional metadata." />
|
<TextField label="Version" value={version} onChange={setVersion} width="w-32" hint="Optional metadata." />
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const STACK_LABELS: Record<string, string> = {
|
|||||||
"python-fastapi": "Python / FastAPI",
|
"python-fastapi": "Python / FastAPI",
|
||||||
"python-cli": "Python / CLI",
|
"python-cli": "Python / CLI",
|
||||||
"react-vite": "React / Vite",
|
"react-vite": "React / Vite",
|
||||||
|
supabase: "Supabase",
|
||||||
rust: "Rust",
|
rust: "Rust",
|
||||||
go: "Go",
|
go: "Go",
|
||||||
bash: "Bash",
|
bash: "Bash",
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ export function useJobs() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The stacks castle has handlers for — authoritative source for the program
|
||||||
|
// stack select, so a new backend stack appears without a frontend change.
|
||||||
|
export function useStacks() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["stacks"],
|
||||||
|
queryFn: () => apiClient.get<string[]>("/stacks"),
|
||||||
|
staleTime: Infinity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function useJob(name: string) {
|
export function useJob(name: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["jobs", name],
|
queryKey: ["jobs", name],
|
||||||
|
|||||||
@@ -4,12 +4,19 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, status
|
from fastapi import APIRouter, HTTPException, status
|
||||||
|
|
||||||
from castle_core.stacks import available_actions, run_action
|
from castle_core.stacks import available_actions, available_stacks, run_action
|
||||||
|
|
||||||
from castle_api.config import get_config
|
from castle_api.config import get_config
|
||||||
|
|
||||||
programs_router = APIRouter(tags=["programs"])
|
programs_router = APIRouter(tags=["programs"])
|
||||||
|
|
||||||
|
|
||||||
|
@programs_router.get("/stacks")
|
||||||
|
def list_stacks() -> list[str]:
|
||||||
|
"""Stack names castle has handlers for — populates the dashboard's stack select
|
||||||
|
and keeps it in sync with the backend (no hardcoded frontend list)."""
|
||||||
|
return available_stacks()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Unified program action endpoint
|
# Unified program action endpoint
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from castle_core.stacks import available_stacks
|
||||||
|
|
||||||
from castle_cli import __version__
|
from castle_cli import __version__
|
||||||
|
|
||||||
DEV_VERBS = ["build", "test", "lint", "format", "type-check", "check"]
|
DEV_VERBS = ["build", "test", "lint", "format", "type-check", "check"]
|
||||||
@@ -37,11 +39,7 @@ def _build_program_group(subparsers: argparse._SubParsersAction) -> None:
|
|||||||
|
|
||||||
p = sub.add_parser("create", help="Scaffold a new program")
|
p = sub.add_parser("create", help="Scaffold a new program")
|
||||||
_add_name(p, "Program name")
|
_add_name(p, "Program name")
|
||||||
p.add_argument(
|
p.add_argument("--stack", choices=available_stacks(), default=None)
|
||||||
"--stack",
|
|
||||||
choices=["python-cli", "python-fastapi", "react-vite", "supabase"],
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
p.add_argument("--description", default="", help="Program description")
|
p.add_argument("--description", default="", help="Program description")
|
||||||
p.add_argument("--port", type=int, help="Port (daemons only)")
|
p.add_argument("--port", type=int, help="Port (daemons only)")
|
||||||
|
|
||||||
|
|||||||
@@ -520,6 +520,13 @@ def get_handler(stack: str | None) -> StackHandler | None:
|
|||||||
return HANDLERS.get(stack)
|
return HANDLERS.get(stack)
|
||||||
|
|
||||||
|
|
||||||
|
def available_stacks() -> list[str]:
|
||||||
|
"""The stack names castle has handlers for — the single source of truth for the
|
||||||
|
CLI ``--stack`` choices, the ``GET /stacks`` endpoint, and the dashboard select.
|
||||||
|
"""
|
||||||
|
return sorted(HANDLERS)
|
||||||
|
|
||||||
|
|
||||||
def _declared_commands(comp: ProgramSpec, verb: str) -> list[list[str]] | None:
|
def _declared_commands(comp: ProgramSpec, verb: str) -> list[list[str]] | None:
|
||||||
"""Declared argv-lists for a verb, or None.
|
"""Declared argv-lists for a verb, or None.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user