diff --git a/src/check.rs b/src/check.rs index 27e188e..02acfe3 100644 --- a/src/check.rs +++ b/src/check.rs @@ -107,8 +107,9 @@ pub async fn get_updates( // Complete in_use field images.iter_mut().for_each(|image| { - if in_use_images.contains(&image.reference) { - image.in_use = true + match in_use_images.get(&image.reference) { + Some(images) => image.used_by = images.clone(), + None => {} } }); diff --git a/src/docker.rs b/src/docker.rs index 9d31d3e..cb09827 100644 --- a/src/docker.rs +++ b/src/docker.rs @@ -1,6 +1,7 @@ use bollard::{container::ListContainersOptions, models::ImageInspect, ClientVersion, Docker}; use futures::future::join_all; +use rustc_hash::FxHashMap; use crate::{error, structs::image::Image, Context}; @@ -95,7 +96,7 @@ pub async fn get_images_from_docker_daemon( local_images } -pub async fn get_in_use_images(ctx: &Context) -> Vec { +pub async fn get_in_use_images(ctx: &Context) -> FxHashMap> { let client: Docker = create_docker_client(ctx.config.socket.as_deref()); let containers = match client @@ -111,17 +112,40 @@ pub async fn get_in_use_images(ctx: &Context) -> Vec { } }; + let mut result: FxHashMap> = FxHashMap::default(); + containers .iter() - .filter_map(|container| match &container.image { - Some(image) => Some({ - if image.contains(":") { - image.clone() - } else { - format!("{image}:latest") + .filter(|container| container.image.is_some()) + .for_each(|container| { + let reference = match &container.image { + Some(image) => { + if image.contains(":") { + image.clone() + } else { + format!("{image}:latest") + } } - }), - None => None, - }) - .collect() + None => unreachable!(), + }; + + let mut names: Vec = container + .names + .as_ref() + .map(|names| { + names + .iter() + .map(|name| name.trim_start_matches('/').to_owned()) + .collect() + }) + .unwrap_or(Vec::new()); + + match result.get_mut(&reference) { + Some(containers) => containers.append(&mut names), + None => { + let _ = result.insert(reference, names); + } + } + }); + result.clone() } diff --git a/src/structs/image.rs b/src/structs/image.rs index 17a19b7..482e2ba 100644 --- a/src/structs/image.rs +++ b/src/structs/image.rs @@ -38,7 +38,7 @@ pub struct Image { pub url: Option, pub digest_info: Option, pub version_info: Option, - pub in_use: bool, + pub used_by: Vec, pub error: Option, pub time_ms: u32, } @@ -222,7 +222,7 @@ impl Image { }, time: self.time_ms, server: None, - in_use: self.in_use, + used_by: self.used_by.clone(), status: has_update, } } diff --git a/src/structs/update.rs b/src/structs/update.rs index d16b98c..93e8896 100644 --- a/src/structs/update.rs +++ b/src/structs/update.rs @@ -11,7 +11,7 @@ pub struct Update { pub result: UpdateResult, pub time: u32, pub server: Option, - pub in_use: bool, + pub used_by: Vec, #[serde(skip_serializing, skip_deserializing)] pub status: Status, } diff --git a/web/src/App.tsx b/web/src/App.tsx index 61725bd..c041d3f 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -123,7 +123,7 @@ function App() { {images .filter((image) => - filters.onlyInUse ? !!image.in_use : true, + filters.onlyInUse ? image.used_by.length > 0 : true, ) .filter((image) => filters.registries.length == 0 diff --git a/web/src/components/Image.tsx b/web/src/components/Image.tsx index 7cccaf5..4523bcf 100644 --- a/web/src/components/Image.tsx +++ b/web/src/components/Image.tsx @@ -4,6 +4,9 @@ import { DialogBackdrop, DialogPanel, DialogTitle, + Disclosure, + DisclosureButton, + DisclosurePanel, } from "@headlessui/react"; import { WithTooltip } from "./ui/Tooltip"; import type { Image } from "../types"; @@ -11,15 +14,17 @@ import { theme } from "../theme"; import { CodeBlock } from "./CodeBlock"; import { Box, + ChevronDown, CircleArrowUp, CircleCheck, + Container, HelpCircle, Timer, TriangleAlert, X, } from "lucide-react"; import Badge from "./Badge"; -import { getDescription } from "../utils"; +import { cn, getDescription, truncateArray } from "../utils"; const clickable_registries = [ "registry-1.docker.io", @@ -30,12 +35,16 @@ const clickable_registries = [ export default function Image({ data }: { data: Image }) { const [open, setOpen] = useState(false); + const [showUsedBy, setShowUsedBy] = useState(false); const handleOpen = () => { setOpen(true); }; const handleClose = () => { setOpen(false); }; + const toggleShowUsedBy = () => { + setShowUsedBy(!showUsedBy) + } const new_reference = data.result.info?.type == "version" ? data.reference.split(":")[0] + ":" + data.result.info.new_tag @@ -140,6 +149,21 @@ export default function Image({ data }: { data: Image }) { Checked in {data.time} ms + {data.used_by.length !== 0 && ( +
+ +
+ Used by + {data.used_by.length > 1 ? ( + + ) : ( + data.used_by[0] + )} +
+
+ )} {data.result.error && (
diff --git a/web/src/components/ui/Select.tsx b/web/src/components/ui/Select.tsx index 1fe2de7..3bb7d71 100644 --- a/web/src/components/ui/Select.tsx +++ b/web/src/components/ui/Select.tsx @@ -6,7 +6,7 @@ import { } from "@headlessui/react"; import { ChevronDown, Check } from "lucide-react"; import { theme } from "../../theme"; -import { cn } from "../../utils"; +import { cn, truncateArray } from "../../utils"; import { Server } from "lucide-react"; export default function Select({ @@ -27,7 +27,7 @@ export default function Select({
)} - {selectedItems.length == 0 - ? placeholder - : selectedItems.length == 1 - ? selectedItems[0] - : `${selectedItems[0]} +${(selectedItems.length - 1).toString()} more`} + {selectedItems.length == 0 + ? placeholder + : truncateArray(selectedItems)} +