packages/devtools-ui/src/components/package-item.tsx
import React from "react";
import clsx from "clsx";
import {
PackageLatestVersionType,
PackageType,
} from "@refinedev/devtools-shared";
import semverDiff from "semver-diff";
import { UpdateIcon } from "./icons/update";
import { CheckIcon } from "./icons/check";
import { InfoIcon } from "./icons/info";
import { getLatestInfo } from "src/utils/packages";
type Props = {
item: PackageType;
blocked?: boolean;
onUpdate: (packageName: string) => Promise<boolean>;
onOutdated: (packageName: string) => void;
};
export const PackageItem = ({ item, blocked, onUpdate, onOutdated }: Props) => {
const [latestLoading, setLatestLoading] = React.useState(true);
const [latestData, setLatestData] =
React.useState<PackageLatestVersionType | null>(null);
React.useEffect(() => {
setLatestLoading(true);
getLatestInfo(item.name)
.then((data) => {
setLatestData(data);
})
.catch(() => 0)
.finally(() => {
setLatestLoading(false);
});
}, []);
const updateKind =
latestData && item.currentVersion && latestData.latestVersion
? semverDiff(item.currentVersion, latestData.latestVersion)
: undefined;
const hasUpdate = typeof updateKind !== "undefined";
const [status, setStatus] = React.useState<
"idle" | "updating" | "done" | "error"
>("idle");
const icon = React.useMemo(() => {
switch (status) {
case "updating":
return <UpdateIcon className="re-animate-spin" />;
case "done":
return <CheckIcon />;
case "error":
return <InfoIcon className="re-rotate-180" />;
case "idle":
return <UpdateIcon />;
}
}, [status]);
const statusText = React.useMemo(() => {
switch (status) {
case "updating":
return "Updating";
case "done":
return "Updated";
case "error":
return "Error";
case "idle":
return "Update";
}
}, [status]);
const updatePackage = React.useCallback(async () => {
if (status !== "idle") return;
try {
setStatus("updating");
const response = await onUpdate?.(item.name);
if (response) {
setStatus("done");
} else {
setStatus("error");
}
} catch (err) {
setStatus("error");
//
}
}, [item.name, status]);
React.useEffect(() => {
if (hasUpdate) {
onOutdated(item.name);
}
}, [hasUpdate]);
return (
<div
className={clsx(
"re-border",
"re-border-gray-700",
"re-rounded-lg",
"re-bg-gray-900",
hasUpdate && "re-bg-package-item-has-updates",
"re-flex",
"re-flex-col",
)}
>
<div
className={clsx(
"re-flex-1",
"re-px-4",
"re-pt-4",
"re-pb-6",
"re-flex",
"re-flex-col",
"re-gap-3",
"re-border-b",
"re-border-b-gray-700",
)}
>
<div
className={clsx("re-flex", "re-items-center", "re-justify-between")}
>
<div
className={clsx(
"re-text-base",
"re-leading-8",
"re-font-semibold",
"re-text-gray-300",
)}
>
{item.name}
</div>
{hasUpdate && (
<button
type="button"
disabled={(blocked && status === "idle") || status !== "idle"}
onClick={updatePackage}
className={clsx(
"re-py-2",
"re-pl-2",
"re-pr-3",
"re-rounded",
(status === "idle" || status === "updating") &&
"re-bg-alt-blue re-text-alt-blue",
status === "done" && "re-bg-alt-green re-text-alt-green",
status === "error" && "re-bg-alt-red re-text-alt-red",
"re-bg-opacity-[0.15]",
"re-text-xs",
"re-flex",
"re-items-center",
"re-flex-shrink-0",
"re-gap-2",
)}
>
{icon}
<span>{statusText}</span>
</button>
)}
{!hasUpdate && (
<div
className={clsx(
"re-text-gray-500",
"re-text-xs",
"re-flex",
"re-items-center",
"re-gap-2",
)}
>
{latestLoading ? (
<span className="re-block re-h-4 re-w-20 re-bg-gray-700 re-animate-pulse re-rounded-md" />
) : (
<>
<CheckIcon />
<span>Up to date</span>
</>
)}
</div>
)}
</div>
<div className={clsx("re-text-xs", "re-leading-5", "re-text-gray-400")}>
{item.description ?? ""}
</div>
</div>
<div
className={clsx(
"re-p-4",
"re-flex",
"re-flex-col",
"re-gap-3",
"re-flex-shrink-0",
)}
>
{hasUpdate && (
<div
className={clsx(
"re-text-alt-green",
"re-text-xs",
"re-leading-5",
"re-flex",
"re-items-center",
"re-flex-shrink-0",
"re-gap-2",
)}
>
<InfoIcon />
<span>
<span>{"There's a "}</span>
<span className="re-font-semibold">{updateKind}</span>
<span>{" release "}</span>
<span
className={clsx(
"re-py-1",
"re-px-2",
"re-rounded",
"re-bg-alt-green",
"re-bg-opacity-30",
"re-text-alt-green",
"re-font-mono",
"re-font-bold",
"re-leading-4",
)}
>
{latestData?.latestVersion
? `v${latestData.latestVersion}`
: ""}
</span>
</span>
</div>
)}
<div
className={clsx(
"re-flex",
"re-items-center",
"re-gap-2",
"re-text-xs",
"re-text-gray-500",
)}
>
{item.documentation && (
<>
<a
href={item.documentation}
rel="noopener noreferrer"
target="_blank"
>
documentation
</a>
<span className="re-font-black re-text-gray-600 re-text-base re-leading-4">
·
</span>
</>
)}
{item.changelog && (
<>
<a
href={item.changelog}
rel="noopener noreferrer"
target="_blank"
>
changelog
</a>
<span className="re-font-black re-text-gray-600 re-text-base re-leading-4">
·
</span>
</>
)}
<span
className={clsx(
"re-block",
"re-py-1",
"re-px-2",
"re-rounded",
"re-bg-gray-700",
"re-text-gray-400",
"re-font-mono",
"re-font-bold",
)}
>
v{item.currentVersion}
</span>
</div>
</div>
</div>
);
};