From b9278ca01098ace3aea0846a06f352beae0705de Mon Sep 17 00:00:00 2001 From: Sergio <77530549+sergi0g@users.noreply.github.com> Date: Sat, 13 Jul 2024 15:25:39 +0300 Subject: [PATCH] Refactor and simplify server code, UI updates requested in #5 and #6 --- Cargo.lock | 1 + Cargo.toml | 1 + src/main.rs | 3 +- src/server.rs | 151 ++++++++++++++++++++++--------------- src/static/index.css | 2 +- src/static/template.liquid | 90 +++++++++++++++++++++- src/utils.rs | 26 +++++-- 7 files changed, 202 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0318aa8..1e32446 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -353,6 +353,7 @@ name = "cup" version = "1.1.3" dependencies = [ "bollard", + "chrono", "clap", "http-auth", "indicatif", diff --git a/Cargo.toml b/Cargo.toml index 6d9acf6..de6c14d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ once_cell = "1.19.0" http-auth = { version = "0.1.9", features = [] } termsize = { version = "0.1.8", optional = true } regex = "1.10.5" +chrono = { version = "0.4.38", default-features = false, features = ["std", "alloc", "clock"] } [features] default = ["server", "cli"] diff --git a/src/main.rs b/src/main.rs index 7580bf6..ff7c446 100644 --- a/src/main.rs +++ b/src/main.rs @@ -95,8 +95,7 @@ async fn main() { }, #[cfg(feature = "server")] Some(Commands::Serve { port }) => { - let updates = get_all_updates(cli.socket).await; - let _ = serve(port, &updates).await; + let _ = serve(port, cli.socket).await; } None => (), } diff --git a/src/server.rs b/src/server.rs index 77f97d8..72217c8 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,27 +1,33 @@ -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; +use chrono::Local; use liquid::{object, Object}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use xitca_web::{ body::ResponseBody, - handler::{handler_service, state::StateOwn}, + handler::{handler_service, state::StateRef}, http::WebResponse, + middleware::Logger, route::get, App, - middleware::Logger }; +use crate::{get_all_updates, utils::sort_update_vec}; + const RAW_TEMPLATE: &str = include_str!("static/template.liquid"); const STYLE: &str = include_str!("static/index.css"); const FAVICON_ICO: &[u8] = include_bytes!("static/favicon.ico"); const FAVICON_SVG: &[u8] = include_bytes!("static/favicon.svg"); const APPLE_TOUCH_ICON: &[u8] = include_bytes!("static/apple-touch-icon.png"); -pub async fn serve(port: &u16, updates: &[(String, Option)]) -> std::io::Result<()> { +pub async fn serve(port: &u16, socket: Option) -> std::io::Result<()> { + let mut data = UpdateData::new(socket).await; + data.refresh().await; App::new() - .with_state(updates.to_owned()) + .with_state(Arc::new(Mutex::new(data))) .at("/", get(handler_service(home))) .at("/json", get(handler_service(json))) + .at("/refresh", get(handler_service(refresh))) .at("/favicon.ico", handler_service(favicon_ico)) // These aren't pretty but this is xitca-web... .at("/favicon.svg", handler_service(favicon_svg)) .at("/apple-touch-icon.png", handler_service(apple_touch_icon)) @@ -32,62 +38,17 @@ pub async fn serve(port: &u16, updates: &[(String, Option)]) -> std::io::R .wait() } -async fn home( - updates: StateOwn)>>, -) -> WebResponse { - let template = liquid::ParserBuilder::with_stdlib() - .build() - .unwrap() - .parse(RAW_TEMPLATE) - .unwrap(); - let images = updates - .0 - .par_iter() - .map(|(name, image)| match image { - Some(value) => { - if *value { - object!({"name": name, "status": "update-available"}) - } else { - object!({"name": name, "status": "up-to-date"}) - } - } - None => object!({"name": name, "status": "unknown"}), - }) - .collect::>(); - let uptodate = images - .par_iter() - .filter(|&o| o["status"] == "up-to-date") - .collect::>() - .len(); - let updatable = images - .par_iter() - .filter(|&o| o["status"] == "update-available") - .collect::>() - .len(); - let unknown = images - .par_iter() - .filter(|&o| o["status"] == "unknown") - .collect::>() - .len(); - let globals = object!({ - "metrics": [{"name": "Monitored images", "value": images.len()}, {"name": "Up to date", "value": uptodate}, {"name": "Updates available", "value": updatable}, {"name": "Unknown", "value": unknown}], - "images": images, - "style": STYLE - }); - let result = template.render(&globals).unwrap(); - WebResponse::new(ResponseBody::from(result)) +async fn home(data: StateRef<'_, Arc>>) -> WebResponse { + WebResponse::new(ResponseBody::from(data.lock().unwrap().template.clone())) } -async fn json( - updates: StateOwn)>> -) -> WebResponse { - let result_mutex: Mutex = Mutex::new(json::object::Object::new()); - updates.par_iter().for_each(|image| match image.1 { - Some(b) => result_mutex.lock().unwrap().insert(&image.0, json::from(b)), - None => result_mutex.lock().unwrap().insert(&image.0, json::Null), - }); - let result = json::stringify(result_mutex.lock().unwrap().clone()); - WebResponse::new(ResponseBody::from(result)) +async fn json(data: StateRef<'_, Arc>>) -> WebResponse { + WebResponse::new(ResponseBody::from(data.lock().unwrap().json.clone())) +} + +async fn refresh(data: StateRef<'_, Arc>>) -> WebResponse { + data.lock().unwrap().refresh().await; + return WebResponse::new(ResponseBody::from("OK")); } async fn favicon_ico() -> WebResponse { @@ -100,4 +61,74 @@ async fn favicon_svg() -> WebResponse { async fn apple_touch_icon() -> WebResponse { WebResponse::new(ResponseBody::from(APPLE_TOUCH_ICON)) -} \ No newline at end of file +} + +struct UpdateData { + template: String, + raw: Vec<(String, Option)>, + json: String, + socket: Option, +} + +impl UpdateData { + async fn new(socket: Option) -> Self { + return Self { + socket, + template: String::new(), + json: String::new(), + raw: Vec::new(), + }; + } + async fn refresh(self: &mut Self) { + let updates = sort_update_vec(&get_all_updates(self.socket.clone()).await); + self.raw = updates; + let template = liquid::ParserBuilder::with_stdlib() + .build() + .unwrap() + .parse(RAW_TEMPLATE) + .unwrap(); + let images = self + .raw + .iter() + .map(|(name, image)| match image { + Some(value) => { + if *value { + object!({"name": name, "status": "update-available"}) + } else { + object!({"name": name, "status": "up-to-date"}) + } + } + None => object!({"name": name, "status": "unknown"}), + }) + .collect::>(); + let uptodate = images + .par_iter() + .filter(|&o| o["status"] == "up-to-date") + .collect::>() + .len(); + let updatable = images + .par_iter() + .filter(|&o| o["status"] == "update-available") + .collect::>() + .len(); + let unknown = images + .par_iter() + .filter(|&o| o["status"] == "unknown") + .collect::>() + .len(); + let last_updated = Local::now().format("%Y-%m-%d %H:%M:%S"); + let globals = object!({ + "metrics": [{"name": "Monitored images", "value": images.len()}, {"name": "Up to date", "value": uptodate}, {"name": "Updates available", "value": updatable}, {"name": "Unknown", "value": unknown}], + "images": images, + "style": STYLE, + "last_updated": last_updated.to_string() + }); + self.template = template.render(&globals).unwrap(); + let json_data: Mutex = Mutex::new(json::object::Object::new()); + self.raw.par_iter().for_each(|image| match image.1 { + Some(b) => json_data.lock().unwrap().insert(&image.0, json::from(b)), + None => json_data.lock().unwrap().insert(&image.0, json::Null), + }); + self.json = json::stringify(json_data.lock().unwrap().clone()); + } +} diff --git a/src/static/index.css b/src/static/index.css index 7c74bc4..9dac4df 100644 --- a/src/static/index.css +++ b/src/static/index.css @@ -1 +1 @@ -/*! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{-webkit-text-size-adjust:100%;font-feature-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-feature-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.absolute{position:absolute}.relative{position:relative}.mx-auto{margin-left:auto;margin-right:auto}.my-8{margin-bottom:2rem;margin-top:2rem}.ml-auto{margin-left:auto}.flex{display:flex}.grid{display:grid}.size-16{height:4rem;width:4rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-full{width:100%}.max-w-\[48rem\]{max-width:48rem}.max-w-\[80rem\]{max-width:80rem}.flex-none{flex:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-2{row-gap:.5rem}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.overflow-hidden{overflow:hidden}.rounded-md{border-radius:.375rem}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-4{padding-bottom:1rem;padding-top:1rem}.align-baseline{vertical-align:initial}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-6xl{font-size:3.75rem;line-height:1}.font-bold{font-weight:700}.font-medium{font-weight:500}.leading-10{line-height:2.5rem}.leading-6{line-height:1.5rem}.tracking-tight{letter-spacing:-.025em}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.\*\:relative>*{position:relative}.\*\:flex>*{display:flex}.\*\:items-center>*{align-items:center}.\*\:gap-3>*{gap:.75rem}.\*\:px-6>*{padding-left:1.5rem;padding-right:1.5rem}.\*\:py-4>*{padding-bottom:1rem;padding-top:1rem}@media (min-width:640px){.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width:1280px){.xl\:px-8{padding-left:2rem;padding-right:2rem}}@media (prefers-color-scheme:dark){.dark\:divide-gray-800>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(31 41 55/var(--tw-divide-opacity))}.dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.dark\:bg-gray-950{--tw-bg-opacity:1;background-color:rgb(3 7 18/var(--tw-bg-opacity))}.dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}} \ No newline at end of file +/*! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{-webkit-text-size-adjust:100%;font-feature-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-feature-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.absolute{position:absolute}.relative{position:relative}.mx-auto{margin-left:auto;margin-right:auto}.my-8{margin-bottom:2rem;margin-top:2rem}.ml-auto{margin-left:auto}.flex{display:flex}.grid{display:grid}.size-16{height:4rem;width:4rem}.size-6{height:1.5rem;width:1.5rem}.h-full{height:100%}.min-h-screen{min-height:100vh}.w-full{width:100%}.max-w-\[48rem\]{max-width:48rem}.max-w-\[80rem\]{max-width:80rem}.flex-none{flex:none}.shrink-0{flex-shrink:0}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-2{row-gap:.5rem}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.overflow-hidden{overflow:hidden}.rounded-md{border-radius:.375rem}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-4{padding-bottom:1rem;padding-top:1rem}.align-baseline{vertical-align:initial}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-5xl{font-size:3rem;line-height:1}.font-bold{font-weight:700}.font-medium{font-weight:500}.leading-10{line-height:2.5rem}.leading-6{line-height:1.5rem}.tracking-tight{letter-spacing:-.025em}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.\*\:relative>*{position:relative}.\*\:flex>*{display:flex}.\*\:items-center>*{align-items:center}.\*\:gap-3>*{gap:.75rem}.\*\:px-6>*{padding-left:1.5rem;padding-right:1.5rem}.\*\:py-4>*{padding-bottom:1rem;padding-top:1rem}@keyframes spin{to{transform:rotate(1turn)}}.group:disabled .group-disabled\:animate-spin{animation:spin 1s linear infinite}@media (min-width:640px){.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:text-6xl{font-size:3.75rem;line-height:1}}@media (min-width:1280px){.xl\:px-8{padding-left:2rem;padding-right:2rem}}@media (prefers-color-scheme:dark){.dark\:divide-gray-800>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(31 41 55/var(--tw-divide-opacity))}.dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.dark\:bg-gray-950{--tw-bg-opacity:1;background-color:rgb(3 7 18/var(--tw-bg-opacity))}.dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}} \ No newline at end of file diff --git a/src/static/template.liquid b/src/static/template.liquid index 7c4d1ed..9e79a1f 100644 --- a/src/static/template.liquid +++ b/src/static/template.liquid @@ -6,7 +6,7 @@ - + @@ -70,13 +70,28 @@ } } +
-

Cup

+

Cup

-
{{ metric.name }}
+
+ {{ metric.name }} + {% if metric.name == 'Monitored images' %} + + + + {% elsif metric.name == 'Up to date' %} + + + + + {% elsif metric.name == 'Updates available' %} + + + + + {% elsif metric.name == 'Unknown' %} + + + + + {% endif %} +
{{ metric.value }}
@@ -130,6 +195,25 @@
+
+

Last checked: {{ last_updated }}

+ +
    {% for image in images %}
  • diff --git a/src/utils.rs b/src/utils.rs index f0d03e7..03542fa 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,7 @@ -use regex::Regex; use once_cell::sync::Lazy; +use regex::Regex; +/// This macro is an alternative to panic. It prints the message you give it and exits the process with code 1, without printing a stack trace. Useful for when the program has to exit due to a user error or something unexpected which is unrelated to the program (e.g. a failed web request) #[macro_export] macro_rules! error { ($($arg:tt)*) => ({ @@ -9,7 +10,7 @@ macro_rules! error { }) } -// Takes an image and splits it into registry, repository and tag. For example ghcr.io/sergi0g/cup:latest becomes ['ghcr.io', 'sergi0g/cup', 'latest']. +/// Takes an image and splits it into registry, repository and tag. For example ghcr.io/sergi0g/cup:latest becomes ['ghcr.io', 'sergi0g/cup', 'latest']. pub fn split_image(image: &str) -> (String, String, String) { static RE: Lazy = Lazy::new(|| { Regex::new( @@ -45,26 +46,39 @@ pub fn split_image(image: &str) -> (String, String, String) { } } +/// Given an image's parts which were previously created by split_image, recreate a reference that docker would use. This means removing the registry part, if it's Docker Hub and removing "library" if the image is official pub fn unsplit_image(registry: &str, repository: &str, tag: &str) -> String { let reg = match registry { "registry-1.docker.io" => String::new(), r => format!("{}/", r), }; let repo = match repository.split('/').collect::>()[0] { - "library" => repository.strip_prefix("library/").unwrap(), + "library" => { + if reg.is_empty() { + repository.strip_prefix("library/").unwrap() + } else { + repository + } + } _ => repository, }; format!("{}{}:{}", reg, repo, tag) } -#[cfg(feature = "cli")] +/// Sorts the update vector alphabetically and where Some(true) > Some(false) > None pub fn sort_update_vec(updates: &[(String, Option)]) -> Vec<(String, Option)> { let mut sorted_updates = updates.to_vec(); sorted_updates.sort_unstable_by(|a, b| match (a.1, b.1) { - (Some(a), Some(b)) => (!a).cmp(&!b), + (Some(c), Some(d)) => { + if c == d { + a.0.cmp(&b.0) + } else { + (!c).cmp(&!d) + } + } (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, - (None, None) => std::cmp::Ordering::Equal, + (None, None) => a.0.cmp(&b.0), }); sorted_updates.to_vec() }