Refactor component terminology to programs in config and manifest
- Updated the terminology from "components" to "programs" across the codebase, including in config loading, saving, and manifest specifications. - Introduced a new `stacks.py` file to handle lifecycle actions for development stacks, implementing handlers for Python and React Vite stacks. - Adjusted tests to reflect the new program structure and ensure proper functionality. - Revised documentation to align with the new terminology and structure, ensuring clarity on the purpose and configuration of programs, services, and jobs.
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
import { ExternalLink, Play, RefreshCw, Server, Square, Terminal } from "lucide-react"
|
||||
import { Link } from "react-router-dom"
|
||||
import type { ComponentSummary, HealthStatus } from "@/types"
|
||||
import type { ServiceSummary, HealthStatus } from "@/types"
|
||||
import { useServiceAction } from "@/services/api/hooks"
|
||||
import { runnerLabel } from "@/lib/labels"
|
||||
import { HealthBadge } from "./HealthBadge"
|
||||
import { BehaviorBadge } from "./BehaviorBadge"
|
||||
import { StackBadge } from "./StackBadge"
|
||||
|
||||
interface ComponentCardProps {
|
||||
component: ComponentSummary
|
||||
component: ServiceSummary
|
||||
health?: HealthStatus
|
||||
}
|
||||
|
||||
@@ -39,7 +38,6 @@ export function ComponentCard({ component, health }: ComponentCardProps) {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1.5 mb-2">
|
||||
<BehaviorBadge behavior={component.behavior} />
|
||||
<StackBadge stack={component.stack} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import { Check, Loader2, Save, Trash2 } from "lucide-react"
|
||||
import type { ComponentDetail } from "@/types"
|
||||
import type { AnyDetail } from "@/types"
|
||||
import { runnerLabel } from "@/lib/labels"
|
||||
import { SecretsEditor } from "./SecretsEditor"
|
||||
|
||||
interface ComponentFieldsProps {
|
||||
component: ComponentDetail
|
||||
component: AnyDetail
|
||||
onSave: (name: string, config: Record<string, unknown>) => Promise<void>
|
||||
onDelete?: (name: string) => Promise<void>
|
||||
}
|
||||
@@ -122,7 +122,7 @@ export function ComponentFields({ component, onSave, onDelete }: ComponentFields
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{(component.managed || port) && (
|
||||
{("managed" in component && component.managed || port) && (
|
||||
<Field label="Port">
|
||||
<input
|
||||
value={port}
|
||||
@@ -133,7 +133,7 @@ export function ComponentFields({ component, onSave, onDelete }: ComponentFields
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{(component.managed || healthPath) && (
|
||||
{("managed" in component && component.managed || healthPath) && (
|
||||
<Field label="Health path">
|
||||
<input
|
||||
value={healthPath}
|
||||
@@ -205,11 +205,13 @@ export function ComponentFields({ component, onSave, onDelete }: ComponentFields
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field label="Systemd">
|
||||
<span className="text-sm text-[var(--muted)]">
|
||||
{component.managed ? "Yes" : "No"}
|
||||
</span>
|
||||
</Field>
|
||||
{"managed" in component && (
|
||||
<Field label="Systemd">
|
||||
<span className="text-sm text-[var(--muted)]">
|
||||
{component.managed ? "Yes" : "No"}
|
||||
</span>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-3 border-t border-[var(--border)]">
|
||||
{onDelete ? (
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { ComponentSummary, HealthStatus } from "@/types"
|
||||
import { BEHAVIOR_LABELS } from "@/lib/labels"
|
||||
import { ComponentCard } from "./ComponentCard"
|
||||
|
||||
const BEHAVIOR_ORDER = ["daemon", "tool", "frontend"]
|
||||
|
||||
interface ComponentGridProps {
|
||||
components: ComponentSummary[]
|
||||
statuses: HealthStatus[]
|
||||
}
|
||||
|
||||
export function ComponentGrid({ components, statuses }: ComponentGridProps) {
|
||||
const statusMap = new Map(statuses.map((s) => [s.id, s]))
|
||||
|
||||
// Group by behavior
|
||||
const groups = new Map<string, ComponentSummary[]>()
|
||||
for (const comp of components) {
|
||||
const key = comp.behavior ?? "other"
|
||||
const list = groups.get(key) ?? []
|
||||
list.push(comp)
|
||||
groups.set(key, list)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{BEHAVIOR_ORDER.map((key) => {
|
||||
const items = groups.get(key)
|
||||
if (!items?.length) return null
|
||||
return (
|
||||
<section key={key}>
|
||||
<h2 className="text-lg font-semibold mb-3 text-[var(--muted)]">
|
||||
{BEHAVIOR_LABELS[key] ?? key}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{items.map((comp) => (
|
||||
<ComponentCard
|
||||
key={comp.id}
|
||||
component={comp}
|
||||
health={statusMap.get(comp.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,18 @@
|
||||
import { useMemo, useState } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown, Plug, Unplug } from "lucide-react"
|
||||
import type { ComponentSummary } from "@/types"
|
||||
import { useToolAction } from "@/services/api/hooks"
|
||||
import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react"
|
||||
import type { ProgramSummary } from "@/types"
|
||||
import { BehaviorBadge } from "./BehaviorBadge"
|
||||
import { StackBadge } from "./StackBadge"
|
||||
import { ProgramActions } from "./ProgramActions"
|
||||
|
||||
interface ComponentTableProps {
|
||||
components: ComponentSummary[]
|
||||
components: ProgramSummary[]
|
||||
}
|
||||
|
||||
type SortKey = "id" | "stack" | "behavior" | "status"
|
||||
type SortKey = "id" | "stack" | "behavior"
|
||||
type SortDir = "asc" | "desc"
|
||||
|
||||
function installedRank(installed: boolean | null): number {
|
||||
if (installed === false) return 0
|
||||
if (installed === true) return 1
|
||||
return 2
|
||||
}
|
||||
|
||||
export function ComponentTable({ components }: ComponentTableProps) {
|
||||
const [search, setSearch] = useState("")
|
||||
const [sortKey, setSortKey] = useState<SortKey>("id")
|
||||
@@ -53,8 +47,6 @@ export function ComponentTable({ components }: ComponentTableProps) {
|
||||
return dir * (a.stack ?? "").localeCompare(b.stack ?? "")
|
||||
case "behavior":
|
||||
return dir * (a.behavior ?? "").localeCompare(b.behavior ?? "")
|
||||
case "status":
|
||||
return dir * (installedRank(a.installed) - installedRank(b.installed))
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
@@ -67,7 +59,7 @@ export function ComponentTable({ components }: ComponentTableProps) {
|
||||
<input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Filter components..."
|
||||
placeholder="Filter programs..."
|
||||
className="bg-black/30 border border-[var(--border)] rounded px-3 py-1.5 text-sm focus:outline-none focus:border-[var(--primary)] w-56"
|
||||
/>
|
||||
</div>
|
||||
@@ -79,21 +71,17 @@ export function ComponentTable({ components }: ComponentTableProps) {
|
||||
<SortHeader label="Name" sortKey="id" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
||||
<SortHeader label="Stack" sortKey="stack" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
||||
<SortHeader label="Behavior" sortKey="behavior" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
||||
<SortHeader label="Status" sortKey="status" current={sortKey} dir={sortDir} onSort={toggleSort} />
|
||||
<th className="px-3 py-2 font-medium text-[var(--muted)]">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sorted.map((comp) => (
|
||||
<ComponentRow
|
||||
key={comp.id}
|
||||
component={comp}
|
||||
/>
|
||||
<ComponentRow key={comp.id} component={comp} />
|
||||
))}
|
||||
{sorted.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-3 py-6 text-center text-[var(--muted)]">
|
||||
No components match.
|
||||
<td colSpan={4} className="px-3 py-6 text-center text-[var(--muted)]">
|
||||
No programs match.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@@ -132,33 +120,12 @@ function SortHeader({
|
||||
)
|
||||
}
|
||||
|
||||
function InstalledBadge({ installed }: { installed: boolean }) {
|
||||
return installed ? (
|
||||
<span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-green-900/40 text-green-400 border border-green-800/50">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-400" />
|
||||
installed
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded-full bg-zinc-800/40 text-[var(--muted)] border border-[var(--border)]">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-zinc-500" />
|
||||
not installed
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function ComponentRow({
|
||||
component,
|
||||
}: {
|
||||
component: ComponentSummary
|
||||
}) {
|
||||
const isTool = component.installed !== null
|
||||
const { mutate: toolAction, isPending: toolPending } = useToolAction()
|
||||
|
||||
function ComponentRow({ component }: { component: ProgramSummary }) {
|
||||
return (
|
||||
<tr className="border-b border-[var(--border)] last:border-b-0 hover:bg-[var(--card)]/50 transition-colors">
|
||||
<td className="px-3 py-2.5">
|
||||
<Link
|
||||
to={`/components/${component.id}`}
|
||||
to={`/programs/${component.id}`}
|
||||
className="font-medium hover:text-[var(--primary)] transition-colors"
|
||||
>
|
||||
{component.id}
|
||||
@@ -176,38 +143,7 @@ function ComponentRow({
|
||||
<BehaviorBadge behavior={component.behavior} />
|
||||
</td>
|
||||
<td className="px-3 py-2.5">
|
||||
{isTool ? (
|
||||
<InstalledBadge installed={component.installed!} />
|
||||
) : (
|
||||
<span className="text-[var(--muted)]">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2.5">
|
||||
{isTool ? (
|
||||
<div className="flex items-center gap-1">
|
||||
{component.installed ? (
|
||||
<button
|
||||
onClick={() => toolAction({ name: component.id, action: "uninstall" })}
|
||||
disabled={toolPending}
|
||||
className="p-1 rounded hover:bg-red-800/30 text-red-400 transition-colors disabled:opacity-40"
|
||||
title="Uninstall from PATH"
|
||||
>
|
||||
<Unplug size={14} />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => toolAction({ name: component.id, action: "install" })}
|
||||
disabled={toolPending}
|
||||
className="p-1 rounded hover:bg-green-800/30 text-green-400 transition-colors disabled:opacity-40"
|
||||
title="Install to PATH"
|
||||
>
|
||||
<Plug size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-[var(--muted)]">—</span>
|
||||
)}
|
||||
<ProgramActions name={component.id} actions={component.actions} installed={component.installed} compact />
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useEffect, useImperativeHandle, useRef, useState, forwardRef } from "react"
|
||||
import { apiClient } from "@/services/api/client"
|
||||
|
||||
interface LogViewerProps {
|
||||
@@ -7,7 +7,14 @@ interface LogViewerProps {
|
||||
follow?: boolean
|
||||
}
|
||||
|
||||
export function LogViewer({ name, lines = 50, follow = true }: LogViewerProps) {
|
||||
export interface LogViewerHandle {
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
export const LogViewer = forwardRef<LogViewerHandle, LogViewerProps>(function LogViewer(
|
||||
{ name, lines = 50, follow = true },
|
||||
ref,
|
||||
) {
|
||||
const [logs, setLogs] = useState<string[]>([])
|
||||
const [connected, setConnected] = useState(false)
|
||||
const bottomRef = useRef<HTMLDivElement>(null)
|
||||
@@ -40,6 +47,8 @@ export function LogViewer({ name, lines = 50, follow = true }: LogViewerProps) {
|
||||
return () => es.close()
|
||||
}, [name, lines, follow])
|
||||
|
||||
useImperativeHandle(ref, () => ({ clear: () => setLogs([]) }), [])
|
||||
|
||||
useEffect(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: "smooth" })
|
||||
}, [logs])
|
||||
@@ -68,4 +77,4 @@ export function LogViewer({ name, lines = 50, follow = true }: LogViewerProps) {
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
167
app/src/components/ProgramActions.tsx
Normal file
167
app/src/components/ProgramActions.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { useState } from "react"
|
||||
import {
|
||||
FileCheck,
|
||||
FlaskConical,
|
||||
Hammer,
|
||||
Loader2,
|
||||
Plug,
|
||||
ShieldCheck,
|
||||
Sparkles,
|
||||
Unplug,
|
||||
type LucideIcon,
|
||||
} from "lucide-react"
|
||||
import { useProgramAction } from "@/services/api/hooks"
|
||||
|
||||
interface ActionConfig {
|
||||
icon: LucideIcon
|
||||
label: string
|
||||
color: string
|
||||
hoverBg: string
|
||||
borderColor: string
|
||||
}
|
||||
|
||||
const ACTION_CONFIG: Record<string, ActionConfig> = {
|
||||
build: { icon: Hammer, label: "Build", color: "text-blue-400", hoverBg: "hover:bg-blue-800/30", borderColor: "border-blue-800" },
|
||||
test: { icon: FlaskConical, label: "Test", color: "text-purple-400", hoverBg: "hover:bg-purple-800/30", borderColor: "border-purple-800" },
|
||||
lint: { icon: Sparkles, label: "Lint", color: "text-amber-400", hoverBg: "hover:bg-amber-800/30", borderColor: "border-amber-800" },
|
||||
"type-check": { icon: FileCheck, label: "Type Check", color: "text-cyan-400", hoverBg: "hover:bg-cyan-800/30", borderColor: "border-cyan-800" },
|
||||
check: { icon: ShieldCheck, label: "Check All", color: "text-green-400", hoverBg: "hover:bg-green-800/30", borderColor: "border-green-800" },
|
||||
install: { icon: Plug, label: "Install", color: "text-green-400", hoverBg: "hover:bg-green-800/30", borderColor: "border-green-800" },
|
||||
uninstall: { icon: Unplug, label: "Uninstall", color: "text-red-400", hoverBg: "hover:bg-red-800/30", borderColor: "border-red-800" },
|
||||
}
|
||||
|
||||
const DEV_ACTIONS = ["build", "test", "lint", "type-check", "check"]
|
||||
|
||||
interface ProgramActionsProps {
|
||||
name: string
|
||||
actions: string[]
|
||||
installed?: boolean | null
|
||||
compact?: boolean
|
||||
}
|
||||
|
||||
function visibleActions(
|
||||
actions: string[],
|
||||
installed: boolean | null | undefined,
|
||||
compact: boolean,
|
||||
): string[] {
|
||||
if (compact) {
|
||||
// Table: only install/uninstall based on state
|
||||
if (installed === true) return actions.includes("uninstall") ? ["uninstall"] : []
|
||||
if (installed === false) return actions.includes("install") ? ["install"] : []
|
||||
// null — show install if available (frontends, etc.)
|
||||
return actions.includes("install") ? ["install"] : []
|
||||
}
|
||||
|
||||
// Detail page: dev actions always, install/uninstall based on state
|
||||
const visible: string[] = []
|
||||
for (const a of DEV_ACTIONS) {
|
||||
if (actions.includes(a)) visible.push(a)
|
||||
}
|
||||
if (installed === true) {
|
||||
if (actions.includes("uninstall")) visible.push("uninstall")
|
||||
} else if (installed === false) {
|
||||
if (actions.includes("install")) visible.push("install")
|
||||
} else {
|
||||
// null — show both if available
|
||||
if (actions.includes("install")) visible.push("install")
|
||||
if (actions.includes("uninstall")) visible.push("uninstall")
|
||||
}
|
||||
return visible
|
||||
}
|
||||
|
||||
export function ProgramActions({ name, actions, installed, compact }: 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)
|
||||
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 })
|
||||
}
|
||||
},
|
||||
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)
|
||||
text = parsed.detail ?? text
|
||||
} catch {
|
||||
text = (err as Error).message ?? text
|
||||
}
|
||||
setOutput({ action, text, ok: false })
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Link } from "react-router-dom"
|
||||
import { Play, RefreshCw, Square } from "lucide-react"
|
||||
import type { ComponentSummary, HealthStatus } from "@/types"
|
||||
import type { JobSummary, HealthStatus } from "@/types"
|
||||
import { useServiceAction } from "@/services/api/hooks"
|
||||
import { SectionHeader } from "./SectionHeader"
|
||||
import { StackBadge } from "./StackBadge"
|
||||
|
||||
interface ScheduledSectionProps {
|
||||
jobs: ComponentSummary[]
|
||||
jobs: JobSummary[]
|
||||
statuses: HealthStatus[]
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function ScheduledSection({ jobs, statuses }: ScheduledSectionProps) {
|
||||
)
|
||||
}
|
||||
|
||||
function ScheduledRow({ job, health }: { job: ComponentSummary; health?: HealthStatus }) {
|
||||
function ScheduledRow({ job, health }: { job: JobSummary; health?: HealthStatus }) {
|
||||
const { mutate, isPending } = useServiceAction()
|
||||
const isDown = health?.status === "down"
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMemo } from "react"
|
||||
import type { ComponentSummary, HealthStatus } from "@/types"
|
||||
import type { ServiceSummary, HealthStatus } from "@/types"
|
||||
import { ComponentCard } from "./ComponentCard"
|
||||
import { SectionHeader } from "./SectionHeader"
|
||||
|
||||
interface ServiceSectionProps {
|
||||
services: ComponentSummary[]
|
||||
services: ServiceSummary[]
|
||||
statuses: HealthStatus[]
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import { useNavigate } from "react-router-dom"
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import { Check } from "lucide-react"
|
||||
import { apiClient } from "@/services/api/client"
|
||||
import type { ComponentDetail } from "@/types"
|
||||
import type { AnyDetail } from "@/types"
|
||||
import { ComponentFields } from "@/components/ComponentFields"
|
||||
|
||||
interface ConfigPanelProps {
|
||||
component: ComponentDetail
|
||||
configSection: "services" | "jobs" | "components"
|
||||
component: AnyDetail
|
||||
configSection: "services" | "jobs" | "programs"
|
||||
onRefetch: () => void
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function ConfigPanel({ component, configSection, onRefetch }: ConfigPanel
|
||||
await apiClient.put(`/config/${configSection}/${compName}`, { config })
|
||||
setMessage({ type: "ok", text: "Saved to castle.yaml" })
|
||||
onRefetch()
|
||||
qc.invalidateQueries({ queryKey: ["components"] })
|
||||
qc.invalidateQueries({ queryKey: [configSection] })
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
setMessage({ type: "error", text: msg })
|
||||
@@ -33,7 +33,7 @@ export function ConfigPanel({ component, configSection, onRefetch }: ConfigPanel
|
||||
const handleDelete = async (compName: string) => {
|
||||
try {
|
||||
await apiClient.delete(`/config/${configSection}/${compName}`)
|
||||
qc.invalidateQueries({ queryKey: ["components"] })
|
||||
qc.invalidateQueries({ queryKey: [configSection] })
|
||||
navigate("/")
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e)
|
||||
|
||||
Reference in New Issue
Block a user