feat: working jobs page with sse

This commit is contained in:
2024-03-08 02:08:01 +01:00
parent 28b16aee7a
commit e87a9bef18
26 changed files with 1521 additions and 535 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
import _ from 'lodash'
import { clsx } from '../utils.js'
import { clsx } from '@/lib/utils'
export const Value = ({ value, borderless }) => {
return Array.isArray(value) ? (
+114
View File
@@ -0,0 +1,114 @@
import _ from 'lodash'
import { useEffect, useState } from 'preact/hooks'
import { durationToString } from './lib/utils'
/**
* @param {import('@/jobs.ts').QueuedJob} props
*/
export const QueuedJob = ({ uuid, name, submitter, submittedAt }) => (
<div class="job queued" title={uuid}>
<div class="name">{name}</div>
<div class="footer">
<div class="submitted-at">{new Date(submittedAt).toLocaleString()}</div>
</div>
</div>
)
/**
* @param {import('@/jobs.ts').CompletedJob} props
*/
export const CompletedJob = ({
uuid,
name,
submitter,
submittedAt,
startedAt,
completedAt,
successful,
error,
}) => {
return (
<div class="job completed" title={uuid}>
<div class="name">{name}</div>
<div class="footer">
<div class="submitted-at">{new Date(submittedAt).toLocaleString()}</div>
<div class="delta">{durationToString(startedAt, completedAt)}</div>
</div>
</div>
)
}
export const JobsPage = ({}) => {
const [jobStore, setJobStore] = useState({
queuedJobs: {},
completedJobs: {},
})
useEffect(async () => {
const res = await fetch('/api/jobs')
const { queuedJobs, completedJobs } = await res.json()
const result = {
queuedJobs: {},
completedJobs: {},
}
for (const item of queuedJobs) {
result.queuedJobs[item.uuid] = item
}
for (const item of completedJobs) {
result.completedJobs[item.uuid] = item
}
setJobStore(result)
// Setup SSE
const es = new EventSource('/api/sse')
es.addEventListener('message', ({ data }) => {
const event = JSON.parse(data)
if (event.type === 'added') {
setJobStore(s => ({
...s,
queuedJobs: {
...s.queuedJobs,
[event.job.uuid]: event.job,
},
}))
}
if (event.type === 'completed') {
setJobStore(s => ({
queuedJobs: {
..._.omit(s.queuedJobs, event.job.uuid),
},
completedJobs: {
...s.completedJobs,
[event.job.uuid]: event.job,
},
}))
}
})
}, [])
return (
<>
<h2>Queued Jobs</h2>
<div class="list">
{Object.values(jobStore.queuedJobs)
.toReversed()
.map(queuedJob => (
<QueuedJob {...queuedJob} />
))}
</div>
<h2>Completed Jobs</h2>
<div class="list">
{Object.values(jobStore.completedJobs)
.toReversed()
.map(completedJob => (
<CompletedJob {...completedJob} />
))}
</div>
</>
)
}
+233
View File
@@ -0,0 +1,233 @@
function inspectZod(schema, path = []) {
if ('typeName' in schema._def) {
debug(' '.repeat(path.length), path.at(-1) ?? '<root>', '::', schema._def.typeName)
if (schema._def.typeName === 'ZodUnion') {
schema._def.options.forEach(option => inspectZod(option, [...path, '<union>']))
return
}
if (schema._def.typeName === 'ZodObject') {
Object.entries(schema._def.shape()).forEach(([k, v]) => {
inspectZod(v, [...path, k])
})
return
}
if (schema._def.typeName === 'ZodTuple') {
schema._def.items.forEach((item, i) => {
inspectZod(item, [...path, i])
})
return
}
if (schema._def.typeName === 'ZodArray') {
inspectZod(schema._def.type, [...path, '<index>'])
return
}
if (schema._def.typeName === 'ZodOptional') {
inspectZod(schema._def.innerType, [...path, '?'])
return
}
if (schema._def.typeName === 'ZodLiteral') {
debug(' '.repeat(path.length + 1), schema._def)
return
}
if (schema._def.typeName === 'ZodString') {
return
}
if (schema._def.typeName === 'ZodRecord') {
inspectZod(schema._def.keyType, [...path, '<key>'])
inspectZod(schema._def.valueType, [...path, '<value>'])
return
}
debug('_def:', schema._def)
}
}
const ZodField = ({ value, setValue, schema, path }) => {
if (schema._def.typeName === 'ZodString') {
return (
<div>
<label>{path.at(-1)}</label>
<input value={value} onInput={e => setValue(path, e.target.value)} />
</div>
)
}
if (schema._def.typeName === 'ZodNumber') {
return (
<div>
<label>{path.at(-1)}</label>
<input value={value} onInput={e => setValue(path, e.target.value)} />
</div>
)
}
if (schema._def.typeName === 'ZodBoolean') {
return (
<div>
<label>{path.at(-1)}</label>
<input
type="checkbox"
checked={value}
onInput={e => setValue(path, e.target.checked)}
/>
</div>
)
}
if (schema._def.typeName === 'ZodDate') {
return (
<div>
<label>{path.at(-1)}</label>
<input type="date" value={value} onInput={e => setValue(path, e.target.value)} />
</div>
)
}
if (schema._def.typeName === 'ZodObject') {
return (
<div>
{Object.entries(schema._def.shape()).map(([k, v]) => {
return (
<ZodField
value={value[k]}
setValue={setValue}
schema={v}
path={[...path, k]}
/>
)
})}
</div>
)
}
if (schema._def.typeName === 'ZodArray') {
return (
<div>
{value.map((v, i) => {
return (
<ZodField
value={v}
setValue={setValue}
schema={schema._def.type}
path={[...path, i]}
/>
)
})}
<button onClick={() => setValue(path, [...value, ''])}>Add</button>
</div>
)
}
if (schema._def.typeName === 'ZodOptional') {
return (
<div>
<ZodField
value={value}
setValue={setValue}
schema={schema._def.innerType}
path={path}
/>
</div>
)
}
if (schema._def.typeName === 'ZodLiteral') {
return (
<div>
<ZodField value={value} setValue={setValue} schema={schema} path={path} />
</div>
)
}
if (schema._def.typeName === 'ZodUnion') {
return (
<div>
<select value={value} onInput={e => setValue(path, e.target.value)}>
{schema._def.options.map(option => {
return <option value={option._def.value}>{option._def.value}</option>
})}
</select>
</div>
)
}
}
const ZodObject = ({ value, setValue, schema, path }) => {
return (
<div>
{Object.entries(schema._def.shape()).map(([k, v]) => {
return (
<ZodField value={value[k]} setValue={setValue} schema={v} path={[...path, k]} />
)
})}
</div>
)
}
const ZodArray = ({ value, setValue, schema, path }) => {
return (
<div>
{value.map((v, i) => {
return (
<ZodField
value={v}
setValue={setValue}
schema={schema._def.type}
path={[...path, i]}
/>
)
})}
<button onClick={() => setValue(path, [...value, ''])}>Add</button>
</div>
)
}
const ZodOptional = ({ value, setValue, schema, path }) => {
return (
<div>
<ZodField
value={value}
setValue={setValue}
schema={schema._def.innerType}
path={path}
/>
</div>
)
}
const ZodLiteral = ({ value, setValue, schema, path }) => {
return (
<div>
<ZodField value={value} setValue={setValue} schema={schema} path={path} />
</div>
)
}
const ZodUnion = ({ value, setValue, schema, path }) => {
return (
<div>
<select value={value} onInput={e => setValue(path, e.target.value)}>
{schema._def.options.map(option => {
return <option value={option._def.value}>{option._def.value}</option>
})}
</select>
</div>
)
}
export const ZodForm = ({ value, setValue, schema }) => {
return (
<div>
{schema._def.typeName === 'ZodObject' && (
<ZodObject value={value} setValue={setValue} schema={schema} path={[]} />
)}
{schema._def.typeName === 'ZodArray' && (
<ZodArray value={value} setValue={setValue} schema={schema} path={[]} />
)}
{schema._def.typeName === 'ZodOptional' && (
<ZodOptional value={value} setValue={setValue} schema={schema} path={[]} />
)}
{schema._def.typeName === 'ZodLiteral' && (
<ZodLiteral value={value} setValue={setValue} schema={schema} path={[]} />
)}
{schema._def.typeName === 'ZodUnion' && (
<ZodUnion value={value} setValue={setValue} schema={schema} path={[]} />
)}
</div>
)
}
+21
View File
@@ -0,0 +1,21 @@
export function durationToString(from, to) {
from = new Date(from)
to = new Date(to)
let s = to.getTime() - from.getTime()
const millis = s % 1000
s = (s - millis) / 1000
if (s === 0) return `${millis}ms`
const seconds = s % 60
s = (s - seconds) / 60
if (s === 0) return `${seconds}s${millis}ms`
const minutes = s % 60
s = (s - minutes) / 60
if (s === 0) return `${minutes}s${seconds}s`
const hours = s
return `${hours}h${minutes}m`
}