feat: working jobs page with sse
This commit is contained in:
@@ -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) ? (
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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`
|
||||
}
|
||||
Reference in New Issue
Block a user