feat: Add type-check script and enhance UI for better text handling

This commit is contained in:
2026-06-27 12:46:15 -07:00
parent 3997682ab0
commit a3a862b63f
10 changed files with 121 additions and 37 deletions

View File

@@ -20,16 +20,16 @@ export function DetailHeader({ backTo, backLabel, name, behavior, stack, source,
<ArrowLeft size={16} /> {backLabel}
</Link>
<div className="flex items-start justify-between mb-4">
<h1 className="text-2xl font-bold">{name}</h1>
<div className="flex items-start justify-between gap-3 flex-wrap mb-4">
<h1 className="text-2xl font-bold break-all min-w-0">{name}</h1>
{children}
</div>
<div className="flex items-center gap-3 mb-6">
<div className="flex items-center gap-3 flex-wrap mb-6">
<BehaviorBadge behavior={behavior ?? null} />
<StackBadge stack={stack ?? null} />
{source && (
<span className="text-sm text-[var(--muted)] font-mono">{source}</span>
<span className="text-sm text-[var(--muted)] font-mono break-all min-w-0">{source}</span>
)}
</div>
</>

View File

@@ -96,7 +96,7 @@ export function ServiceFields({ service, onSave, onDelete }: Props) {
<input
value={runProgram}
onChange={(e) => setRunProgram(e.target.value)}
className="w-56 bg-black/30 border border-[var(--border)] rounded px-2 py-1 text-xs font-mono focus:outline-none focus:border-[var(--primary)]"
className="w-full sm:w-56 bg-black/30 border border-[var(--border)] rounded px-2 py-1 text-xs font-mono focus:outline-none focus:border-[var(--primary)]"
/>
</Field>
<TextField

View File

@@ -26,9 +26,9 @@ export function SystemdPanel({ name, systemd }: SystemdPanelProps) {
</div>
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm mt-3">
<span className="text-[var(--muted)]">Unit</span>
<span className="font-mono">{systemd.unit_name}</span>
<span className="font-mono break-all">{systemd.unit_name}</span>
<span className="text-[var(--muted)]">Path</span>
<span className="font-mono">{systemd.unit_path}</span>
<span className="font-mono break-all">{systemd.unit_path}</span>
{systemd.timer && (
<>
<span className="text-[var(--muted)]">Timer</span>

View File

@@ -16,8 +16,8 @@ export function Field({
}) {
return (
<div className="flex items-start gap-4">
<label className="w-32 shrink-0 text-sm font-medium pt-1.5">{label}</label>
<div className="flex-1">
<label className="w-24 sm:w-32 shrink-0 text-sm font-medium pt-1.5">{label}</label>
<div className="flex-1 min-w-0">
{children}
{hint && <p className="text-xs text-[var(--muted)] mt-1 leading-snug">{hint}</p>}
</div>
@@ -54,20 +54,34 @@ export function TextField({
)
}
// A value that is *exactly* a secret ref → fully editable via SecretsEditor.
const SECRET_RE = /^\$\{secret:([^}]+)\}$/
// A secret ref embedded anywhere in a value (e.g. `neo4j/${secret:PW}`). These
// are composite literals, so we surface them read-only rather than letting
// merged() rewrite the value and clobber the surrounding text.
const EMBEDDED_SECRET_RE = /\$\{secret:([^}]+)\}/g
/** Hook for editing a run env that mixes plain vars and `${secret:NAME}` refs.
* Returns the editor element plus a `merged()` that reconstitutes the env. */
export function useEnvSecrets(initial: Record<string, string>) {
const { plain, secretRefs } = useMemo(() => {
const { plain, secretRefs, embeddedRefs } = useMemo(() => {
const p: Record<string, string> = {}
const s: Record<string, string> = {}
const e: { envKey: string; secretName: string }[] = []
for (const [k, v] of Object.entries(initial)) {
const m = SECRET_RE.exec(v)
if (m) s[k] = m[1]
else p[k] = v
if (m) {
s[k] = m[1]
} else {
// Composite values stay in `plain` so their literal text round-trips
// untouched; any embedded secret names are surfaced read-only below.
p[k] = v
for (const em of v.matchAll(EMBEDDED_SECRET_RE)) {
e.push({ envKey: k, secretName: em[1] })
}
}
}
return { plain: p, secretRefs: s }
return { plain: p, secretRefs: s, embeddedRefs: e }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@@ -92,12 +106,16 @@ export function useEnvSecrets(initial: Record<string, string>) {
</p>
{Object.entries(env).map(([key, val]) => (
<div key={key} className="flex items-center gap-2">
<input value={key} readOnly className={`w-56 ${INPUT} text-xs text-[var(--muted)]`} />
<span className="text-[var(--muted)]">=</span>
<input
value={key}
readOnly
className={`w-24 sm:w-56 min-w-0 ${INPUT} text-xs text-[var(--muted)]`}
/>
<span className="text-[var(--muted)] shrink-0">=</span>
<input
value={val}
onChange={(e) => setEnv((p) => ({ ...p, [key]: e.target.value }))}
className={`flex-1 ${INPUT} text-xs font-mono`}
className={`flex-1 min-w-0 ${INPUT} text-xs font-mono`}
/>
<button
onClick={() =>
@@ -107,7 +125,7 @@ export function useEnvSecrets(initial: Record<string, string>) {
return n
})
}
className="text-red-400 hover:text-red-300 p-0.5"
className="text-red-400 hover:text-red-300 p-0.5 shrink-0"
title="Remove"
>
<Trash2 size={12} />
@@ -127,6 +145,19 @@ export function useEnvSecrets(initial: Record<string, string>) {
</Field>
<Field label="Secrets">
<SecretsEditor secrets={secrets} onSecretsChange={setSecrets} />
{embeddedRefs.length > 0 && (
<div className="mt-2 space-y-1">
{embeddedRefs.map(({ envKey, secretName }) => (
<p
key={`${envKey}:${secretName}`}
className="text-xs font-mono text-[var(--muted)]"
>
<span className="text-[var(--foreground)]">{secretName}</span> embedded in{" "}
{envKey} (read-only)
</p>
))}
</div>
)}
</Field>
</div>
)