mirror of
https://github.com/sergi0g/cup.git
synced 2025-11-11 22:53:48 -05:00
Various frontend improvements
This commit is contained in:
@@ -7,11 +7,8 @@ import {
|
|||||||
} from "@headlessui/react";
|
} from "@headlessui/react";
|
||||||
import {
|
import {
|
||||||
IconAlertTriangleFilled,
|
IconAlertTriangleFilled,
|
||||||
IconArrowUpRight,
|
|
||||||
IconCheck,
|
|
||||||
IconCircleArrowUpFilled,
|
IconCircleArrowUpFilled,
|
||||||
IconCircleCheckFilled,
|
IconCircleCheckFilled,
|
||||||
IconCopy,
|
|
||||||
IconCube,
|
IconCube,
|
||||||
IconHelpCircleFilled,
|
IconHelpCircleFilled,
|
||||||
IconStopwatch,
|
IconStopwatch,
|
||||||
@@ -20,6 +17,7 @@ import {
|
|||||||
import { WithTooltip } from "./Tooltip";
|
import { WithTooltip } from "./Tooltip";
|
||||||
import type { Image } from "../types";
|
import type { Image } from "../types";
|
||||||
import { theme } from "../theme";
|
import { theme } from "../theme";
|
||||||
|
import { PullCommand } from "./PullCommand";
|
||||||
|
|
||||||
const clickable_registries = [
|
const clickable_registries = [
|
||||||
"registry-1.docker.io",
|
"registry-1.docker.io",
|
||||||
@@ -30,23 +28,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 [copySuccess, setCopySuccess] = useState(false);
|
|
||||||
const handleOpen = () => {
|
const handleOpen = () => {
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
};
|
};
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
const handleCopy = (text: string) => {
|
const new_reference =
|
||||||
return () => {
|
data.result.info?.type == "version"
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
? data.reference.replace(data.parts.tag, data.result.info.new_version)
|
||||||
setCopySuccess(true);
|
: data.reference;
|
||||||
setTimeout(() => {
|
|
||||||
setCopySuccess(false);
|
|
||||||
}, 3000);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
var url: string | null = null;
|
var url: string | null = null;
|
||||||
if (clickable_registries.includes(data.parts.registry)) {
|
if (clickable_registries.includes(data.parts.registry)) {
|
||||||
switch (data.parts.registry) {
|
switch (data.parts.registry) {
|
||||||
@@ -93,12 +84,21 @@ export default function Image({ data }: { data: Image }) {
|
|||||||
href={url}
|
href={url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="after:relative after:bottom-[1px] after:left-0 after:block after:h-[2px] after:w-full after:scale-x-0 after:bg-black after:transition-transform after:duration-300 hover:after:scale-x-100 after:dark:bg-white"
|
className={`group flex w-fit items-center justify-center gap-1 text-black hover:underline dark:text-white`}
|
||||||
>
|
>
|
||||||
{data.reference}
|
<span>{data.reference}</span>
|
||||||
<span className="inline-flex align-text-top">
|
<svg
|
||||||
<IconArrowUpRight className="size-3 shrink-0" />
|
viewBox="0 0 12 12"
|
||||||
</span>
|
fill="none"
|
||||||
|
width="10px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="transition-all duration-100 group-hover:rotate-45"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M11 9.283V1H2.727v1.44h5.83L1 9.99 2.01 11l7.556-7.55v5.833H11Z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -119,35 +119,13 @@ export default function Image({ data }: { data: Image }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{data.result.error && (
|
{data.result.error && (
|
||||||
<div className="mb-4 flex items-center gap-3 overflow-hidden break-all rounded-md bg-yellow-400/10 px-3 py-2">
|
<div className="break-before mb-4 flex items-center gap-3 overflow-hidden rounded-md bg-yellow-400/10 px-3 py-2">
|
||||||
<IconAlertTriangleFilled className="size-5 shrink-0 text-yellow-500" />
|
<IconAlertTriangleFilled className="size-6 shrink-0 text-yellow-500" />
|
||||||
{data.result.error}
|
{data.result.error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{data.result.has_update && (
|
{data.result.has_update && (
|
||||||
<div className="flex flex-col gap-1">
|
<PullCommand reference={new_reference} />
|
||||||
Pull command
|
|
||||||
<div
|
|
||||||
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.result.info?.type == "version" ? data.reference.replace(data.parts.tag, data.result.info.new_version) : data.reference}
|
|
||||||
</p>
|
|
||||||
{navigator.clipboard &&
|
|
||||||
(copySuccess ? (
|
|
||||||
<IconCheck className="absolute right-3" />
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="duration-50 absolute right-3 opacity-0 transition-opacity group-hover:opacity-100"
|
|
||||||
onClick={handleCopy(
|
|
||||||
`docker pull ${data.result.info?.type == "version" ? data.reference.replace(data.parts.tag, data.result.info.new_version) : data.reference}`,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<IconCopy />
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{data.result.info?.type == "digest" && (
|
{data.result.info?.type == "digest" && (
|
||||||
@@ -156,7 +134,7 @@ export default function Image({ data }: { data: Image }) {
|
|||||||
? "Local digests"
|
? "Local digests"
|
||||||
: "Local digest"}
|
: "Local digest"}
|
||||||
<div
|
<div
|
||||||
className={`bg-${theme}-100 dark:bg-${theme}-950 scrollable rounded-md px-3 py-2 font-mono text-gray-500`}
|
className={`bg-${theme}-100 dark:bg-${theme}-950 scrollable rounded-md px-3 py-2 font-mono text-${theme}-500`}
|
||||||
>
|
>
|
||||||
<p className="overflow-x-scroll">
|
<p className="overflow-x-scroll">
|
||||||
{data.result.info.local_digests.join("\n")}
|
{data.result.info.local_digests.join("\n")}
|
||||||
@@ -166,7 +144,7 @@ export default function Image({ data }: { data: Image }) {
|
|||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
Remote digest
|
Remote digest
|
||||||
<div
|
<div
|
||||||
className={`bg-${theme}-100 dark:bg-${theme}-950 rounded-md px-3 py-2 font-mono text-gray-500`}
|
className={`bg-${theme}-100 dark:bg-${theme}-950 rounded-md px-3 py-2 font-mono text-${theme}-500`}
|
||||||
>
|
>
|
||||||
<p className="overflow-x-scroll">
|
<p className="overflow-x-scroll">
|
||||||
{data.result.info.remote_digest}
|
{data.result.info.remote_digest}
|
||||||
|
|||||||
38
web/src/components/PullCommand.tsx
Normal file
38
web/src/components/PullCommand.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { theme } from "../theme";
|
||||||
|
import { IconCheck, IconClipboard } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export function PullCommand({ reference }: { reference: string }) {
|
||||||
|
const [copySuccess, setCopySuccess] = useState(false);
|
||||||
|
const handleCopy = (text: string) => {
|
||||||
|
return () => {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
setCopySuccess(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopySuccess(false);
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
Pull command
|
||||||
|
<div
|
||||||
|
className={`w-full group relative flex items-center rounded-lg bg-${theme}-100 px-3 py-2 font-mono text-${theme}-700 dark:bg-${theme}-950 dark:text-${theme}-300`}
|
||||||
|
>
|
||||||
|
<p className="overflow-scroll">docker pull {reference}</p>
|
||||||
|
{navigator.clipboard &&
|
||||||
|
(copySuccess ? (
|
||||||
|
<IconCheck className="absolute right-3 size-7 bg-neutral-100 pl-2 dark:bg-neutral-950" />
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="duration-50 absolute right-3 opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
|
onClick={handleCopy(`docker pull ${reference}`)}
|
||||||
|
>
|
||||||
|
<IconClipboard className="size-5"/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ export default function RefreshButton() {
|
|||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
className="group-disabled:animate-spin"
|
className="shrink-0 group-disabled:animate-spin"
|
||||||
>
|
>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
<path d="M4,11A8.1,8.1 0 0 1 19.5,9M20,5v4h-4" />
|
<path d="M4,11A8.1,8.1 0 0 1 19.5,9M20,5v4h-4" />
|
||||||
|
|||||||
Reference in New Issue
Block a user