more stats
This commit is contained in:
parent
2cde2f57f6
commit
a3f0ddb99f
4 changed files with 151 additions and 16 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -266,6 +266,7 @@ dependencies = [
|
|||
"serde_repr",
|
||||
"serde_urlencoded",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"url",
|
||||
|
@ -281,6 +282,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_repr",
|
||||
"serde_with",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1083,6 +1085,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
|
|
|
@ -8,13 +8,14 @@ anyhow = "1"
|
|||
askama = { version = "0.12", features = ["with-axum"] }
|
||||
askama_axum = "0.3"
|
||||
axum = "0.6"
|
||||
bollard = "0.15"
|
||||
bollard = { version = "0.15", features = ["time"] }
|
||||
clap = { version = "4", features = ["env", "derive"] }
|
||||
dotenvy = "0.15"
|
||||
futures-util = "0.3"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
thiserror = "1"
|
||||
time = { version = "0.3", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
tracing = "0.1"
|
||||
|
|
|
@ -1,34 +1,59 @@
|
|||
use crate::http::error::Result;
|
||||
use bollard::{
|
||||
container::InspectContainerOptions,
|
||||
service::{ContainerInspectResponse, ContainerSummary},
|
||||
service::{ContainerInspectResponse, ContainerSummary, MountPoint},
|
||||
Docker,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ContainerInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub state: String,
|
||||
pub image: String,
|
||||
pub image_id: String,
|
||||
pub created_at: String,
|
||||
pub volumes: Option<Vec<MountPoint>>,
|
||||
pub env: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl From<ContainerSummary> for ContainerInfo {
|
||||
fn from(value: ContainerSummary) -> Self {
|
||||
let created = OffsetDateTime::from_unix_timestamp(value.created.unwrap())
|
||||
.unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
||||
ContainerInfo {
|
||||
name: value.names.unwrap()[0].trim_start_matches('/').to_string(),
|
||||
state: value.state.unwrap(),
|
||||
image: value.image.unwrap(),
|
||||
id: value.id.unwrap_or_default(),
|
||||
name: value.names.unwrap_or_default()[0]
|
||||
.trim_start_matches('/')
|
||||
.to_string(),
|
||||
state: value.state.unwrap_or_default(),
|
||||
image: value.image.unwrap_or_default(),
|
||||
image_id: value.image_id.unwrap_or_default(),
|
||||
created_at: created.time().to_string(),
|
||||
volumes: value.mounts,
|
||||
env: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContainerInspectResponse> for ContainerInfo {
|
||||
fn from(value: ContainerInspectResponse) -> Self {
|
||||
let config = value.config.unwrap_or_default();
|
||||
ContainerInfo {
|
||||
name: value.name.unwrap(),
|
||||
state: value.state.and_then(|s| s.status).unwrap().to_string(),
|
||||
image: value.image.unwrap(),
|
||||
id: value.id.unwrap_or_default(),
|
||||
name: value.name.unwrap_or_default(),
|
||||
state: value
|
||||
.state
|
||||
.and_then(|s| s.status)
|
||||
.unwrap_or_else(|| bollard::service::ContainerStateStatusEnum::EMPTY)
|
||||
.to_string(),
|
||||
image: config.image.unwrap_or_default(),
|
||||
image_id: value.image.unwrap_or_default(),
|
||||
created_at: value.created.unwrap_or_default(),
|
||||
volumes: value.mounts,
|
||||
env: config.env,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,44 @@
|
|||
{% block content %}
|
||||
<main>
|
||||
<h1>Container <span class="container-name">{{container_name}}</span></h1>
|
||||
<section class="status-section">
|
||||
<h2>Status</h2>
|
||||
<div>TODO</div>
|
||||
<dl>
|
||||
<dt>ID</dt>
|
||||
<dd>{{ info.id }}</dd>
|
||||
<dt>Status</dt>
|
||||
<dd>{{ 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>
|
||||
{% 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 %}
|
||||
</dl>
|
||||
</section>
|
||||
<section class="log-section">
|
||||
<h2>Logs</h2>
|
||||
<code id="log">
|
||||
</code>
|
||||
<code id="log"></code>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style scoped>
|
||||
|
@ -23,6 +56,7 @@
|
|||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
|
@ -39,7 +73,6 @@
|
|||
display: flex;
|
||||
|
||||
display: block;
|
||||
height: 800px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
|
||||
|
@ -59,6 +92,73 @@
|
|||
}
|
||||
}
|
||||
|
||||
& time {
|
||||
padding-right: 1ch;
|
||||
color: #B1A9FF;
|
||||
}
|
||||
}
|
||||
|
||||
dl {
|
||||
border: 1px solid #3D3E82;
|
||||
background-color: #202248;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dt,
|
||||
dd {
|
||||
box-sizing: border-box;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
dd~dt,
|
||||
dd~dd {
|
||||
border-top: 1px solid #262A65;
|
||||
}
|
||||
|
||||
dt {
|
||||
float: left;
|
||||
width: 25%;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 25%;
|
||||
border-left: 1px dotted #3D3E82;
|
||||
background-color: #171625;
|
||||
padding: 4px 8px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
|
||||
dd:after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
main>h1 {
|
||||
grid-area: head;
|
||||
}
|
||||
|
||||
.status-section {
|
||||
grid-area: info;
|
||||
}
|
||||
|
||||
.log-section {
|
||||
grid-area: log;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"head head head"
|
||||
"info log log"
|
||||
"info log log";
|
||||
|
||||
grid-template-rows: 80px 1fr;
|
||||
grid-template-columns: 30vw 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@ -98,9 +198,15 @@
|
|||
}
|
||||
// Received lines of log
|
||||
if ("lines" in data) {
|
||||
data.lines.split("\n").map(line => convert.toHtml(line)).filter(line => line).forEach(line => {
|
||||
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 = line.trim();
|
||||
lineEl.innerHTML = `<time datetime="${timestamp}">${timestamp}</time>${convert.toHtml(logline)}`.trim();
|
||||
logEl.appendChild(lineEl);
|
||||
lineEl.scrollIntoView();
|
||||
});
|
||||
|
|
Reference in a new issue