chore: snapshot workflow migration progress
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,32 @@ const EMPTY_FORM = {
|
||||
lighting_only: false,
|
||||
shadow_catcher_enabled: false,
|
||||
camera_orbit: true,
|
||||
workflow_input_schema_text: '[]',
|
||||
}
|
||||
|
||||
function stringifyWorkflowInputSchema(value: unknown): string {
|
||||
try {
|
||||
return JSON.stringify(Array.isArray(value) ? value : [], null, 2)
|
||||
} catch {
|
||||
return '[]'
|
||||
}
|
||||
}
|
||||
|
||||
function parseWorkflowInputSchemaText(rawValue: unknown): unknown[] {
|
||||
const text = typeof rawValue === 'string' ? rawValue.trim() : ''
|
||||
if (!text) return []
|
||||
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(text)
|
||||
} catch {
|
||||
throw new Error('Workflow input schema must be valid JSON')
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('Workflow input schema must be a JSON array')
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
export default function RenderTemplateTable() {
|
||||
@@ -43,7 +69,7 @@ export default function RenderTemplateTable() {
|
||||
const [addFile, setAddFile] = useState<File | null>(null)
|
||||
const [cloneBlendFrom, setCloneBlendFrom] = useState<string>('')
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
const [editDraft, setEditDraft] = useState<Partial<RenderTemplate>>({})
|
||||
const [editDraft, setEditDraft] = useState<(Partial<RenderTemplate> & { workflow_input_schema_text?: string })>({})
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const reuploadRef = useRef<HTMLInputElement>(null)
|
||||
const [reuploadId, setReuploadId] = useState<string | null>(null)
|
||||
@@ -75,6 +101,7 @@ export default function RenderTemplateTable() {
|
||||
fd.append('lighting_only', String(form.lighting_only))
|
||||
fd.append('shadow_catcher_enabled', String(form.shadow_catcher_enabled))
|
||||
fd.append('camera_orbit', String(form.camera_orbit))
|
||||
fd.append('workflow_input_schema', JSON.stringify(parseWorkflowInputSchemaText(form.workflow_input_schema_text)))
|
||||
return createRenderTemplate(fd)
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -85,7 +112,7 @@ export default function RenderTemplateTable() {
|
||||
setCloneBlendFrom('')
|
||||
setShowAdd(false)
|
||||
},
|
||||
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to create template'),
|
||||
onError: (e: any) => toast.error(e.response?.data?.detail || e.message || 'Failed to create template'),
|
||||
})
|
||||
|
||||
const updateMut = useMutation({
|
||||
@@ -96,7 +123,7 @@ export default function RenderTemplateTable() {
|
||||
qc.invalidateQueries({ queryKey: ['render-templates'] })
|
||||
setEditingId(null)
|
||||
},
|
||||
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to update'),
|
||||
onError: (e: any) => toast.error(e.response?.data?.detail || e.message || 'Failed to update'),
|
||||
})
|
||||
|
||||
const deleteMut = useMutation({
|
||||
@@ -128,6 +155,7 @@ export default function RenderTemplateTable() {
|
||||
shadow_catcher_enabled: t.shadow_catcher_enabled,
|
||||
camera_orbit: t.camera_orbit,
|
||||
output_type_ids: t.output_type_ids ?? [],
|
||||
workflow_input_schema: t.workflow_input_schema ?? [],
|
||||
}),
|
||||
onSuccess: () => {
|
||||
toast.success('Template duplicated')
|
||||
@@ -147,13 +175,19 @@ export default function RenderTemplateTable() {
|
||||
lighting_only: t.lighting_only,
|
||||
shadow_catcher_enabled: t.shadow_catcher_enabled,
|
||||
camera_orbit: t.camera_orbit,
|
||||
workflow_input_schema_text: stringifyWorkflowInputSchema(t.workflow_input_schema),
|
||||
is_active: t.is_active,
|
||||
})
|
||||
}
|
||||
|
||||
function saveEdit() {
|
||||
if (!editingId) return
|
||||
updateMut.mutate({ id: editingId, data: editDraft as Record<string, unknown> })
|
||||
const data: Record<string, unknown> = { ...editDraft }
|
||||
if (Object.prototype.hasOwnProperty.call(editDraft, 'workflow_input_schema_text')) {
|
||||
data.workflow_input_schema = parseWorkflowInputSchemaText(editDraft.workflow_input_schema_text)
|
||||
delete data.workflow_input_schema_text
|
||||
}
|
||||
updateMut.mutate({ id: editingId, data })
|
||||
}
|
||||
|
||||
// Render the edit form grid (shared between edit-row and add-row)
|
||||
@@ -174,6 +208,9 @@ export default function RenderTemplateTable() {
|
||||
if (field === 'lighting_only') return editDraft.lighting_only ?? t!.lighting_only
|
||||
if (field === 'shadow_catcher_enabled') return editDraft.shadow_catcher_enabled ?? t!.shadow_catcher_enabled
|
||||
if (field === 'camera_orbit') return editDraft.camera_orbit ?? t!.camera_orbit
|
||||
if (field === 'workflow_input_schema_text') {
|
||||
return editDraft.workflow_input_schema_text ?? stringifyWorkflowInputSchema(t!.workflow_input_schema)
|
||||
}
|
||||
if (field === 'is_active') return editDraft.is_active ?? t!.is_active
|
||||
return (editDraft as any)[field] ?? (t as any)[field]
|
||||
}
|
||||
@@ -381,7 +418,27 @@ export default function RenderTemplateTable() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 4: Active + Save/Cancel */}
|
||||
<div className="mt-4">
|
||||
<label className="block text-xs font-medium text-content-muted mb-1">
|
||||
Workflow Input Schema (JSON)
|
||||
</label>
|
||||
<textarea
|
||||
className="input-sm w-full min-h-36 font-mono text-xs"
|
||||
value={String(val('workflow_input_schema_text') ?? '[]')}
|
||||
onChange={(e) => set('workflow_input_schema_text', e.target.value)}
|
||||
placeholder='[{"key":"studio_variant","label":"Studio Variant","type":"select","options":[{"value":"default","label":"Default"}]}]'
|
||||
/>
|
||||
<p className="mt-1 text-xs text-content-muted">
|
||||
Defines additional `resolve_template` node inputs for this .blend template.
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-content-muted">
|
||||
Matching variants can be bound inside the template via markers like
|
||||
`template-input:studio_variant=warm` or a `template_input=studio_variant=warm`
|
||||
custom property on collections, objects, or worlds.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Row 5: Active + Save/Cancel */}
|
||||
<div className="flex items-center justify-between mt-4 pt-3 border-t border-border-light">
|
||||
{isEdit ? (
|
||||
<label className="flex items-center gap-2">
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { OutputTypeWorkflowRolloutMode } from '../../api/outputTypes'
|
||||
|
||||
export interface OutputTypeRolloutPresentation {
|
||||
badgeLabel: string
|
||||
badgeClassName: string
|
||||
statusLabel: string
|
||||
statusClassName: string
|
||||
operatorHint: string
|
||||
rowSummary: string
|
||||
}
|
||||
|
||||
interface OutputTypeRolloutPresentationOptions {
|
||||
hasWorkflowLink: boolean
|
||||
workflowRolloutMode: OutputTypeWorkflowRolloutMode
|
||||
hasBlockingIssues?: boolean
|
||||
}
|
||||
|
||||
export function getOutputTypeRolloutPresentation({
|
||||
hasWorkflowLink,
|
||||
workflowRolloutMode,
|
||||
hasBlockingIssues = false,
|
||||
}: OutputTypeRolloutPresentationOptions): OutputTypeRolloutPresentation {
|
||||
if (!hasWorkflowLink) {
|
||||
return {
|
||||
badgeLabel: 'Legacy Only',
|
||||
badgeClassName: 'bg-surface-muted text-content-muted',
|
||||
statusLabel: 'Production: Legacy',
|
||||
statusClassName: 'bg-slate-100 text-slate-700',
|
||||
operatorHint:
|
||||
'No workflow is linked. Production stays entirely on the legacy dispatcher until a compatible graph workflow is attached.',
|
||||
rowSummary: 'No linked graph workflow.',
|
||||
}
|
||||
}
|
||||
|
||||
if (hasBlockingIssues) {
|
||||
return {
|
||||
badgeLabel: 'Contract Blocked',
|
||||
badgeClassName: 'bg-red-100 text-red-700',
|
||||
statusLabel: 'Do Not Promote',
|
||||
statusClassName: 'bg-red-100 text-red-700',
|
||||
operatorHint:
|
||||
'The current workflow binding is contract-invalid. Keep legacy authoritative until family, artifact, and rollout settings are fixed.',
|
||||
rowSummary: 'Linked workflow needs contract fixes before rollout.',
|
||||
}
|
||||
}
|
||||
|
||||
switch (workflowRolloutMode) {
|
||||
case 'graph':
|
||||
return {
|
||||
badgeLabel: 'Graph Authoritative',
|
||||
badgeClassName: 'bg-status-success-bg text-status-success-text',
|
||||
statusLabel: 'Production: Graph',
|
||||
statusClassName: 'bg-emerald-100 text-emerald-700',
|
||||
operatorHint:
|
||||
'Graph dispatch is authoritative for production. Legacy remains the operational fallback if graph dispatch fails.',
|
||||
rowSummary: 'Graph drives production with legacy fallback armed.',
|
||||
}
|
||||
case 'shadow':
|
||||
return {
|
||||
badgeLabel: 'Shadow',
|
||||
badgeClassName: 'bg-status-info-bg text-status-info-text',
|
||||
statusLabel: 'Production: Legacy',
|
||||
statusClassName: 'bg-sky-100 text-sky-700',
|
||||
operatorHint:
|
||||
'Legacy stays authoritative while the graph runs as an observer for parity and rollout-gate checks.',
|
||||
rowSummary: 'Graph observes only; legacy remains authoritative.',
|
||||
}
|
||||
case 'legacy_only':
|
||||
default:
|
||||
return {
|
||||
badgeLabel: 'Legacy Only',
|
||||
badgeClassName: 'bg-surface-muted text-content-muted',
|
||||
statusLabel: 'Production: Legacy',
|
||||
statusClassName: 'bg-slate-100 text-slate-700',
|
||||
operatorHint:
|
||||
'A workflow is linked for authoring and future rollout, but production dispatch remains on the legacy path.',
|
||||
rowSummary: 'Linked graph is not active in production.',
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user