mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-14 16:13:48 -05:00
feat: show which containers use a specific image
This commit is contained in:
@@ -107,8 +107,9 @@ pub async fn get_updates(
|
|||||||
|
|
||||||
// Complete in_use field
|
// Complete in_use field
|
||||||
images.iter_mut().for_each(|image| {
|
images.iter_mut().for_each(|image| {
|
||||||
if in_use_images.contains(&image.reference) {
|
match in_use_images.get(&image.reference) {
|
||||||
image.in_use = true
|
Some(images) => image.used_by = images.clone(),
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use bollard::{container::ListContainersOptions, models::ImageInspect, ClientVersion, Docker};
|
use bollard::{container::ListContainersOptions, models::ImageInspect, ClientVersion, Docker};
|
||||||
|
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use crate::{error, structs::image::Image, Context};
|
use crate::{error, structs::image::Image, Context};
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ pub async fn get_images_from_docker_daemon(
|
|||||||
local_images
|
local_images
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_in_use_images(ctx: &Context) -> Vec<String> {
|
pub async fn get_in_use_images(ctx: &Context) -> FxHashMap<String, Vec<String>> {
|
||||||
let client: Docker = create_docker_client(ctx.config.socket.as_deref());
|
let client: Docker = create_docker_client(ctx.config.socket.as_deref());
|
||||||
|
|
||||||
let containers = match client
|
let containers = match client
|
||||||
@@ -111,17 +112,40 @@ pub async fn get_in_use_images(ctx: &Context) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut result: FxHashMap<String, Vec<String>> = FxHashMap::default();
|
||||||
|
|
||||||
containers
|
containers
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|container| match &container.image {
|
.filter(|container| container.image.is_some())
|
||||||
Some(image) => Some({
|
.for_each(|container| {
|
||||||
|
let reference = match &container.image {
|
||||||
|
Some(image) => {
|
||||||
if image.contains(":") {
|
if image.contains(":") {
|
||||||
image.clone()
|
image.clone()
|
||||||
} else {
|
} else {
|
||||||
format!("{image}:latest")
|
format!("{image}:latest")
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
None => None,
|
None => unreachable!(),
|
||||||
})
|
};
|
||||||
|
|
||||||
|
let mut names: Vec<String> = container
|
||||||
|
.names
|
||||||
|
.as_ref()
|
||||||
|
.map(|names| {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.map(|name| name.trim_start_matches('/').to_owned())
|
||||||
.collect()
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or(Vec::new());
|
||||||
|
|
||||||
|
match result.get_mut(&reference) {
|
||||||
|
Some(containers) => containers.append(&mut names),
|
||||||
|
None => {
|
||||||
|
let _ = result.insert(reference, names);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
result.clone()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ pub struct Image {
|
|||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
pub digest_info: Option<DigestInfo>,
|
pub digest_info: Option<DigestInfo>,
|
||||||
pub version_info: Option<VersionInfo>,
|
pub version_info: Option<VersionInfo>,
|
||||||
pub in_use: bool,
|
pub used_by: Vec<String>,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
pub time_ms: u32,
|
pub time_ms: u32,
|
||||||
}
|
}
|
||||||
@@ -222,7 +222,7 @@ impl Image {
|
|||||||
},
|
},
|
||||||
time: self.time_ms,
|
time: self.time_ms,
|
||||||
server: None,
|
server: None,
|
||||||
in_use: self.in_use,
|
used_by: self.used_by.clone(),
|
||||||
status: has_update,
|
status: has_update,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ pub struct Update {
|
|||||||
pub result: UpdateResult,
|
pub result: UpdateResult,
|
||||||
pub time: u32,
|
pub time: u32,
|
||||||
pub server: Option<String>,
|
pub server: Option<String>,
|
||||||
pub in_use: bool,
|
pub used_by: Vec<String>,
|
||||||
#[serde(skip_serializing, skip_deserializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
pub status: Status,
|
pub status: Status,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ function App() {
|
|||||||
<Server name={server} key={server}>
|
<Server name={server} key={server}>
|
||||||
{images
|
{images
|
||||||
.filter((image) =>
|
.filter((image) =>
|
||||||
filters.onlyInUse ? !!image.in_use : true,
|
filters.onlyInUse ? image.used_by.length > 0 : true,
|
||||||
)
|
)
|
||||||
.filter((image) =>
|
.filter((image) =>
|
||||||
filters.registries.length == 0
|
filters.registries.length == 0
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import {
|
|||||||
DialogBackdrop,
|
DialogBackdrop,
|
||||||
DialogPanel,
|
DialogPanel,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
Disclosure,
|
||||||
|
DisclosureButton,
|
||||||
|
DisclosurePanel,
|
||||||
} from "@headlessui/react";
|
} from "@headlessui/react";
|
||||||
import { WithTooltip } from "./ui/Tooltip";
|
import { WithTooltip } from "./ui/Tooltip";
|
||||||
import type { Image } from "../types";
|
import type { Image } from "../types";
|
||||||
@@ -11,15 +14,17 @@ import { theme } from "../theme";
|
|||||||
import { CodeBlock } from "./CodeBlock";
|
import { CodeBlock } from "./CodeBlock";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
ChevronDown,
|
||||||
CircleArrowUp,
|
CircleArrowUp,
|
||||||
CircleCheck,
|
CircleCheck,
|
||||||
|
Container,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
Timer,
|
Timer,
|
||||||
TriangleAlert,
|
TriangleAlert,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Badge from "./Badge";
|
import Badge from "./Badge";
|
||||||
import { getDescription } from "../utils";
|
import { cn, getDescription, truncateArray } from "../utils";
|
||||||
|
|
||||||
const clickable_registries = [
|
const clickable_registries = [
|
||||||
"registry-1.docker.io",
|
"registry-1.docker.io",
|
||||||
@@ -30,12 +35,16 @@ const clickable_registries = [
|
|||||||
|
|
||||||
export default function Image({ data }: { data: Image }) {
|
export default function Image({ data }: { data: Image }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [showUsedBy, setShowUsedBy] = useState(false);
|
||||||
const handleOpen = () => {
|
const handleOpen = () => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
};
|
};
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
const toggleShowUsedBy = () => {
|
||||||
|
setShowUsedBy(!showUsedBy)
|
||||||
|
}
|
||||||
const new_reference =
|
const new_reference =
|
||||||
data.result.info?.type == "version"
|
data.result.info?.type == "version"
|
||||||
? data.reference.split(":")[0] + ":" + data.result.info.new_tag
|
? data.reference.split(":")[0] + ":" + data.result.info.new_tag
|
||||||
@@ -140,6 +149,21 @@ export default function Image({ data }: { data: Image }) {
|
|||||||
Checked in <b>{data.time}</b> ms
|
Checked in <b>{data.time}</b> ms
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{data.used_by.length !== 0 && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Container className="size-6 shrink-0 text-gray-500" />
|
||||||
|
<div className="flex items-start gap-1">
|
||||||
|
<span className="shrink-0">Used by</span>
|
||||||
|
{data.used_by.length > 1 ? (
|
||||||
|
<button onClick={toggleShowUsedBy} className={cn("flex gap-x-2 flex-wrap font-bold", !showUsedBy && "underline")}>
|
||||||
|
{showUsedBy ? data.used_by.map((container) => <><pre>{container}</pre></>) : truncateArray(data.used_by)}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
data.used_by[0]
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{data.result.error && (
|
{data.result.error && (
|
||||||
<div className="break-before mt-4 flex items-center gap-3 overflow-hidden rounded-md bg-yellow-400/10 px-3 py-2">
|
<div className="break-before mt-4 flex items-center gap-3 overflow-hidden rounded-md bg-yellow-400/10 px-3 py-2">
|
||||||
<TriangleAlert className="size-6 shrink-0 text-yellow-500" />
|
<TriangleAlert className="size-6 shrink-0 text-yellow-500" />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from "@headlessui/react";
|
} from "@headlessui/react";
|
||||||
import { ChevronDown, Check } from "lucide-react";
|
import { ChevronDown, Check } from "lucide-react";
|
||||||
import { theme } from "../../theme";
|
import { theme } from "../../theme";
|
||||||
import { cn } from "../../utils";
|
import { cn, truncateArray } from "../../utils";
|
||||||
import { Server } from "lucide-react";
|
import { Server } from "lucide-react";
|
||||||
|
|
||||||
export default function Select({
|
export default function Select({
|
||||||
@@ -27,7 +27,7 @@ export default function Select({
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex overflow-x-hidden w-full gap-2 rounded-md bg-${theme}-100 dark:bg-${theme}-900 border border-${theme}-200 dark:border-${theme}-700 group relative items-center py-1.5 pl-3 pr-2 text-left transition-colors duration-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-blue-500 sm:text-sm/6`,
|
`flex w-full gap-2 overflow-x-hidden rounded-md bg-${theme}-100 dark:bg-${theme}-900 border border-${theme}-200 dark:border-${theme}-700 group relative items-center py-1.5 pl-3 pr-2 text-left transition-colors duration-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-blue-500 sm:text-sm/6`,
|
||||||
selectedItems.length == 0
|
selectedItems.length == 0
|
||||||
? `text-${theme}-600 dark:text-${theme}-400 hover:text-black hover:dark:text-white`
|
? `text-${theme}-600 dark:text-${theme}-400 hover:text-black hover:dark:text-white`
|
||||||
: "text-black dark:text-white",
|
: "text-black dark:text-white",
|
||||||
@@ -46,13 +46,12 @@ export default function Select({
|
|||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{selectedItems.length == 0
|
{selectedItems.length == 0
|
||||||
? placeholder
|
? placeholder
|
||||||
: selectedItems.length == 1
|
: truncateArray(selectedItems)}
|
||||||
? selectedItems[0]
|
</span>
|
||||||
: `${selectedItems[0]} +${(selectedItems.length - 1).toString()} more`}</span>
|
|
||||||
|
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`size-5 shrink-0 ml-auto self-center text-${theme}-600 dark:text-${theme}-400 transition-colors duration-200 group-hover:text-black sm:size-4 group-hover:dark:text-white`}
|
className={`ml-auto size-5 shrink-0 self-center text-${theme}-600 dark:text-${theme}-400 transition-colors duration-200 group-hover:text-black sm:size-4 group-hover:dark:text-white`}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="absolute -bottom-px left-1/2 h-full w-0 -translate-x-1/2 rounded-md border-b-2 border-b-blue-600 transition-all duration-200 group-data-[open]:w-[calc(100%+2px)]"
|
className="absolute -bottom-px left-1/2 h-full w-0 -translate-x-1/2 rounded-md border-b-2 border-b-blue-600 transition-all duration-200 group-data-[open]:w-[calc(100%+2px)]"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export interface Image {
|
|||||||
};
|
};
|
||||||
time: number;
|
time: number;
|
||||||
server: string | null;
|
server: string | null;
|
||||||
in_use: boolean | null;
|
used_by: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VersionInfo {
|
interface VersionInfo {
|
||||||
|
|||||||
@@ -28,3 +28,11 @@ export function getDescription(image: Image) {
|
|||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function truncateArray(arr: string[]) {
|
||||||
|
if (arr.length > 1) {
|
||||||
|
return `${arr[0]} +${(arr.length - 1).toString()} more`
|
||||||
|
} else if (arr.length == 1) {
|
||||||
|
return arr[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user