feat: dynamic latest logs in deploy page
parent
4ff33b06ab
commit
686e009368
@ -1,24 +1,29 @@
|
|||||||
|
import { useStore } from '@nanostores/preact'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
import { triggerCounter } from './store.js'
|
||||||
|
|
||||||
|
export const JobLogs = ({ endpoint }) => {
|
||||||
|
const $triggerCounter = useStore(triggerCounter)
|
||||||
|
|
||||||
export const JobLogs = ({}) => {
|
|
||||||
const [logLines, setLogLines] = useState([])
|
const [logLines, setLogLines] = useState([])
|
||||||
|
|
||||||
useEffect(async () => {
|
useEffect(async () => {
|
||||||
const res = await fetch(location.href + '/logs?format=raw')
|
const res = await fetch(endpoint + '?format=raw')
|
||||||
const rawLogs = (await res.text()).trim()
|
const rawLogs = (await res.text()).trim()
|
||||||
|
|
||||||
if (rawLogs.length > 0) setLogLines(rawLogs.split('\n'))
|
if (rawLogs.length > 0) setLogLines(rawLogs.split('\n'))
|
||||||
|
else setLogLines([])
|
||||||
|
|
||||||
// Setup SSE
|
// Setup SSE
|
||||||
const es = new EventSource(location.href + '/logs?format=sse')
|
const es = new EventSource(endpoint + '?format=sse')
|
||||||
es.addEventListener('message', ({ data }) => {
|
es.addEventListener('message', ({ data }) => {
|
||||||
const event = JSON.parse(data)
|
const event = JSON.parse(data)
|
||||||
setLogLines(lines => [...lines, event.content])
|
setLogLines(lines => [...lines, event.content])
|
||||||
})
|
})
|
||||||
}, [])
|
}, [$triggerCounter])
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return (
|
return (
|
||||||
<pre><code>{logLines.join('\n')}</code></pre>
|
<pre title={`Triggered ${$triggerCounter} times`}><code>{logLines.join('\n')}</code></pre>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
import { atom } from 'nanostores'
|
||||||
|
|
||||||
|
export const triggerCounter = atom(0)
|
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
import { Value } from '@/client/Inspect'
|
||||||
|
import Docker from 'dockerode'
|
||||||
|
|
||||||
|
const { name } = Astro.props
|
||||||
|
|
||||||
|
const docker = new Docker({ socketPath: '/var/run/docker.sock' })
|
||||||
|
const containers = await docker.listContainers()
|
||||||
|
const container = containers.find(c => c.Names.includes(`/${name}`))
|
||||||
|
---
|
||||||
|
|
||||||
|
<h2>Containers</h2>
|
||||||
|
|
||||||
|
{
|
||||||
|
container ? (
|
||||||
|
<div class="card">
|
||||||
|
<div class="container">
|
||||||
|
<Value value={container} borderless />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p>No container found</p>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
import { EventEmitter } from 'events'
|
||||||
|
import type { Job } from './jobs'
|
||||||
|
|
||||||
|
const emitter = new EventEmitter()
|
||||||
|
|
||||||
|
export const OnJobAdded = {
|
||||||
|
emit(job: Job) {
|
||||||
|
emitter.emit('job:add', job)
|
||||||
|
},
|
||||||
|
addListener(cb: (job: Job) => void) {
|
||||||
|
emitter.on('job:add', cb)
|
||||||
|
},
|
||||||
|
removeListener(cb: (job: Job) => void) {
|
||||||
|
emitter.off('job:add', cb)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OnJobCompleted = {
|
||||||
|
emit(job: Job) {
|
||||||
|
emitter.emit('job:completed', job)
|
||||||
|
},
|
||||||
|
addListener(cb: (job: Job) => void) {
|
||||||
|
emitter.on('job:completed', cb)
|
||||||
|
},
|
||||||
|
removeListener(cb: (job: Job) => void) {
|
||||||
|
emitter.off('job:completed', cb)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OnJobStarted = {
|
||||||
|
emit(job: Job) {
|
||||||
|
emitter.emit('job:started', job)
|
||||||
|
},
|
||||||
|
addListener(cb: (job: Job) => void) {
|
||||||
|
emitter.on('job:started', cb)
|
||||||
|
},
|
||||||
|
removeListener(cb: (job: Job) => void) {
|
||||||
|
emitter.off('job:started', cb)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OnJobLog = {
|
||||||
|
emit(uuid: string, line: string) {
|
||||||
|
emitter.emit(`job:log:${uuid}`, line)
|
||||||
|
},
|
||||||
|
addListener(uuid: string, cb: (line: string) => void) {
|
||||||
|
emitter.on(`job:log:${uuid}`, cb)
|
||||||
|
},
|
||||||
|
removeListener(uuid: string, cb: (line: string) => void) {
|
||||||
|
emitter.off(`job:log:${uuid}`, cb)
|
||||||
|
},
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import { OnJobLog } from '@/events'
|
||||||
|
import { getJobLogs, getLatestJobByName } from '@/jobs'
|
||||||
|
import { JsonResponse, JsonStreamResponse, createUrlQuery } from '@/lib/utils'
|
||||||
|
import { debug } from '@/logger'
|
||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ params: { name }, url, redirect }) => {
|
||||||
|
const latestJob = await getLatestJobByName(name!)
|
||||||
|
if (!latestJob) {
|
||||||
|
return new Response('No jobs yet for this deploy', { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(latestJob)
|
||||||
|
|
||||||
|
const rawLogs = await getJobLogs(latestJob.uuid)
|
||||||
|
|
||||||
|
const format = url.searchParams.get('format')
|
||||||
|
switch (format) {
|
||||||
|
case 'raw':
|
||||||
|
return new Response(rawLogs)
|
||||||
|
case 'json':
|
||||||
|
return new JsonResponse(rawLogs.trim().split('\n'))
|
||||||
|
case 'sse':
|
||||||
|
return new JsonStreamResponse(sendData => {
|
||||||
|
const jobLog = (content: string) => sendData({ type: 'log', content })
|
||||||
|
|
||||||
|
debug('[SSE] Registering job log client')
|
||||||
|
OnJobLog.addListener(latestJob.uuid, jobLog)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// cancel
|
||||||
|
OnJobLog.removeListener(latestJob.uuid, jobLog)
|
||||||
|
debug('[SSE] Un-registered job log client')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return redirect(createUrlQuery('/error', { message: `Invalid format "${format}"` }))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { getLatestJobByName } from '@/jobs'
|
||||||
|
import { JsonResponse, JsonStreamResponse, createUrlQuery } from '@/lib/utils'
|
||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ params: { name }, url, redirect }) => {
|
||||||
|
const latestJob = await getLatestJobByName(name!)
|
||||||
|
|
||||||
|
const format = url.searchParams.get('format')
|
||||||
|
switch (format) {
|
||||||
|
case 'json':
|
||||||
|
return new JsonResponse(latestJob)
|
||||||
|
// case 'sse':
|
||||||
|
// return new JsonStreamResponse(sendData => {
|
||||||
|
// const jobLog = (content: string) => sendData({ type: 'log', content })
|
||||||
|
|
||||||
|
// debug('[SSE] Registering job log client')
|
||||||
|
// OnJobLog.addListener(uuid!, jobLog)
|
||||||
|
|
||||||
|
// return () => {
|
||||||
|
// // cancel
|
||||||
|
// OnJobLog.removeListener(uuid!, jobLog)
|
||||||
|
// debug('[SSE] Un-registered job log client')
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
default:
|
||||||
|
return redirect(
|
||||||
|
createUrlQuery('/error', {
|
||||||
|
message: `Invalid format "${format}"`,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
import { loadConfig } from '@/config'
|
||||||
|
import { createDeployJob } from '@/deploys'
|
||||||
|
import { enqueueJob } from '@/jobs'
|
||||||
|
import type { APIRoute } from 'astro'
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ params: { name } }) => {
|
||||||
|
return new Response('ok')
|
||||||
|
}
|
Loading…
Reference in New Issue