feat: add SQS + EventBridge

This commit is contained in:
Zakaria
2026-09-24 22:34:04 -04:00
parent 9bbe80b5f2
commit 1b129e9dcf
37 changed files with 2902 additions and 66 deletions
@@ -1,5 +1,5 @@
import {FormEvent, useState} from 'react'
import {Plus} from 'lucide-react'
import {FormEvent, useRef, useState} from 'react'
import {Plus, Trash2} from 'lucide-react'
import type {FieldSchema, ServiceSchema} from '@/types/schema'
interface DynamicFormRendererProps {
@@ -146,6 +146,10 @@ function FieldInput({field, required, maxLength, value, invalid, messageId, onCh
)
}
if (field.type === 'key-value') {
return <KeyValueInput field={field} value={value} onChange={onChange}/>
}
return (
<input
type={field.type === 'password' ? 'password' : 'text'}
@@ -163,6 +167,35 @@ function FieldInput({field, required, maxLength, value, invalid, messageId, onCh
)
}
function KeyValueInput({field, value, onChange}: {field: FieldSchema; value: string; onChange: (value: string) => void}) {
const [entries, setEntries] = useState(() => keyValueEntries(value))
const entriesRef = useRef(entries)
function commit(next: Array<{key: string; value: string}>) {
entriesRef.current = next
setEntries(next)
onChange(JSON.stringify(next))
}
function updateEntry(index: number, property: 'key' | 'value', nextValue: string) {
const next = entriesRef.current.map((entry, entryIndex) => entryIndex === index ? {...entry, [property]: nextValue} : entry)
commit(next)
}
return (
<div className="key-value-editor">
{entries.map((entry, index) => (
<div className="key-value-row" key={index}>
<input className="input" value={entry.key} placeholder="Variable name" aria-label={`${field.label} key ${index + 1}`} onChange={(event) => updateEntry(index, 'key', event.target.value)}/>
<input className="input" value={entry.value} placeholder="Value" aria-label={`${field.label} value ${index + 1}`} onChange={(event) => updateEntry(index, 'value', event.target.value)}/>
<button className="icon-btn" type="button" aria-label={`Remove ${entry.key || 'environment variable'}`} onClick={() => commit(entriesRef.current.filter((_, entryIndex) => entryIndex !== index))}><Trash2 size={13}/></button>
</div>
))}
<button className="button compact" type="button" aria-label="Add variable" onClick={() => commit([...entriesRef.current, {key: '', value: ''}])}><Plus size={13}/> Add variable</button>
</div>
)
}
function readFileAsBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
@@ -187,13 +220,27 @@ function getInitialFormValues(
if (initialValues) {
for (const [key, val] of Object.entries(initialValues)) {
if (val !== undefined && val !== null) {
defaults[key] = String(val)
const field = fields.find((entry) => entry.name === key)
defaults[key] = field?.type === 'key-value' && typeof val === 'object'
? JSON.stringify(Object.entries(val as Record<string, unknown>).map(([entryKey, entryValue]) => ({key: entryKey, value: String(entryValue)})))
: String(val)
}
}
}
return defaults
}
function keyValueEntries(value: string): Array<{key: string; value: string}> {
if (!value) return []
try {
const parsed: unknown = JSON.parse(value)
if (!Array.isArray(parsed)) return []
return parsed.filter((entry): entry is {key: string; value: string} => Boolean(entry) && typeof entry === 'object' && typeof entry.key === 'string' && typeof entry.value === 'string')
} catch {
return []
}
}
function defaultValues(fields: FieldSchema[]): Record<string, string> {
const values = Object.fromEntries(fields.map((field) => [field.name, field.defaultValue ?? '']))
for (const field of fields) {