refactor: Update component specifications to use 'program' and 'behavior' attributes, enhancing clarity and consistency across the application
This commit is contained in:
@@ -5,7 +5,7 @@ const TEMPLATES: Record<string, Record<string, unknown>> = {
|
||||
service: {
|
||||
run: {
|
||||
runner: "python",
|
||||
tool: "",
|
||||
program: "",
|
||||
cwd: "",
|
||||
env: {},
|
||||
},
|
||||
@@ -23,9 +23,7 @@ const TEMPLATES: Record<string, Record<string, unknown>> = {
|
||||
},
|
||||
},
|
||||
tool: {
|
||||
install: {
|
||||
path: { alias: "" },
|
||||
},
|
||||
behavior: "tool",
|
||||
},
|
||||
job: {
|
||||
run: {
|
||||
@@ -72,7 +70,7 @@ export function AddComponent({ onAdd, existingNames }: AddComponentProps) {
|
||||
// Fill in template-specific fields
|
||||
if (template === "service") {
|
||||
const run = config.run as Record<string, unknown>
|
||||
run.tool = name
|
||||
run.program = name
|
||||
run.cwd = name
|
||||
const proxy = (config.proxy as Record<string, Record<string, string>>).caddy
|
||||
proxy.path_prefix = `/${name}`
|
||||
@@ -81,8 +79,7 @@ export function AddComponent({ onAdd, existingNames }: AddComponentProps) {
|
||||
expose.http.internal.port = parseInt(port, 10)
|
||||
}
|
||||
} else if (template === "tool") {
|
||||
const install = (config.install as Record<string, Record<string, string>>).path
|
||||
install.alias = name
|
||||
// tool template has behavior preset, no extra config needed
|
||||
}
|
||||
|
||||
await onAdd(name, config)
|
||||
|
||||
@@ -115,7 +115,7 @@ export function ComponentFields({ component, onSave, onDelete }: ComponentFields
|
||||
<Field label="Runner">
|
||||
<span className="text-sm font-mono text-[var(--muted)]">
|
||||
{runnerLabel(runner)}
|
||||
{(m.run as Record<string, string>)?.tool && (
|
||||
{(m.run as Record<string, string>)?.program && (
|
||||
<> · {(m.run as Record<string, string>).tool}</>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -32,11 +32,18 @@ const ACTION_CONFIG: Record<string, ActionConfig> = {
|
||||
|
||||
const DEV_ACTIONS = ["build", "test", "lint", "type-check", "check"]
|
||||
|
||||
export interface ActionOutput {
|
||||
action: string
|
||||
text: string
|
||||
ok: boolean
|
||||
}
|
||||
|
||||
interface ProgramActionsProps {
|
||||
name: string
|
||||
actions: string[]
|
||||
installed?: boolean | null
|
||||
compact?: boolean
|
||||
onOutput?: (output: ActionOutput) => void
|
||||
}
|
||||
|
||||
function visibleActions(
|
||||
@@ -69,30 +76,27 @@ function visibleActions(
|
||||
return visible
|
||||
}
|
||||
|
||||
export function ProgramActions({ name, actions, installed, compact }: ProgramActionsProps) {
|
||||
export function ProgramActions({ name, actions, installed, compact, onOutput }: ProgramActionsProps) {
|
||||
const { mutate, isPending } = useProgramAction()
|
||||
const [runningAction, setRunningAction] = useState<string | null>(null)
|
||||
const [output, setOutput] = useState<{ action: string; text: string; ok: boolean } | null>(null)
|
||||
|
||||
const visible = visibleActions(actions, installed, !!compact)
|
||||
|
||||
const handleAction = (action: string) => {
|
||||
setRunningAction(action)
|
||||
setOutput(null)
|
||||
onOutput?.({ action: "", text: "", ok: true }) // clear previous
|
||||
mutate(
|
||||
{ name, action },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setRunningAction(null)
|
||||
// Show output for dev actions on detail page
|
||||
if (!compact && DEV_ACTIONS.includes(action) && data.output) {
|
||||
setOutput({ action, text: data.output, ok: true })
|
||||
onOutput?.({ action, text: data.output, ok: true })
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
setRunningAction(null)
|
||||
if (!compact && DEV_ACTIONS.includes(action)) {
|
||||
// Extract detail from API error JSON
|
||||
let text = String(err)
|
||||
try {
|
||||
const parsed = JSON.parse((err as Error).message)
|
||||
@@ -100,7 +104,7 @@ export function ProgramActions({ name, actions, installed, compact }: ProgramAct
|
||||
} catch {
|
||||
text = (err as Error).message ?? text
|
||||
}
|
||||
setOutput({ action, text, ok: false })
|
||||
onOutput?.({ action, text, ok: false })
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -110,58 +114,59 @@ export function ProgramActions({ name, actions, installed, compact }: ProgramAct
|
||||
if (visible.length === 0) return null
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{visible.map((action) => {
|
||||
const config = ACTION_CONFIG[action]
|
||||
if (!config) return null
|
||||
const Icon = config.icon
|
||||
const isRunning = isPending && runningAction === action
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<button
|
||||
key={action}
|
||||
onClick={() => handleAction(action)}
|
||||
disabled={isPending}
|
||||
className={`p-1 rounded ${config.color} ${config.hoverBg} transition-colors disabled:opacity-40`}
|
||||
title={config.label}
|
||||
>
|
||||
{isRunning ? <Loader2 size={14} className="animate-spin" /> : <Icon size={14} />}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{visible.map((action) => {
|
||||
const config = ACTION_CONFIG[action]
|
||||
if (!config) return null
|
||||
const Icon = config.icon
|
||||
const isRunning = isPending && runningAction === action
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<button
|
||||
key={action}
|
||||
onClick={() => handleAction(action)}
|
||||
disabled={isPending}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 text-sm rounded border ${config.borderColor} ${config.color} ${config.hoverBg} transition-colors disabled:opacity-40`}
|
||||
className={`p-1 rounded ${config.color} ${config.hoverBg} transition-colors disabled:opacity-40`}
|
||||
title={config.label}
|
||||
>
|
||||
{isRunning ? <Loader2 size={14} className="animate-spin" /> : <Icon size={14} />}
|
||||
{config.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
|
||||
{output && !compact && (
|
||||
<div className={`mt-3 rounded-lg border overflow-hidden ${output.ok ? "border-[var(--border)]" : "border-red-800"}`}>
|
||||
<div className={`flex items-center justify-between px-3 py-1.5 text-xs ${output.ok ? "text-green-400 bg-green-900/20" : "text-red-400 bg-red-900/20"}`}>
|
||||
<span>{ACTION_CONFIG[output.action]?.label ?? output.action} — {output.ok ? "ok" : "error"}</span>
|
||||
<button
|
||||
onClick={() => setOutput(null)}
|
||||
className="text-[var(--muted)] hover:text-[var(--foreground)]"
|
||||
>
|
||||
dismiss
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-3 text-xs font-mono text-gray-300 overflow-x-auto max-h-64 overflow-y-auto bg-black/40">
|
||||
{output.text}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<button
|
||||
key={action}
|
||||
onClick={() => handleAction(action)}
|
||||
disabled={isPending}
|
||||
className={`flex items-center gap-1.5 px-2 py-1 text-sm rounded border ${config.borderColor} ${config.color} ${config.hoverBg} transition-colors disabled:opacity-40`}
|
||||
>
|
||||
{isRunning ? <Loader2 size={14} className="animate-spin" /> : <Icon size={14} />}
|
||||
{config.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ActionOutputPanel({ output, onDismiss }: { output: ActionOutput; onDismiss: () => void }) {
|
||||
if (!output.action) return null
|
||||
return (
|
||||
<div className={`rounded-lg border overflow-hidden ${output.ok ? "border-[var(--border)]" : "border-red-800"}`}>
|
||||
<div className={`flex items-center justify-between px-3 py-1.5 text-xs ${output.ok ? "text-green-400 bg-green-900/20" : "text-red-400 bg-red-900/20"}`}>
|
||||
<span>{ACTION_CONFIG[output.action]?.label ?? output.action} — {output.ok ? "ok" : "error"}</span>
|
||||
<button
|
||||
onClick={onDismiss}
|
||||
className="text-[var(--muted)] hover:text-[var(--foreground)]"
|
||||
>
|
||||
dismiss
|
||||
</button>
|
||||
</div>
|
||||
<pre className="p-3 text-xs font-mono text-gray-300 overflow-x-auto max-h-64 overflow-y-auto bg-black/40">
|
||||
{output.text}
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user