m/cup
1
0
mirror of https://github.com/sergi0g/cup.git synced 2025-11-16 09:03:46 -05:00

Nearly complete versioning support. Fixed old bugs where not all tags were fetched.

This commit is contained in:
Sergio
2024-11-15 13:21:30 +02:00
parent c11b5e6432
commit d94abecf35
30 changed files with 1288 additions and 962 deletions

View File

@@ -67,30 +67,7 @@ export default function Image({ data }: { data: Image }) {
<li className="break-all text-start">
<IconCube className="size-6 shrink-0" />
{data.reference}
{data.result.has_update == false && (
<WithTooltip
text="Up to date"
className="ml-auto size-6 shrink-0 text-green-500"
>
<IconCircleCheckFilled />
</WithTooltip>
)}
{data.result.has_update == true && (
<WithTooltip
text="Update available"
className="ml-auto size-6 shrink-0 text-blue-500"
>
<IconCircleArrowUpFilled />
</WithTooltip>
)}
{data.result.has_update == null && (
<WithTooltip
text="Unknown"
className="ml-auto size-6 shrink-0 text-gray-500"
>
<IconHelpCircleFilled />
</WithTooltip>
)}
<Icon data={data} />
</li>
</button>
<Dialog open={open} onClose={setOpen} className="relative z-10">
@@ -133,24 +110,7 @@ export default function Image({ data }: { data: Image }) {
</button>
</div>
<div className="flex items-center gap-3">
{data.result.has_update == false && (
<>
<IconCircleCheckFilled className="size-6 shrink-0 text-green-500" />
Up to date
</>
)}
{data.result.has_update == true && (
<>
<IconCircleArrowUpFilled className="size-6 shrink-0 text-blue-500" />
Update available
</>
)}
{data.result.has_update == null && (
<>
<IconHelpCircleFilled className="size-6 shrink-0 text-gray-500" />
Unknown
</>
)}
<DialogIcon data={data} />
</div>
<div className="mb-4 flex items-center gap-3">
<IconStopwatch className="size-6 shrink-0 text-gray-500" />
@@ -171,7 +131,7 @@ export default function Image({ data }: { data: Image }) {
className={`bg-${theme}-100 dark:bg-${theme}-950 group relative mb-4 flex items-center rounded-md px-3 py-2 font-mono text-gray-500`}
>
<p className="overflow-scroll">
docker pull {data.reference}
docker pull {data.result.info?.type == "version" ? data.reference.replace(data.parts.tag, data.result.info.new_version) : data.reference}
</p>
{navigator.clipboard &&
(copySuccess ? (
@@ -180,7 +140,7 @@ export default function Image({ data }: { data: Image }) {
<button
className="duration-50 absolute right-3 opacity-0 transition-opacity group-hover:opacity-100"
onClick={handleCopy(
`docker pull ${data.reference}`,
`docker pull ${data.result.info?.type == "version" ? data.reference.replace(data.parts.tag, data.result.info.new_version) : data.reference}`,
)}
>
<IconCopy />
@@ -190,25 +150,33 @@ export default function Image({ data }: { data: Image }) {
</div>
)}
<div className="flex flex-col gap-1">
{data.local_digests.length > 1 ? "Local digests" : "Local digest"}
<div
className={`bg-${theme}-100 dark:bg-${theme}-950 scrollable rounded-md px-3 py-2 font-mono text-gray-500`}
>
<p className="overflow-x-scroll">
{data.local_digests.join("\n")}
</p>
</div>
{data.result.info?.type == "digest" && (
<>
{data.result.info.local_digests.length > 1
? "Local digests"
: "Local digest"}
<div
className={`bg-${theme}-100 dark:bg-${theme}-950 scrollable rounded-md px-3 py-2 font-mono text-gray-500`}
>
<p className="overflow-x-scroll">
{data.result.info.local_digests.join("\n")}
</p>
</div>
{data.result.info.remote_digest && (
<div className="flex flex-col gap-1">
Remote digest
<div
className={`bg-${theme}-100 dark:bg-${theme}-950 rounded-md px-3 py-2 font-mono text-gray-500`}
>
<p className="overflow-x-scroll">
{data.result.info.remote_digest}
</p>
</div>
</div>
)}
</>
)}
</div>
{data.remote_digest && (
<div className="flex flex-col gap-1">
Remote digest
<div
className={`bg-${theme}-100 dark:bg-${theme}-950 rounded-md px-3 py-2 font-mono text-gray-500`}
>
<p className="overflow-x-scroll">{data.remote_digest}</p>
</div>
</div>
)}
</div>
</DialogPanel>
</div>
@@ -217,3 +185,119 @@ export default function Image({ data }: { data: Image }) {
</>
);
}
function Icon({ data }: { data: Image }) {
switch (data.result.has_update) {
case null:
return (
<WithTooltip
text="Unknown"
className="ml-auto size-6 shrink-0 text-gray-500"
>
<IconHelpCircleFilled />
</WithTooltip>
);
case false:
return (
<WithTooltip
text="Up to date"
className="ml-auto size-6 shrink-0 text-green-500"
>
<IconCircleCheckFilled />
</WithTooltip>
);
case true:
if (data.result.info?.type === "version") {
switch (data.result.info.version_update_type) {
case "major":
return (
<WithTooltip
text="Major Update"
className="ml-auto size-6 shrink-0 text-red-500"
>
<IconCircleArrowUpFilled />
</WithTooltip>
);
case "minor":
return (
<WithTooltip
text="Minor Update"
className="ml-auto size-6 shrink-0 text-yellow-500"
>
<IconCircleArrowUpFilled />
</WithTooltip>
);
case "patch":
return (
<WithTooltip
text="Patch Update"
className="ml-auto size-6 shrink-0 text-blue-500"
>
<IconCircleArrowUpFilled />
</WithTooltip>
);
}
} else if (data.result.info?.type === "digest") {
return (
<WithTooltip
text="Update available"
className="ml-auto size-6 shrink-0 text-blue-500"
>
<IconCircleArrowUpFilled />
</WithTooltip>
);
}
}
}
function DialogIcon({ data }: { data: Image }) {
switch (data.result.has_update) {
case null:
return (
<>
<IconHelpCircleFilled className="size-6 shrink-0 text-gray-500" />
Unknown
</>
);
case false:
return (
<>
<IconCircleCheckFilled className="size-6 shrink-0 text-green-500" />
Up to date
</>
);
case true:
if (data.result.info?.type === "version") {
switch (data.result.info.version_update_type) {
case "major":
return (
<>
<IconCircleArrowUpFilled className="size-6 shrink-0 text-red-500" />
Major update
</>
);
case "minor":
return (
<>
<IconCircleArrowUpFilled className="size-6 shrink-0 text-yellow-500" />
Minor update
</>
);
case "patch":
return (
<>
<IconCircleArrowUpFilled className="size-6 shrink-0 text-blue-500" />
Patch update
</>
);
}
} else if (data.result.info?.type === "digest") {
return (
<>
<IconCircleArrowUpFilled className="size-6 shrink-0 text-blue-500" />
Update available
</>
);
}
}
}

View File

@@ -5,16 +5,25 @@ import {
IconHelpCircleFilled,
} from "@tabler/icons-react";
import { theme } from "../theme";
import { Data } from "../types";
const metricsToShow = [
"monitored_images",
"up_to_date",
"updates_available",
"unknown",
];
export default function Statistic({
name,
value,
metrics,
}: {
name: string;
value: number;
name: keyof Data["metrics"];
metrics: Data["metrics"];
}) {
name = name.replaceAll("_", " ");
name = name.slice(0, 1).toUpperCase() + name.slice(1); // Capitalize name
if (!metricsToShow.includes(name)) return null;
let displayName = name.replaceAll("_", " ");
displayName = displayName.slice(0, 1).toUpperCase() + displayName.slice(1); // Capitalize name
return (
<div
className={`before:bg-${theme}-200 before:dark:bg-${theme}-800 after:bg-${theme}-200 after:dark:bg-${theme}-800 gi`}
@@ -23,22 +32,20 @@ export default function Statistic({
<dt
className={`text-${theme}-500 dark:text-${theme}-400 font-medium leading-6`}
>
{name}
{displayName}
</dt>
<div className="flex items-center justify-between gap-1">
<dd className="w-full text-3xl font-medium leading-10 tracking-tight text-black dark:text-white">
{value}
{metrics[name]}
</dd>
{name == "Monitored images" && (
{name === "monitored_images" && (
<IconEyeFilled className="size-6 shrink-0 text-black dark:text-white" />
)}
{name == "Up to date" && (
{name === "up_to_date" && (
<IconCircleCheckFilled className="size-6 shrink-0 text-green-500" />
)}
{name == "Update available" && (
<IconCircleArrowUpFilled className="size-6 shrink-0 text-blue-500" />
)}
{name == "Unknown" && (
{name === "updates_available" && getUpdatesAvailableIcon(metrics)}
{name === "unknown" && (
<IconHelpCircleFilled className="size-6 shrink-0 text-gray-500" />
)}
</div>
@@ -46,3 +53,27 @@ export default function Statistic({
</div>
);
}
function getUpdatesAvailableIcon(metrics: Data["metrics"]) {
const filteredMetrics = Object.entries(metrics).filter(
([key]) => !metricsToShow.includes(key),
);
const maxMetric = filteredMetrics.reduce((max, current) => {
if (Number(current[1]) > Number(max[1])) {
return current;
}
return max;
}, filteredMetrics[0])[0];
let color = "";
switch (maxMetric) {
case "major_updates":
color = "text-red-500";
break;
case "minor_updates":
color = "text-yellow-500";
break;
default:
color = "text-blue-500";
}
return <IconCircleArrowUpFilled className={`size-6 shrink-0 ${color}`} />;
}