322 lines
No EOL
6.2 KiB
HTML
322 lines
No EOL
6.2 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Container {{container_name}}{% endblock %}
|
|
|
|
{% block content %}
|
|
<main>
|
|
<header>
|
|
<h1>Container <span class="container-name">{{container_name}}</span> {% match info.stack() %}{% when
|
|
Some
|
|
with (stack) %}(<a class="stack-name" href="/stack/{{stack}}/">{{stack}}</a>)
|
|
{% when None %}{% endmatch %}</h1>
|
|
<div class="actions">
|
|
<form action="./start" method="POST"><button type="submit">Start</button></form>
|
|
<form action="./restart" method="POST"><button type="submit">Restart</button></form>
|
|
<form action="./stop" method="POST"><button type="submit">Stop</button></form>
|
|
<form action="./kill" method="POST"><button type="submit">Kill</button></form>
|
|
<form action="./remove" method="POST"><button type="submit">Delete</button></form>
|
|
</div>
|
|
</header>
|
|
<section class="status-section">
|
|
<h2>Status</h2>
|
|
<article>
|
|
<dl class="table">
|
|
<dt>ID</dt>
|
|
<dd>{{ info.id }}</dd>
|
|
<dt>Status</dt>
|
|
<dd data-state="{{ info.state }}">{{ info.state }}</dd>
|
|
<dt>Image</dt>
|
|
<dd>{{ info.image }}</dd>
|
|
<dt>Image ID</dt>
|
|
<dd>{{ info.image_id }}</dd>
|
|
<dt>Created at</dt>
|
|
<dd>{{ info.created_at }}</dd>
|
|
</dl>
|
|
<dl class="list">
|
|
{% match info.volumes %}
|
|
{% when Some with (volumes) %}
|
|
<dt>Volumes</dt>
|
|
{% for volume in volumes %}
|
|
<dd>
|
|
{{ volume.source.clone().unwrap_or_default() }} → {{
|
|
volume.destination.clone().unwrap_or_default() }}
|
|
</dd>
|
|
{% endfor %}
|
|
{% when None %}
|
|
{% endmatch %}
|
|
{% match info.env %}
|
|
{% when Some with (env) %}
|
|
<dt>Environment</dt>
|
|
{% for var in env %}
|
|
<dd>{{ var }}</dd>
|
|
{% endfor %}
|
|
{% when None %}
|
|
{% endmatch %}
|
|
{% match info.labels %}
|
|
{% when Some with (labels) %}
|
|
<dt>Labels</dt>
|
|
{% for label in labels %}
|
|
<dd class="label"><span class="key">{{ label.0 }}</span> = <span class="value">{{
|
|
label.1
|
|
}}</span></dd>
|
|
{% endfor %}
|
|
{% when None %}
|
|
{% endmatch %}
|
|
</dl>
|
|
</article>
|
|
</section>
|
|
<section class="log-section">
|
|
<h2>Logs</h2>
|
|
<code id="log"></code>
|
|
</section>
|
|
</main>
|
|
|
|
<style scoped>
|
|
a.stack-name {
|
|
text-decoration: none;
|
|
}
|
|
|
|
main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
h2 {
|
|
text-transform: uppercase;
|
|
margin: 0;
|
|
padding: 0;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
pre {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.container-name {
|
|
color: #E796F3;
|
|
}
|
|
|
|
.label {
|
|
|
|
& .key {
|
|
color: #E796F3;
|
|
}
|
|
}
|
|
|
|
#log {
|
|
background-color: var(--bg-raised);
|
|
display: flex;
|
|
|
|
display: block;
|
|
max-height: 80vh;
|
|
overflow-y: auto;
|
|
|
|
& .error {
|
|
padding: 4px 8px;
|
|
background-color: #3B1219;
|
|
color: #FF9592;
|
|
}
|
|
|
|
& p {
|
|
padding: 4px 8px;
|
|
margin: 0;
|
|
background-color: #1a1c38;
|
|
|
|
&:nth-child(odd) {
|
|
background-color: #212345;
|
|
}
|
|
}
|
|
|
|
& time {
|
|
padding-right: 1ch;
|
|
color: #B1A9FF;
|
|
}
|
|
}
|
|
|
|
dd~dt,
|
|
dd~dd {
|
|
border-top: 1px solid #262A65;
|
|
}
|
|
|
|
dd[data-state] {
|
|
background-color: #202248;
|
|
}
|
|
|
|
dd[data-state="running"] {
|
|
background-color: #113B29;
|
|
color: #3DD68C;
|
|
}
|
|
|
|
dd[data-state="stopped"],
|
|
dd[data-state="exited"],
|
|
dd[data-state="dead"] {
|
|
background-color: #500F1C;
|
|
color: #FF9592;
|
|
}
|
|
|
|
dt,
|
|
dd {
|
|
box-sizing: border-box;
|
|
padding: 4px;
|
|
}
|
|
|
|
|
|
dt {
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
dd {
|
|
background-color: #171625;
|
|
padding: 4px 8px;
|
|
word-break: break-all;
|
|
}
|
|
|
|
dl.table {
|
|
border: 1px solid #3D3E82;
|
|
background-color: #202248;
|
|
margin: 0;
|
|
|
|
& dt {
|
|
float: left;
|
|
width: 25%;
|
|
}
|
|
|
|
& dd {
|
|
margin-left: 25%;
|
|
border-left: 1px dotted #3D3E82;
|
|
}
|
|
|
|
|
|
& dd:after {
|
|
content: "";
|
|
display: block;
|
|
clear: both;
|
|
}
|
|
}
|
|
|
|
dl.list {
|
|
border: 1px solid #3D3E82;
|
|
background-color: #202248;
|
|
margin: 0;
|
|
border-top: 0;
|
|
|
|
& dd {
|
|
margin: 0;
|
|
}
|
|
|
|
& dt {
|
|
padding: 4px 8px;
|
|
}
|
|
}
|
|
|
|
|
|
main>header {
|
|
grid-area: head;
|
|
}
|
|
|
|
.status-section {
|
|
grid-area: info;
|
|
}
|
|
|
|
.log-section {
|
|
grid-area: log;
|
|
}
|
|
|
|
@media (min-width: 1000px) {
|
|
main {
|
|
overflow: hidden;
|
|
display: grid;
|
|
grid-template-areas:
|
|
"head head"
|
|
"info log";
|
|
|
|
grid-template-rows: 120px 1fr;
|
|
grid-template-columns: 30vw 1fr;
|
|
}
|
|
|
|
|
|
.status-section>article,
|
|
#log {
|
|
height: calc(100vh - 240px);
|
|
max-height: none;
|
|
overflow: auto;
|
|
}
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
|
|
& form {
|
|
display: flex;
|
|
min-width: 80px;
|
|
}
|
|
|
|
& button {
|
|
flex: 1;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script type="module">
|
|
import ansiFmt from 'https://esm.run/ansi-to-html';
|
|
const convert = new ansiFmt({
|
|
fg: '#E0DFFE',
|
|
bg: '#171625',
|
|
newline: false,
|
|
escapeXML: true,
|
|
stream: false,
|
|
colors: ["#282c34", "#abb2bf", "#e06c75", "#be5046", "#98c379", "#d19a66", "#61afef", "#c678dd", "#56b6c2", "#4b5263", "#5c6370"]
|
|
});
|
|
|
|
const logEl = document.querySelector("#log");
|
|
const logSource = new EventSource(`${location.origin}${location.pathname}log`);
|
|
logSource.addEventListener("error", (ev) => {
|
|
logSource.close();
|
|
const err = document.createElement("div");
|
|
err.className = "error";
|
|
err.appendChild(document.createTextNode("No logs available after this (container not running)"));
|
|
logEl.appendChild(err);
|
|
err.scrollIntoView();
|
|
return;
|
|
});
|
|
logSource.addEventListener("message", (ev) => {
|
|
const data = JSON.parse(ev.data);
|
|
// If an error is received stop listening
|
|
if ("error" in data) {
|
|
logSource.close();
|
|
const err = document.createElement("div");
|
|
err.className = "error";
|
|
err.appendChild(document.createTextNode(data.error));
|
|
logEl.appendChild(err);
|
|
err.scrollIntoView();
|
|
return;
|
|
}
|
|
// Received lines of log
|
|
if ("lines" in data) {
|
|
data.lines.split("\n").forEach(line => {
|
|
if (!line) {
|
|
return;
|
|
}
|
|
// Extract timestamp
|
|
const firstSpace = line.indexOf(' ')
|
|
const [timestamp, logline] = [line.substring(0, firstSpace), line.substring(firstSpace + 1)];
|
|
const lineEl = document.createElement("p");
|
|
lineEl.innerHTML = `<time datetime="${timestamp}">${timestamp}</time>${convert.toHtml(logline)}`.trim();
|
|
logEl.appendChild(lineEl);
|
|
lineEl.scrollIntoView();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Fix copying from log (remove double newlines)
|
|
logEl.addEventListener("copy", (event) => {
|
|
const selection = document.getSelection();
|
|
event.clipboardData.setData("text/plain", selection.toString().replace(/\n\n/g, '\n'));
|
|
event.preventDefault();
|
|
});
|
|
</script>
|
|
|
|
{% endblock %} |