mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-08 05:03:49 -05:00
Finished basic functionality for multiple servers in the backend.
No special CLI or Liquid support yet and also no refresh support
This commit is contained in:
@@ -64,13 +64,13 @@
|
||||
"minLength": 1
|
||||
},
|
||||
"servers": {
|
||||
"type": "array",
|
||||
"type": "object",
|
||||
"description": "Additional servers to connect to and fetch update data from",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"minProperties": 1
|
||||
},
|
||||
"theme": {
|
||||
"description": "The theme used by the web UI",
|
||||
|
||||
43
src/check.rs
43
src/check.rs
@@ -10,22 +10,27 @@ use crate::{
|
||||
registry::{check_auth, get_token},
|
||||
structs::{image::Image, update::Update},
|
||||
utils::request::{get_response_body, parse_json},
|
||||
warn,
|
||||
};
|
||||
|
||||
/// Fetches image data from other Cup servers
|
||||
async fn get_remote_updates(servers: &[String], client: &Client) -> Vec<Update> {
|
||||
/// Fetches image data from other Cup instances
|
||||
async fn get_remote_updates(servers: &FxHashMap<String, String>, client: &Client) -> Vec<Update> {
|
||||
let mut remote_images = Vec::new();
|
||||
|
||||
let futures: Vec<_> = servers
|
||||
let handles: Vec<_> = servers
|
||||
.iter()
|
||||
.map(|server| async {
|
||||
let url = if server.starts_with("http://") || server.starts_with("https://") {
|
||||
format!("{}/api/v3/json", server.trim_end_matches('/'))
|
||||
.map(|(name, url)| async {
|
||||
let url = if url.starts_with("http://") || url.starts_with("https://") {
|
||||
format!("{}/api/v3/json", url.trim_end_matches('/'))
|
||||
} else {
|
||||
format!("https://{}/api/v3/json", server.trim_end_matches('/'))
|
||||
format!("https://{}/api/v3/json", url.trim_end_matches('/'))
|
||||
};
|
||||
match client.get(&url, vec![], false).await {
|
||||
Ok(response) => {
|
||||
if response.status() != 200 {
|
||||
warn!("GET {}: Failed to fetch updates from server. Server returned invalid response code: {}",url,response.status());
|
||||
return Vec::new();
|
||||
}
|
||||
let json = parse_json(&get_response_body(response).await);
|
||||
if let Some(updates) = json["images"].as_array() {
|
||||
let mut server_updates: Vec<Update> = updates
|
||||
@@ -34,7 +39,7 @@ async fn get_remote_updates(servers: &[String], client: &Client) -> Vec<Update>
|
||||
.collect();
|
||||
// Add server origin to each image
|
||||
for update in &mut server_updates {
|
||||
update.server = Some(server.clone());
|
||||
update.server = Some(name.clone());
|
||||
update.status = update.get_status();
|
||||
}
|
||||
return server_updates;
|
||||
@@ -42,12 +47,15 @@ async fn get_remote_updates(servers: &[String], client: &Client) -> Vec<Update>
|
||||
|
||||
Vec::new()
|
||||
}
|
||||
Err(_) => Vec::new(),
|
||||
Err(e) => {
|
||||
warn!("Failed to fetch updates from server. {}", e);
|
||||
Vec::new()
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
for mut images in join_all(futures).await {
|
||||
for mut images in join_all(handles).await {
|
||||
remote_images.append(&mut images);
|
||||
}
|
||||
|
||||
@@ -64,8 +72,7 @@ pub async fn get_updates(references: &Option<Vec<String>>, config: &Config) -> V
|
||||
|
||||
// Add extra images from references
|
||||
if let Some(refs) = references {
|
||||
let image_refs: FxHashSet<&String> =
|
||||
images.iter().map(|image| &image.reference).collect();
|
||||
let image_refs: FxHashSet<&String> = images.iter().map(|image| &image.reference).collect();
|
||||
let extra = refs
|
||||
.iter()
|
||||
.filter(|&reference| !image_refs.contains(reference))
|
||||
@@ -85,10 +92,7 @@ pub async fn get_updates(references: &Option<Vec<String>>, config: &Config) -> V
|
||||
debug!(
|
||||
config.debug,
|
||||
"Checking {:?}",
|
||||
images
|
||||
.iter()
|
||||
.map(|image| &image.reference)
|
||||
.collect_vec()
|
||||
images.iter().map(|image| &image.reference).collect_vec()
|
||||
);
|
||||
|
||||
// Get a list of unique registries our images belong to. We are unwrapping the registry because it's guaranteed to be there.
|
||||
@@ -106,7 +110,10 @@ pub async fn get_updates(references: &Option<Vec<String>>, config: &Config) -> V
|
||||
let mut image_map: FxHashMap<&String, Vec<&Image>> = FxHashMap::default();
|
||||
|
||||
for image in &images {
|
||||
image_map.entry(&image.parts.registry).or_default().push(image);
|
||||
image_map
|
||||
.entry(&image.parts.registry)
|
||||
.or_default()
|
||||
.push(image);
|
||||
}
|
||||
|
||||
// Retrieve an authentication token (if required) for each registry.
|
||||
@@ -149,7 +156,7 @@ pub async fn get_updates(references: &Option<Vec<String>>, config: &Config) -> V
|
||||
.collect::<Vec<&String>>();
|
||||
|
||||
let mut handles = Vec::with_capacity(images.len());
|
||||
|
||||
|
||||
// Loop through images check for updates
|
||||
for image in &images {
|
||||
let is_ignored = ignored_registries.contains(&&image.parts.registry)
|
||||
|
||||
@@ -45,7 +45,7 @@ pub struct Config {
|
||||
pub images: ImageConfig,
|
||||
pub refresh_interval: Option<String>,
|
||||
pub registries: FxHashMap<String, RegistryConfig>,
|
||||
pub servers: Vec<String>,
|
||||
pub servers: FxHashMap<String, String>,
|
||||
pub socket: Option<String>,
|
||||
pub theme: Theme,
|
||||
}
|
||||
@@ -59,7 +59,7 @@ impl Config {
|
||||
images: ImageConfig::default(),
|
||||
refresh_interval: None,
|
||||
registries: FxHashMap::default(),
|
||||
servers: Vec::new(),
|
||||
servers: FxHashMap::default(),
|
||||
socket: None,
|
||||
theme: Theme::Default,
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ use crate::{
|
||||
link::parse_link,
|
||||
request::{
|
||||
get_protocol, get_response_body, parse_json, parse_www_authenticate, to_bearer_string,
|
||||
}, time::{elapsed, now},
|
||||
},
|
||||
time::{elapsed, now},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -173,7 +174,9 @@ pub async fn get_latest_tag(
|
||||
let tag = tags.iter().max();
|
||||
debug!(
|
||||
config.debug,
|
||||
"Checked for tag update to {} in {}ms", image.reference, elapsed(start)
|
||||
"Checked for tag update to {} in {}ms",
|
||||
image.reference,
|
||||
elapsed(start)
|
||||
);
|
||||
match tag {
|
||||
Some(t) => {
|
||||
|
||||
@@ -188,7 +188,7 @@ impl Image {
|
||||
},
|
||||
time: self.time_ms,
|
||||
server: None,
|
||||
status: Status::Unknown(String::new())
|
||||
status: Status::Unknown(String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod image;
|
||||
pub mod inspectdata;
|
||||
pub mod parts;
|
||||
pub mod status;
|
||||
pub mod version;
|
||||
pub mod update;
|
||||
pub mod parts;
|
||||
pub mod version;
|
||||
|
||||
@@ -40,4 +40,4 @@ impl Default for Status {
|
||||
fn default() -> Self {
|
||||
Self::Unknown("".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ pub struct Update {
|
||||
pub parts: Parts,
|
||||
pub result: UpdateResult,
|
||||
pub time: u32,
|
||||
#[serde(skip_serializing)]
|
||||
pub server: Option<String>,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub status: Status,
|
||||
@@ -79,19 +78,15 @@ impl Update {
|
||||
Status::Unknown(s) => {
|
||||
if s.is_empty() {
|
||||
match self.result.has_update {
|
||||
Some(true) => {
|
||||
match &self.result.info {
|
||||
UpdateInfo::Version(info) => {
|
||||
match info.version_update_type.as_str() {
|
||||
"major" => Status::UpdateMajor,
|
||||
"minor" => Status::UpdateMinor,
|
||||
"patch" => Status::UpdatePatch,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
},
|
||||
UpdateInfo::Digest(_) => Status::UpdateAvailable,
|
||||
Some(true) => match &self.result.info {
|
||||
UpdateInfo::Version(info) => match info.version_update_type.as_str() {
|
||||
"major" => Status::UpdateMajor,
|
||||
"minor" => Status::UpdateMinor,
|
||||
"patch" => Status::UpdatePatch,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
},
|
||||
UpdateInfo::Digest(_) => Status::UpdateAvailable,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Some(false) => Status::UpToDate,
|
||||
None => Status::Unknown(self.result.error.clone().unwrap()),
|
||||
@@ -99,8 +94,8 @@ impl Update {
|
||||
} else {
|
||||
self.status.clone()
|
||||
}
|
||||
},
|
||||
status => status.clone()
|
||||
}
|
||||
status => status.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ pub mod logging;
|
||||
pub mod reference;
|
||||
pub mod request;
|
||||
pub mod sort_update_vec;
|
||||
pub mod time;
|
||||
pub mod time;
|
||||
|
||||
@@ -35,8 +35,8 @@ pub fn get_protocol(
|
||||
} else {
|
||||
"https"
|
||||
}
|
||||
},
|
||||
None => "https"
|
||||
}
|
||||
None => "https",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
384
web/index.html
384
web/index.html
@@ -1,34 +1,34 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<meta charset="utf8" />
|
||||
{% if theme == 'neutral' %}
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
content="#fafafa"
|
||||
>
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
content="#0a0a0a"
|
||||
>
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
content="#fafafa"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
content="#0a0a0a"
|
||||
/>
|
||||
{% else %}
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
content="#f9fafb"
|
||||
>
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
content="#030712"
|
||||
>
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: light)"
|
||||
content="#f9fafb"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
content="#030712"
|
||||
/>
|
||||
{% endif %}
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>Cup</title>
|
||||
</head>
|
||||
<body>
|
||||
@@ -39,7 +39,9 @@
|
||||
<div class="mx-auto h-full w-full max-w-[80rem] px-4 sm:px-6 lg:px-8">
|
||||
<div class="mx-auto my-8 h-full max-w-[48rem] flex-col">
|
||||
<div class="flex items-center gap-1">
|
||||
<h1 class="text-5xl font-bold lg:text-6xl dark:text-white">Cup</h1>
|
||||
<h1 class="text-5xl font-bold lg:text-6xl dark:text-white">
|
||||
Cup
|
||||
</h1>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Layer_2"
|
||||
@@ -108,109 +110,97 @@
|
||||
<dl
|
||||
class="grid grid-cols-2 gap-1 overflow-hidden *:relative lg:grid-cols-4"
|
||||
>
|
||||
{% assign metrics_to_show = 'monitored_images,up_to_date,updates_available,unknown' | split: ',' %}
|
||||
{% for metric in metrics %}
|
||||
{% if metrics_to_show contains metric.name %}
|
||||
<div
|
||||
class="before:bg-{{ theme }}-200 before:dark:bg-{{ theme }}-800 after:bg-{{ theme }}-200 after:dark:bg-{{ theme }}-800 gi"
|
||||
{% assign metrics_to_show =
|
||||
'monitored_images,up_to_date,updates_available,unknown' | split:
|
||||
',' %} {% for metric in metrics %} {% if metrics_to_show
|
||||
contains metric.name %}
|
||||
<div
|
||||
class="before:bg-{{ theme }}-200 before:dark:bg-{{ theme }}-800 after:bg-{{ theme }}-200 after:dark:bg-{{ theme }}-800 gi"
|
||||
>
|
||||
<div
|
||||
class="flex h-full flex-col justify-between gap-x-4 gap-y-2 px-6 py-4 align-baseline lg:min-h-32"
|
||||
>
|
||||
<dt
|
||||
class="text-{{ theme }}-500 dark:text-{{ theme }}-400 leading-6 font-medium"
|
||||
>
|
||||
<div
|
||||
class="flex h-full flex-col justify-between gap-x-4 gap-y-2 px-6 py-4 align-baseline lg:min-h-32"
|
||||
{{ metric.name | replace: '_', ' ' | capitalize }}
|
||||
</dt>
|
||||
<div class="flex items-center justify-between gap-1">
|
||||
<dd
|
||||
class="w-full text-3xl font-medium leading-10 tracking-tight text-black dark:text-white"
|
||||
>
|
||||
<dt
|
||||
class="text-{{ theme }}-500 dark:text-{{ theme }}-400 leading-6 font-medium"
|
||||
>
|
||||
{{ metric.name | replace: '_', ' ' | capitalize }}
|
||||
</dt>
|
||||
<div class="flex items-center justify-between gap-1">
|
||||
<dd
|
||||
class="w-full text-3xl font-medium leading-10 tracking-tight text-black dark:text-white"
|
||||
>
|
||||
{{ metric.value }}
|
||||
</dd>
|
||||
{% case metric.name %}
|
||||
{% when 'monitored_images' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 shrink-0 text-black dark:text-white"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 4c4.29 0 7.863 2.429 10.665 7.154l.22 .379l.045 .1l.03 .083l.014 .055l.014 .082l.011 .1v.11l-.014 .111a.992 .992 0 0 1 -.026 .11l-.039 .108l-.036 .075l-.016 .03c-2.764 4.836 -6.3 7.38 -10.555 7.499l-.313 .004c-4.396 0 -8.037 -2.549 -10.868 -7.504a1 1 0 0 1 0 -.992c2.831 -4.955 6.472 -7.504 10.868 -7.504zm0 5a3 3 0 1 0 0 6a3 3 0 0 0 0 -6z"
|
||||
/>
|
||||
</svg>
|
||||
{% when 'up_to_date' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 shrink-0 text-green-500"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z"
|
||||
/>
|
||||
</svg>
|
||||
{% when 'updates_available' %}
|
||||
{% assign max_metric = '' %}
|
||||
{% assign max_value = 0 %}
|
||||
|
||||
{% for m in metrics %}
|
||||
{% unless metrics_to_show contains m.name %}
|
||||
{% if m.value > max_value %}
|
||||
{% assign max_metric = m.name %}
|
||||
{% assign max_value = m.value %}
|
||||
{% endif %}
|
||||
{% endunless %}
|
||||
{% endfor %}
|
||||
|
||||
{% case max_metric %}
|
||||
{% when 'major_updates' %}
|
||||
{% assign color = 'text-red-500' %}
|
||||
{% when 'minor_updates' %}
|
||||
{% assign color = 'text-yellow-500' %}
|
||||
{% else %}
|
||||
{% assign color = 'text-blue-500' %}
|
||||
{% endcase %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 shrink-0 {{ color }}"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z"
|
||||
/>
|
||||
</svg>
|
||||
{% when 'unknown' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-{{ theme }}-500 shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z"
|
||||
/>
|
||||
</svg>
|
||||
{% endcase %}
|
||||
</div>
|
||||
</div>
|
||||
{{ metric.value }}
|
||||
</dd>
|
||||
{% case metric.name %} {% when 'monitored_images' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 shrink-0 text-black dark:text-white"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 4c4.29 0 7.863 2.429 10.665 7.154l.22 .379l.045 .1l.03 .083l.014 .055l.014 .082l.011 .1v.11l-.014 .111a.992 .992 0 0 1 -.026 .11l-.039 .108l-.036 .075l-.016 .03c-2.764 4.836 -6.3 7.38 -10.555 7.499l-.313 .004c-4.396 0 -8.037 -2.549 -10.868 -7.504a1 1 0 0 1 0 -.992c2.831 -4.955 6.472 -7.504 10.868 -7.504zm0 5a3 3 0 1 0 0 6a3 3 0 0 0 0 -6z"
|
||||
/>
|
||||
</svg>
|
||||
{% when 'up_to_date' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 shrink-0 text-green-500"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z"
|
||||
/>
|
||||
</svg>
|
||||
{% when 'updates_available' %} {% assign max_metric = ''
|
||||
%} {% assign max_value = 0 %} {% for m in metrics %} {%
|
||||
unless metrics_to_show contains m.name %} {% if m.value >
|
||||
max_value %} {% assign max_metric = m.name %} {% assign
|
||||
max_value = m.value %} {% endif %} {% endunless %} {%
|
||||
endfor %} {% case max_metric %} {% when 'major_updates' %}
|
||||
{% assign color = 'text-red-500' %} {% when
|
||||
'minor_updates' %} {% assign color = 'text-yellow-500' %}
|
||||
{% else %} {% assign color = 'text-blue-500' %} {% endcase
|
||||
%}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 shrink-0 {{ color }}"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z"
|
||||
/>
|
||||
</svg>
|
||||
{% when 'unknown' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6 text-{{ theme }}-500 shrink-0"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z"
|
||||
/>
|
||||
</svg>
|
||||
{% endcase %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %} {% endfor %}
|
||||
</dl>
|
||||
</div>
|
||||
<div
|
||||
@@ -243,80 +233,74 @@
|
||||
class="*:py-4 *:px-6 *:flex *:items-center *:gap-3 dark:divide-{{ theme }}-800 divide-y dark:text-white"
|
||||
>
|
||||
{% for image in images %}
|
||||
<li>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M21 16.008v-8.018a1.98 1.98 0 0 0 -1 -1.717l-7 -4.008a2.016 2.016 0 0 0 -2 0l-7 4.008c-.619 .355 -1 1.01 -1 1.718v8.018c0 .709 .381 1.363 1 1.717l7 4.008a2.016 2.016 0 0 0 2 0l7 -4.008c.619 -.355 1 -1.01 1 -1.718z"
|
||||
/>
|
||||
<path d="M12 22v-10" />
|
||||
<path d="M12 12l8.73 -5.04" />
|
||||
<path d="M3.27 6.96l8.73 5.04" />
|
||||
</svg>
|
||||
{{ image.name }}
|
||||
{% case image.status %}
|
||||
{% when 'Up to date' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="ml-auto text-green-500"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z"
|
||||
/>
|
||||
</svg>
|
||||
{% when 'Unknown' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="text-{{ theme }}-500 ml-auto"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z"
|
||||
/>
|
||||
</svg>
|
||||
{% else %}
|
||||
{% case image.status %}
|
||||
{% when 'Major update' %}
|
||||
{% assign color = 'text-red-500' %}
|
||||
{% when 'Minor update' %}
|
||||
{% assign color = 'text-yellow-500' %}
|
||||
{% else %}
|
||||
{% assign color = 'text-blue-500' %}
|
||||
{% endcase %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="ml-auto {{ color }}"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z"
|
||||
/>
|
||||
</svg>
|
||||
{% endcase %}
|
||||
</li>
|
||||
<li>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M21 16.008v-8.018a1.98 1.98 0 0 0 -1 -1.717l-7 -4.008a2.016 2.016 0 0 0 -2 0l-7 4.008c-.619 .355 -1 1.01 -1 1.718v8.018c0 .709 .381 1.363 1 1.717l7 4.008a2.016 2.016 0 0 0 2 0l7 -4.008c.619 -.355 1 -1.01 1 -1.718z"
|
||||
/>
|
||||
<path d="M12 22v-10" />
|
||||
<path d="M12 12l8.73 -5.04" />
|
||||
<path d="M3.27 6.96l8.73 5.04" />
|
||||
</svg>
|
||||
{{ image.name }} {% case image.status %} {% when 'Up to date'
|
||||
%}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="ml-auto text-green-500"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-1.293 5.953a1 1 0 0 0 -1.32 -.083l-.094 .083l-3.293 3.292l-1.293 -1.292l-.094 -.083a1 1 0 0 0 -1.403 1.403l.083 .094l2 2l.094 .083a1 1 0 0 0 1.226 0l.094 -.083l4 -4l.083 -.094a1 1 0 0 0 -.083 -1.32z"
|
||||
/>
|
||||
</svg>
|
||||
{% when 'Unknown' %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="text-{{ theme }}-500 ml-auto"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1 -19.995 .324l-.005 -.324l.004 -.28c.148 -5.393 4.566 -9.72 9.996 -9.72zm0 13a1 1 0 0 0 -.993 .883l-.007 .117l.007 .127a1 1 0 0 0 1.986 0l.007 -.117l-.007 -.127a1 1 0 0 0 -.993 -.883zm1.368 -6.673a2.98 2.98 0 0 0 -3.631 .728a1 1 0 0 0 1.44 1.383l.171 -.18a.98 .98 0 0 1 1.11 -.15a1 1 0 0 1 -.34 1.886l-.232 .012a1 1 0 0 0 .111 1.994a3 3 0 0 0 1.371 -5.673z"
|
||||
/>
|
||||
</svg>
|
||||
{% else %} {% case image.status %} {% when 'Major update' %}
|
||||
{% assign color = 'text-red-500' %} {% when 'Minor update' %}
|
||||
{% assign color = 'text-yellow-500' %} {% else %} {% assign
|
||||
color = 'text-blue-500' %} {% endcase %}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="ml-auto {{ color }}"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M17 3.34a10 10 0 1 1 -14.995 8.984l-.005 -.324l.005 -.324a10 10 0 0 1 14.995 -8.336zm-4.98 3.66l-.163 .01l-.086 .016l-.142 .045l-.113 .054l-.07 .043l-.095 .071l-.058 .054l-4 4l-.083 .094a1 1 0 0 0 1.497 1.32l2.293 -2.293v5.586l.007 .117a1 1 0 0 0 1.993 -.117v-5.585l2.293 2.292l.094 .083a1 1 0 0 0 1.32 -1.497l-4 -4l-.082 -.073l-.089 -.064l-.113 -.062l-.081 -.034l-.113 -.034l-.112 -.02l-.098 -.006z"
|
||||
/>
|
||||
</svg>
|
||||
{% endcase %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Data } from "./types";
|
||||
import { theme } from "./theme";
|
||||
import RefreshButton from "./components/RefreshButton";
|
||||
import Search from "./components/Search";
|
||||
import { Server } from "./components/Server";
|
||||
|
||||
const SORT_ORDER = [
|
||||
"monitored_images",
|
||||
@@ -40,15 +41,17 @@ function App() {
|
||||
className={`bg-white shadow-sm dark:bg-${theme}-900 my-8 rounded-md`}
|
||||
>
|
||||
<dl className="grid grid-cols-2 gap-1 overflow-hidden *:relative lg:grid-cols-4">
|
||||
{Object.entries(data.metrics).sort((a, b) => {
|
||||
return SORT_ORDER.indexOf(a[0]) - SORT_ORDER.indexOf(b[0]);
|
||||
}).map(([name]) => (
|
||||
<Statistic
|
||||
name={name as keyof typeof data.metrics}
|
||||
metrics={data.metrics}
|
||||
key={name}
|
||||
/>
|
||||
))}
|
||||
{Object.entries(data.metrics)
|
||||
.sort((a, b) => {
|
||||
return SORT_ORDER.indexOf(a[0]) - SORT_ORDER.indexOf(b[0]);
|
||||
})
|
||||
.map(([name]) => (
|
||||
<Statistic
|
||||
name={name as keyof typeof data.metrics}
|
||||
metrics={data.metrics}
|
||||
key={name}
|
||||
/>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
<div
|
||||
@@ -61,11 +64,27 @@ function App() {
|
||||
<RefreshButton />
|
||||
</div>
|
||||
<Search onChange={setSearchQuery} />
|
||||
<ul className={`dark:divide-${theme}-800 divide-y dark:text-white`}>
|
||||
{data.images
|
||||
.filter((image) => image.reference.includes(searchQuery))
|
||||
.map((image) => (
|
||||
<Image data={image} key={image.reference} />
|
||||
<ul>
|
||||
{Object.entries(
|
||||
data.images.reduce(
|
||||
(acc, image) => {
|
||||
const server = image.server ?? "";
|
||||
if (!acc[server]) acc[server] = [];
|
||||
acc[server].push(image);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, typeof data.images>,
|
||||
),
|
||||
)
|
||||
.sort()
|
||||
.map(([server, images]) => (
|
||||
<Server name={server} key={server}>
|
||||
{images
|
||||
.filter((image) => image.reference.includes(searchQuery))
|
||||
.map((image) => (
|
||||
<Image data={image} key={image.reference} />
|
||||
))}
|
||||
</Server>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -51,11 +51,8 @@ export default function Image({ data }: { data: Image }) {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={handleOpen}
|
||||
className={`w-full *:flex *:items-center *:gap-3 *:px-6 *:py-4`}
|
||||
>
|
||||
<li className="break-all text-start">
|
||||
<button onClick={handleOpen} className="w-full">
|
||||
<li className="flex items-center gap-4 break-all px-6 py-4 text-start">
|
||||
<IconCube className="size-6 shrink-0" />
|
||||
{data.reference}
|
||||
<Icon data={data} />
|
||||
|
||||
45
web/src/components/Server.tsx
Normal file
45
web/src/components/Server.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from "@headlessui/react";
|
||||
import { theme } from "../theme";
|
||||
import { IconChevronDown } from "@tabler/icons-react";
|
||||
|
||||
export function Server({
|
||||
name,
|
||||
children,
|
||||
}: {
|
||||
name: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
if (name.length === 0)
|
||||
return (
|
||||
<li className="mb-8 last:mb-0">
|
||||
<ul className={`dark:divide-${theme}-800 divide-y dark:text-white`}>
|
||||
{children}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
return (
|
||||
<Disclosure defaultOpen as="li" className={`mb-4 last:mb-0`}>
|
||||
<DisclosureButton className="group my-4 flex w-full items-center justify-between px-6">
|
||||
<span
|
||||
className={`text-lg font-semibold text-${theme}-600 dark:text-${theme}-400 group-data-[hover]:text-${theme}-800 group-data-[hover]:dark:text-${theme}-200 transition-colors duration-300`}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
<IconChevronDown
|
||||
className={`duration:300 size-5 text-${theme}-600 transition-transform group-data-[open]:rotate-180 dark:text-${theme}-400 group-data-[hover]:text-${theme}-800 group-data-[hover]:dark:text-${theme}-200 transition-colors duration-300`}
|
||||
/>
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel
|
||||
className={`dark:divide-${theme}-800 divide-y dark:text-white`}
|
||||
as="ul"
|
||||
transition
|
||||
>
|
||||
{children}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export interface Image {
|
||||
error: string | null;
|
||||
};
|
||||
time: number;
|
||||
server: string | null;
|
||||
}
|
||||
|
||||
interface VersionInfo {
|
||||
|
||||
@@ -41,6 +41,14 @@ export default {
|
||||
{
|
||||
pattern: /text-(gray|neutral)-700/,
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-800/,
|
||||
variants: ["group-data-[hover]"],
|
||||
},
|
||||
{
|
||||
pattern: /text-(gray|neutral)-200/,
|
||||
variants: ["group-data-[hover]:dark"],
|
||||
},
|
||||
{
|
||||
pattern: /divide-(gray|neutral)-800/,
|
||||
variants: ["dark"],
|
||||
|
||||
@@ -12,6 +12,6 @@ export default defineConfig({
|
||||
chunkFileNames: `assets/[name].js`,
|
||||
assetFileNames: `assets/[name].[ext]`,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user