feat: better deploys page and single deploy page

main
Antonio De Lucreziis 2 months ago
parent 68283207bb
commit 6ee977d8c6

@ -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>
)
}

@ -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>

@ -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('&')}`

@ -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}"`,
})
)

@ -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/*"],

Loading…
Cancel
Save