feat: better deploys page and single deploy page
This commit is contained in:
+22
-6
@@ -1,3 +1,4 @@
|
||||
import { normalizeURL } from '@/lib/utils'
|
||||
import { Value } from './Inspect.jsx'
|
||||
|
||||
// const GitRef = ({ type, value }) => {
|
||||
@@ -17,15 +18,30 @@ import { Value } from './Inspect.jsx'
|
||||
// <GitRef {...ref} />
|
||||
// </>
|
||||
|
||||
/**
|
||||
* @param {{ deploy: import('@/config.js').Deploy }} param0
|
||||
*/
|
||||
|
||||
export const Deploy = ({ deploy }) => {
|
||||
return (
|
||||
<div class="deploy">
|
||||
<div class="name">{deploy.name}</div>
|
||||
<div class="type">{deploy.type}</div>
|
||||
<div class="type">{deploy.type}</div>
|
||||
<div class="type">{deploy.type}</div>
|
||||
|
||||
<Value value={deploy} borderless />
|
||||
<div class="title">
|
||||
<a href={`/deploys/${deploy.name}`}>{deploy.name}</a>
|
||||
</div>
|
||||
<div class="shield">{deploy.type}</div>
|
||||
{deploy.type === 'docker' ? (
|
||||
<div class="description">{deploy.options.image}</div>
|
||||
) : (
|
||||
<>
|
||||
<div class="description">
|
||||
{deploy.url.startsWith('/') ? (
|
||||
deploy.url
|
||||
) : (
|
||||
<a href={normalizeURL(deploy.url)}>{deploy.url}</a>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+17
-3
@@ -1,8 +1,20 @@
|
||||
import _ from 'lodash'
|
||||
import { clsx } from '@/lib/utils'
|
||||
|
||||
export const Value = ({ value, borderless }) => {
|
||||
return Array.isArray(value) ? (
|
||||
export const Value = ({ value, borderless } = {}) => {
|
||||
return value === undefined ? (
|
||||
<em>undefined</em>
|
||||
) : value === null ? (
|
||||
<em>null</em>
|
||||
) : value === true ? (
|
||||
<em>true</em>
|
||||
) : value === false ? (
|
||||
<em>false</em>
|
||||
) : typeof value === 'string' && value.trim().length === 0 ? (
|
||||
<em>Empty string</em>
|
||||
) : !value ? (
|
||||
value
|
||||
) : Array.isArray(value) ? (
|
||||
<ValueArray value={value} borderless={borderless} />
|
||||
) : typeof value === 'object' ? (
|
||||
<ValueObject value={value} borderless={borderless} />
|
||||
@@ -12,7 +24,9 @@ export const Value = ({ value, borderless }) => {
|
||||
}
|
||||
|
||||
export const ValueArray = ({ value, borderless }) => {
|
||||
return (
|
||||
return value.length === 0 ? (
|
||||
<em>Empty array</em>
|
||||
) : (
|
||||
<div class={clsx('compound-value array', borderless && 'borderless')}>
|
||||
{value.map(item => (
|
||||
<div class="item">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "preact/hooks"
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
|
||||
export const JobLogs = ({}) => {
|
||||
const [logLines, setLogLines] = useState([])
|
||||
@@ -13,13 +13,11 @@ export const JobLogs = ({}) => {
|
||||
const es = new EventSource(location.href + '/logs?format=sse')
|
||||
es.addEventListener('message', ({ data }) => {
|
||||
const event = JSON.parse(data)
|
||||
setLogLines(lines => [
|
||||
...lines,
|
||||
event.content,
|
||||
])
|
||||
setLogLines(lines => [...lines, event.content])
|
||||
})
|
||||
}, [])
|
||||
|
||||
// prettier-ignore
|
||||
return (
|
||||
<pre><code>{logLines.join('\n')}</code></pre>
|
||||
)
|
||||
|
||||
@@ -42,12 +42,14 @@ export const JobsPage = ({}) => {
|
||||
const res = await fetch(location.href + '?format=json')
|
||||
const jobs = await res.json()
|
||||
|
||||
const result = {}
|
||||
// const result = {}
|
||||
|
||||
for (const item of jobs) {
|
||||
result[item.uuid] = item
|
||||
}
|
||||
// for (const item of jobs) {
|
||||
// result[item.uuid] = item
|
||||
// }
|
||||
|
||||
const result = _.keyBy(jobs, 'uuid')
|
||||
console.log(result)
|
||||
setJobStore(result)
|
||||
|
||||
// Setup SSE
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<div class="sidebar">
|
||||
<div class="header">phCD</div>
|
||||
<!-- <div class="header">phCD</div> -->
|
||||
<button class="menu-button flat">
|
||||
<span class="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
<nav>
|
||||
<div class="nav-item group">
|
||||
<a class="label" href="/"> <span class="material-symbols-outlined">home</span> Home</a>
|
||||
</div>
|
||||
<div class="nav-item group">
|
||||
<a class="label" href="/containers"
|
||||
><span class="material-symbols-outlined">view_list</span> Containers</a
|
||||
>
|
||||
<a class="label" href="/containers">
|
||||
<span class="material-symbols-outlined">view_list</span> Containers
|
||||
</a>
|
||||
<!-- <div class="children">
|
||||
<div class="nav-item"><div class="label">Container 1</div></div>
|
||||
<div class="nav-item"><div class="label">Container 2</div></div>
|
||||
@@ -15,9 +18,9 @@
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="nav-item group">
|
||||
<a class="label" href="/deploys"
|
||||
><span class="material-symbols-outlined">deployed_code</span> Deploys</a
|
||||
>
|
||||
<a class="label" href="/deploys">
|
||||
<span class="material-symbols-outlined">deployed_code</span> Deploys
|
||||
</a>
|
||||
<!-- <div class="children">
|
||||
<div class="nav-item"><div class="label">Deploy 1</div></div>
|
||||
<div class="nav-item"><div class="label">Deploy 2</div></div>
|
||||
@@ -25,9 +28,9 @@
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="nav-item group">
|
||||
<a class="label" href="/jobs"
|
||||
><span class="material-symbols-outlined">terminal</span> Jobs</a
|
||||
>
|
||||
<a class="label" href="/jobs">
|
||||
<span class="material-symbols-outlined">terminal</span> Jobs
|
||||
</a>
|
||||
<!-- <div class="children">
|
||||
<div class="nav-item"><div class="label">Deploy 1</div></div>
|
||||
<div class="nav-item"><div class="label">Deploy 2</div></div>
|
||||
|
||||
@@ -23,6 +23,7 @@ const { title } = Astro.props
|
||||
<body>
|
||||
<Sidebar />
|
||||
<div class="scroll-container">
|
||||
<div class="logo">phCD</div>
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ export class JsonStreamResponse extends Response {
|
||||
}
|
||||
}
|
||||
|
||||
export function createQueryUrl(base: string, options: Record<string, string>) {
|
||||
export function createUrlQuery(base: string, options: Record<string, string>) {
|
||||
return `${base}?${Object.entries(options)
|
||||
.map(([k, v]) => k + '=' + encodeURIComponent(v))
|
||||
.join('&')}`
|
||||
|
||||
+22
-22
@@ -1,34 +1,34 @@
|
||||
---
|
||||
import { Value } from '@client/Inspect'
|
||||
import Layout from '../layouts/Layout.astro'
|
||||
import Docker from 'dockerode'
|
||||
|
||||
const docker = new Docker({ socketPath: '/var/run/docker.sock' })
|
||||
|
||||
const containers = await docker.listContainers({ all: true })
|
||||
|
||||
console.dir(containers, { depth: null })
|
||||
---
|
||||
|
||||
<Layout title="Containers | phCD">
|
||||
<h1>Containers</h1>
|
||||
<ul>
|
||||
{
|
||||
containers.map(container => (
|
||||
<li>
|
||||
<strong>Name:</strong> {container.Names[0]} <br />
|
||||
<strong>Command:</strong> <code>{container.Command}</code> <br />
|
||||
<strong>Image:</strong> <code>{container.Image}</code> <br />
|
||||
<strong>Status:</strong> {container.Status} <br />
|
||||
<strong>Mounts:</strong>
|
||||
<ul>
|
||||
{container.Mounts.map(mount => (
|
||||
<li>
|
||||
<strong>Type:</strong> {mount.Type} <br />
|
||||
<strong>From:</strong> <code>{mount.Source}</code> <br />
|
||||
<strong>To:</strong> <code>{mount.Destination}</code> <br />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
{containers.map(container => <Value client:load value={container} borderless={false} />)}
|
||||
<!-- <ul>
|
||||
<li>
|
||||
<strong>Name:</strong> {container.Names[0]} <br />
|
||||
<strong>Command:</strong> <code>{container.Command}</code> <br />
|
||||
<strong>Image:</strong> <code>{container.Image}</code> <br />
|
||||
<strong>Status:</strong> {container.Status} <br />
|
||||
<strong>Mounts:</strong>
|
||||
<ul>
|
||||
{container.Mounts.map(mount => (
|
||||
<li>
|
||||
<strong>Type:</strong> {mount.Type} <br />
|
||||
<strong>From:</strong> <code>{mount.Source}</code> <br />
|
||||
<strong>To:</strong> <code>{mount.Destination}</code> <br />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
</ul> -->
|
||||
</Layout>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
import { Deploy } from '@client/Deploy'
|
||||
import { Value } from '@client/Inspect'
|
||||
|
||||
import { loadConfig } from '@/config'
|
||||
import Layout from '@layouts/Layout.astro'
|
||||
import { createUrlQuery } from '@/lib/utils'
|
||||
|
||||
const { name } = Astro.params
|
||||
|
||||
const { deploys } = await loadConfig()
|
||||
const deploy = deploys.find(d => d.name === name)
|
||||
|
||||
if (!deploy) {
|
||||
return Astro.redirect(
|
||||
createUrlQuery('/error', {
|
||||
message: `No deploy with name "${name}"`,
|
||||
previous: `/deploys`,
|
||||
})
|
||||
)
|
||||
}
|
||||
---
|
||||
|
||||
<Layout title={`${name} | Deploy | phCD`}>
|
||||
<h1>
|
||||
<div class="kind">Deploy</div>
|
||||
<div class="title">{name}</div>
|
||||
</h1>
|
||||
|
||||
<Value value={deploy} borderless />
|
||||
</Layout>
|
||||
@@ -8,7 +8,9 @@ const { deploys } = await loadConfig()
|
||||
|
||||
<Layout title="Deploys | phCD">
|
||||
<h1>Deploys</h1>
|
||||
<a class="button" href="/deploys/new">New Deploy</a>
|
||||
<a class="button" href="/deploys/new">
|
||||
<span class="material-symbols-outlined">add</span> New Deploy
|
||||
</a>
|
||||
<div class="deploys">
|
||||
{deploys.toReversed().map(deploy => <Deploy deploy={deploy} />)}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@ import Layout from '../layouts/Layout.astro'
|
||||
---
|
||||
|
||||
<Layout title="Homepage">
|
||||
<h1>phCD</h1>
|
||||
<h2>Recent Deploys</h2>
|
||||
<p>Bla bla recent jobs</p>
|
||||
<h2>Containers</h2>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getJob, getJobLogs, OnJobLog } from '@/jobs'
|
||||
import Layout from '@layouts/Layout.astro'
|
||||
import { JobLogs } from '@client/JobLogs'
|
||||
|
||||
import { JsonResponse, createQueryUrl } from '@/lib/utils'
|
||||
import { JsonResponse, createUrlQuery } from '@/lib/utils'
|
||||
|
||||
const { uuid } = Astro.params
|
||||
|
||||
@@ -11,10 +11,10 @@ const job = await getJob(uuid!)
|
||||
|
||||
if (!job) {
|
||||
return Astro.redirect(
|
||||
createQueryUrl('/error', {
|
||||
createUrlQuery('/error', {
|
||||
message: `No job with uuid "${uuid}"`,
|
||||
previous: `/jobs`,
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,11 +25,16 @@ if (job.status === 'completed') {
|
||||
}
|
||||
---
|
||||
|
||||
<Layout title="{`${job.name}" | Jobs | phCD`}>
|
||||
<Layout title={`"${job.name}" | Jobs | phCD`}>
|
||||
<h1>Job "{job.name}"</h1>
|
||||
<pre><code>{JSON.stringify(job, null, 2)}</code></pre>
|
||||
<h2>Logs</h2>
|
||||
{job.status === 'completed' ?
|
||||
<pre><code>{logsContent}</code></pre>
|
||||
: <JobLogs client:load />}
|
||||
{
|
||||
job.status === 'completed' ? (
|
||||
// prettier-ignore
|
||||
<pre><code>{logsContent}</code></pre>
|
||||
) : (
|
||||
<JobLogs client:load />
|
||||
)
|
||||
}
|
||||
</Layout>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { OnJobLog, getJob, getJobLogs } from '@/jobs'
|
||||
import { JsonResponse, JsonStreamResponse, createQueryUrl } from '@/lib/utils'
|
||||
import { JsonResponse, JsonStreamResponse, createUrlQuery } from '@/lib/utils'
|
||||
import { debug } from '@/logger'
|
||||
import type { APIRoute } from 'astro'
|
||||
|
||||
@@ -27,7 +27,7 @@ export const GET: APIRoute = async ({ params: { uuid }, url, redirect }) => {
|
||||
})
|
||||
default:
|
||||
return redirect(
|
||||
createQueryUrl('/error', {
|
||||
createUrlQuery('/error', {
|
||||
message: `Invalid format "${format}"`,
|
||||
})
|
||||
)
|
||||
|
||||
+130
-11
@@ -101,8 +101,8 @@ button,
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
|
||||
color: #333;
|
||||
background-color: #00000018;
|
||||
color: darken($accent-400, 80%);
|
||||
background-color: darken($accent-400, 5%);
|
||||
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
@@ -113,8 +113,27 @@ button,
|
||||
|
||||
// border: 1px solid #e0e0e0;
|
||||
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
grid-auto-flow: column;
|
||||
gap: 0.25rem;
|
||||
|
||||
& > .material-symbols-outlined {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #00000030;
|
||||
background-color: darken($accent-400, 10%);
|
||||
}
|
||||
|
||||
&.flat {
|
||||
background-color: transparent;
|
||||
color: #333;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background-color: #00000010;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,11 +317,70 @@ form {
|
||||
width: 100%;
|
||||
|
||||
background: $accent-400;
|
||||
padding: 1rem;
|
||||
padding: 1rem 1rem 2rem;
|
||||
border-radius: 1rem;
|
||||
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
|
||||
align-items: center;
|
||||
|
||||
grid-template-areas:
|
||||
'title shield'
|
||||
'description .';
|
||||
|
||||
gap: 0.5rem;
|
||||
|
||||
& > .title {
|
||||
grid-area: title;
|
||||
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
color: #444;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .shield {
|
||||
grid-area: shield;
|
||||
|
||||
background: darken($accent-400, 4%);
|
||||
|
||||
font-size: 16px;
|
||||
|
||||
display: grid;
|
||||
place-content: center;
|
||||
padding: 0.125rem 0.25rem;
|
||||
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
& > .description {
|
||||
grid-area: description;
|
||||
|
||||
font-family: monospace;
|
||||
font-size: 15px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// border: 1px solid #ddd;
|
||||
// padding: 1rem;
|
||||
// border-radius: 1rem;
|
||||
@@ -328,16 +406,19 @@ body {
|
||||
grid-area: sidebar;
|
||||
|
||||
min-width: 300px;
|
||||
width: 20vw;
|
||||
width: 15vw;
|
||||
|
||||
background: $accent-400;
|
||||
color: #333;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
|
||||
z-index: 1;
|
||||
|
||||
padding: 1rem;
|
||||
|
||||
// box-shadow: 0 0 1rem 0 #0002, 0 0 0.25rem 0 #0002;
|
||||
|
||||
.header {
|
||||
@@ -346,6 +427,16 @@ body {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
aspect-ratio: 1;
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -353,7 +444,7 @@ body {
|
||||
|
||||
gap: 0.5rem;
|
||||
|
||||
padding: 0 1rem;
|
||||
padding-top: 1rem;
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
@@ -364,14 +455,14 @@ body {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 0.5rem;
|
||||
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease-in-out;
|
||||
transition: background-color 150ms ease-in-out;
|
||||
|
||||
font-size: 16px;
|
||||
|
||||
@@ -405,7 +496,7 @@ body {
|
||||
&.group {
|
||||
& > .label {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,8 +509,23 @@ body {
|
||||
justify-items: center;
|
||||
overflow-y: auto;
|
||||
|
||||
position: relative;
|
||||
|
||||
& > .logo {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
overflow-x: auto;
|
||||
|
||||
padding: 5rem 1rem 2rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -436,6 +542,19 @@ body {
|
||||
// Components
|
||||
//
|
||||
|
||||
& > h1 {
|
||||
& > .title {
|
||||
}
|
||||
& > .kind {
|
||||
padding-bottom: 0.25rem;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
|
||||
Reference in New Issue
Block a user