feat: dynamic latest logs in deploy page
This commit is contained in:
@@ -16,11 +16,13 @@
|
|||||||
"@fontsource-variable/material-symbols-outlined": "^5.0.24",
|
"@fontsource-variable/material-symbols-outlined": "^5.0.24",
|
||||||
"@fontsource/jetbrains-mono": "^5.0.19",
|
"@fontsource/jetbrains-mono": "^5.0.19",
|
||||||
"@fontsource/lato": "^5.0.19",
|
"@fontsource/lato": "^5.0.19",
|
||||||
|
"@nanostores/preact": "^0.5.1",
|
||||||
"astro": "^4.4.11",
|
"astro": "^4.4.11",
|
||||||
"async-mutex": "^0.4.1",
|
"async-mutex": "^0.4.1",
|
||||||
"dockerode": "^4.0.2",
|
"dockerode": "^4.0.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"nanostores": "^0.10.3",
|
||||||
"preact": "^10.19.6",
|
"preact": "^10.19.6",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
|
|||||||
Generated
+22
@@ -23,6 +23,9 @@ dependencies:
|
|||||||
'@fontsource/lato':
|
'@fontsource/lato':
|
||||||
specifier: ^5.0.19
|
specifier: ^5.0.19
|
||||||
version: 5.0.19
|
version: 5.0.19
|
||||||
|
'@nanostores/preact':
|
||||||
|
specifier: ^0.5.1
|
||||||
|
version: 0.5.1(nanostores@0.10.3)(preact@10.19.6)
|
||||||
astro:
|
astro:
|
||||||
specifier: ^4.4.11
|
specifier: ^4.4.11
|
||||||
version: 4.4.11(sass@1.71.1)(typescript@5.3.3)
|
version: 4.4.11(sass@1.71.1)(typescript@5.3.3)
|
||||||
@@ -38,6 +41,9 @@ dependencies:
|
|||||||
lodash:
|
lodash:
|
||||||
specifier: ^4.17.21
|
specifier: ^4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
nanostores:
|
||||||
|
specifier: ^0.10.3
|
||||||
|
version: 0.10.3
|
||||||
preact:
|
preact:
|
||||||
specifier: ^10.19.6
|
specifier: ^10.19.6
|
||||||
version: 10.19.6
|
version: 10.19.6
|
||||||
@@ -722,6 +728,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-JmU7JIBwyL8RAzefvzALT4sP2M0biGk8i2invAgpQmma/QgfsaqoHIvJ7S0YC8n9hUVG8X3Leul2nGa06PvhbQ==}
|
resolution: {integrity: sha512-JmU7JIBwyL8RAzefvzALT4sP2M0biGk8i2invAgpQmma/QgfsaqoHIvJ7S0YC8n9hUVG8X3Leul2nGa06PvhbQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@nanostores/preact@0.5.1(nanostores@0.10.3)(preact@10.19.6):
|
||||||
|
resolution: {integrity: sha512-kofyeDwzM3TrOd37ay+Xxgk3Cn6jih23dxELc7Mr9IJV55jmWATfNP9b7O/awwCL7CE5z5PfzFnNk/W+tMaWGw==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
nanostores: ^0.9.0 || ^0.10.0
|
||||||
|
preact: '>=10.0.0'
|
||||||
|
dependencies:
|
||||||
|
nanostores: 0.10.3
|
||||||
|
preact: 10.19.6
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@nodelib/fs.scandir@2.1.5:
|
/@nodelib/fs.scandir@2.1.5:
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -2989,6 +3006,11 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/nanostores@0.10.3:
|
||||||
|
resolution: {integrity: sha512-Nii8O1XqmawqSCf9o2aWqVxhKRN01+iue9/VEd1TiJCr9VT5XxgPFbF1Edl1XN6pwJcZRsl8Ki+z01yb/T/C2g==}
|
||||||
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/napi-build-utils@1.0.2:
|
/napi-build-utils@1.0.2:
|
||||||
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
|
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
|
|||||||
+10
-5
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
|
import { triggerCounter } from './store.js'
|
||||||
|
|
||||||
// target is a url to send a POST request to
|
// target is a url to send a POST request to
|
||||||
export const TriggerButton = ({ target, children }) => {
|
export const TriggerButton = ({ target, children }) => {
|
||||||
@@ -6,6 +7,7 @@ export const TriggerButton = ({ target, children }) => {
|
|||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
triggerCounter.set(triggerCounter.get() + 1)
|
||||||
|
|
||||||
const response = await fetch(target, {
|
const response = await fetch(target, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -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)
|
||||||
|
},
|
||||||
|
}
|
||||||
+12
-43
@@ -1,5 +1,3 @@
|
|||||||
import { EventEmitter } from 'events'
|
|
||||||
|
|
||||||
import { debug } from './logger'
|
import { debug } from './logger'
|
||||||
|
|
||||||
import { createJsonDatabase } from './lib/file-db'
|
import { createJsonDatabase } from './lib/file-db'
|
||||||
@@ -11,7 +9,9 @@ import { createWriteStream } from 'fs'
|
|||||||
import { readFile, mkdir } from 'fs/promises'
|
import { readFile, mkdir } from 'fs/promises'
|
||||||
|
|
||||||
import { dirname } from 'path'
|
import { dirname } from 'path'
|
||||||
import type { random } from 'lodash'
|
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { OnJobAdded, OnJobCompleted, OnJobLog, OnJobStarted } from './events'
|
||||||
|
|
||||||
type JobStatus = 'queued' | 'running' | 'completed'
|
type JobStatus = 'queued' | 'running' | 'completed'
|
||||||
|
|
||||||
@@ -38,8 +38,6 @@ type QueuedJob = {
|
|||||||
completed?: () => void
|
completed?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const emitter = new EventEmitter()
|
|
||||||
|
|
||||||
const jobsDB = createJsonDatabase<Job[]>(`${import.meta.env.DATA_PATH}/jobs.json`, [])
|
const jobsDB = createJsonDatabase<Job[]>(`${import.meta.env.DATA_PATH}/jobs.json`, [])
|
||||||
|
|
||||||
export function getJobLogFile(job: Job): string {
|
export function getJobLogFile(job: Job): string {
|
||||||
@@ -69,7 +67,7 @@ async function processQueue() {
|
|||||||
return job
|
return job
|
||||||
})
|
})
|
||||||
|
|
||||||
emitter.emit('job:started', job)
|
OnJobStarted.emit(job)
|
||||||
|
|
||||||
const startedAt = new Date().toISOString()
|
const startedAt = new Date().toISOString()
|
||||||
let error: string | undefined
|
let error: string | undefined
|
||||||
@@ -83,7 +81,7 @@ async function processQueue() {
|
|||||||
logsFile.write(line + '\n')
|
logsFile.write(line + '\n')
|
||||||
|
|
||||||
// to not block log file creation
|
// to not block log file creation
|
||||||
setImmediate(() => emitter.emit(`job:log:${uuid}`, line))
|
setImmediate(() => OnJobLog.emit(uuid, line))
|
||||||
})
|
})
|
||||||
|
|
||||||
debug(`[Jobs] Starting job "${job.name}"`)
|
debug(`[Jobs] Starting job "${job.name}"`)
|
||||||
@@ -117,7 +115,7 @@ async function processQueue() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
completed?.()
|
completed?.()
|
||||||
emitter.emit('job:completed', completedJob)
|
OnJobCompleted.emit(completedJob)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
working = false
|
working = false
|
||||||
@@ -147,7 +145,7 @@ export async function enqueueJob(
|
|||||||
})
|
})
|
||||||
|
|
||||||
queue.push({ uuid, deployFn, completed })
|
queue.push({ uuid, deployFn, completed })
|
||||||
emitter.emit('job:add', job)
|
OnJobAdded.emit(job)
|
||||||
|
|
||||||
// starts concurrently a function to process jobs
|
// starts concurrently a function to process jobs
|
||||||
processQueue()
|
processQueue()
|
||||||
@@ -174,40 +172,11 @@ export async function getJobLogs(uuid: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OnJobAdded = {
|
export async function getLatestJobByName(name: string): Promise<Job | undefined> {
|
||||||
addListener(cb: (job: Job) => void) {
|
const jobs = await jobsDB.load()
|
||||||
emitter.on('job:add', cb)
|
const jobsByName = jobs.filter(job => job.name === name)
|
||||||
},
|
const latestJobs = _.sortBy(jobsByName, job => new Date(job.submittedAt))
|
||||||
removeListener(cb: (job: Job) => void) {
|
return latestJobs.length > 0 ? latestJobs.at(-1) : undefined
|
||||||
emitter.off('job:add', cb)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OnJobCompleted = {
|
|
||||||
addListener(cb: (job: Job) => void) {
|
|
||||||
emitter.on('job:completed', cb)
|
|
||||||
},
|
|
||||||
removeListener(cb: (job: Job) => void) {
|
|
||||||
emitter.off('job:completed', cb)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OnJobStarted = {
|
|
||||||
addListener(cb: (job: Job) => void) {
|
|
||||||
emitter.on('job:started', cb)
|
|
||||||
},
|
|
||||||
removeListener(cb: (job: Job) => void) {
|
|
||||||
emitter.off('job:started', cb)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OnJobLog = {
|
|
||||||
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)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================[ Old Version ]===================
|
// ===================[ Old Version ]===================
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
import { Deploy } from '@client/Deploy'
|
import { Deploy } from '@client/Deploy'
|
||||||
import { Value } from '@client/Inspect'
|
import { Value } from '@client/Inspect'
|
||||||
|
import { JobLogs } from '@client/JobLogs'
|
||||||
import { TriggerButton } from '@client/TriggerButton'
|
import { TriggerButton } from '@client/TriggerButton'
|
||||||
|
|
||||||
import { loadConfig } from '@/config'
|
import { loadConfig } from '@/config'
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import { createUrlQuery } from '@/lib/utils'
|
import { createUrlQuery } from '@/lib/utils'
|
||||||
|
import DockerDeployInfo from '@/components/DockerDeployInfo.astro'
|
||||||
import Docker from 'dockerode'
|
|
||||||
|
|
||||||
const { name } = Astro.params
|
const { name } = Astro.params
|
||||||
|
|
||||||
@@ -22,10 +22,6 @@ if (!deploy) {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const docker = new Docker({ socketPath: '/var/run/docker.sock' })
|
|
||||||
const containers = await docker.listContainers()
|
|
||||||
const container = containers.find(c => c.Names.includes(`/${name}`))
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={`${name} | Deploy | phCD`}>
|
<Layout title={`${name} | Deploy | phCD`}>
|
||||||
@@ -34,10 +30,10 @@ const container = containers.find(c => c.Names.includes(`/${name}`))
|
|||||||
<div class="title">{name}</div>
|
<div class="title">{name}</div>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<TriggerButton client:load target={Astro.url.href + '/force-deploy'}>
|
<TriggerButton client:load target={Astro.url.pathname + '/force-deploy'}>
|
||||||
<span class="material-symbols-outlined">replay</span> Force re-deploy
|
<span class="material-symbols-outlined">replay</span> Force re-deploy
|
||||||
</TriggerButton>
|
</TriggerButton>
|
||||||
<TriggerButton client:load target={Astro.url.href + '/stop'}>
|
<TriggerButton client:load target={Astro.url.pathname + '/stop'}>
|
||||||
<span class="material-symbols-outlined">stop</span> Stop
|
<span class="material-symbols-outlined">stop</span> Stop
|
||||||
</TriggerButton>
|
</TriggerButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,21 +42,15 @@ const container = containers.find(c => c.Names.includes(`/${name}`))
|
|||||||
|
|
||||||
<Value value={deploy} borderless />
|
<Value value={deploy} borderless />
|
||||||
|
|
||||||
<h2>Containers</h2>
|
{deploy.type === 'docker-image' && <DockerDeployInfo name={name} />}
|
||||||
|
|
||||||
{
|
|
||||||
container ? (
|
|
||||||
<div class="card">
|
|
||||||
<div class="container">
|
|
||||||
<Value value={container} borderless />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p>No container found</p>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
<h2>Jobs</h2>
|
<h2>Jobs</h2>
|
||||||
|
|
||||||
|
<h3>Latest Job</h3>
|
||||||
|
|
||||||
|
<JobLogs client:load endpoint={`${Astro.url.pathname}/latest-job-logs`} />
|
||||||
|
|
||||||
|
<h3>Archive</h3>
|
||||||
|
|
||||||
...
|
...
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -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')
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
---
|
---
|
||||||
import { getJob, getJobLogs, OnJobLog } from '@/jobs'
|
import { getJob, getJobLogs } from '@/jobs'
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import { JobLogs } from '@client/JobLogs'
|
import { JobLogs } from '@client/JobLogs'
|
||||||
|
import { createUrlQuery } from '@/lib/utils'
|
||||||
import { JsonResponse, createUrlQuery } from '@/lib/utils'
|
|
||||||
|
|
||||||
const { uuid } = Astro.params
|
const { uuid } = Astro.params
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ if (job.status === 'completed') {
|
|||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
<pre><code>{logsContent}</code></pre>
|
<pre><code>{logsContent}</code></pre>
|
||||||
) : (
|
) : (
|
||||||
<JobLogs client:load />
|
<JobLogs client:load endpoint={`${Astro.url.pathname}/logs`} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OnJobLog, getJob, getJobLogs } from '@/jobs'
|
import { OnJobLog } from '@/events'
|
||||||
|
import { getJob, getJobLogs } from '@/jobs'
|
||||||
import { JsonResponse, JsonStreamResponse, createUrlQuery } from '@/lib/utils'
|
import { JsonResponse, JsonStreamResponse, createUrlQuery } from '@/lib/utils'
|
||||||
import { debug } from '@/logger'
|
import { debug } from '@/logger'
|
||||||
import type { APIRoute } from 'astro'
|
import type { APIRoute } from 'astro'
|
||||||
|
|||||||
@@ -466,6 +466,8 @@ body {
|
|||||||
place-content: center;
|
place-content: center;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
|
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
.material-symbols-outlined {
|
.material-symbols-outlined {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user