mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 08:28:36 +00:00
281 lines
11 KiB
TypeScript
281 lines
11 KiB
TypeScript
import { useState } from "react";
|
|
import { ExternalLink, ShieldCheck, Trash2 } from "lucide-react";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Sheet, SheetContent, SheetDescription, SheetTitle } from "@/components/ui/sheet";
|
|
import type { ExtensionAction } from "@/lib/api";
|
|
import type { ExtensionDiagnosticInfo, ExtensionInfo } from "@/lib/types";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
import {
|
|
DetailPill,
|
|
DetailSection,
|
|
ExtensionMark,
|
|
MetaItem,
|
|
NamedItems,
|
|
RuntimeBadge,
|
|
StatusBadge,
|
|
} from "./extension-ui";
|
|
|
|
interface ExtensionDetailSheetProps {
|
|
extension: ExtensionInfo | null;
|
|
diagnostics: ExtensionDiagnosticInfo[];
|
|
busy: string | null;
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onAction: (
|
|
action: ExtensionAction,
|
|
values: Record<string, unknown>,
|
|
) => Promise<void>;
|
|
}
|
|
|
|
export function ExtensionDetailSheet({
|
|
extension,
|
|
diagnostics,
|
|
busy,
|
|
open,
|
|
onOpenChange,
|
|
onAction,
|
|
}: ExtensionDetailSheetProps) {
|
|
const { t } = useTranslation();
|
|
const [uninstallOpen, setUninstallOpen] = useState(false);
|
|
if (!extension) return null;
|
|
|
|
const external = extension.scope !== "builtin";
|
|
const requested = new Set(extension.requested_permissions);
|
|
const granted = new Set(extension.granted_permissions);
|
|
const allGranted = [...requested].every((permission) => granted.has(permission));
|
|
|
|
return (
|
|
<>
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
<SheetContent
|
|
side="right"
|
|
className="w-[min(36rem,calc(100vw-1rem))] max-w-none gap-0 overflow-hidden p-0 sm:max-w-none"
|
|
>
|
|
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-5">
|
|
<div className="flex items-start gap-3 pr-8">
|
|
<ExtensionMark runtime={extension.runtime} large />
|
|
<div className="min-w-0 flex-1">
|
|
<SheetTitle className="truncate text-[20px] font-semibold">
|
|
{extension.name}
|
|
</SheetTitle>
|
|
<SheetDescription className="mt-1 line-clamp-2 text-[13px]">
|
|
{extension.description || extension.id}
|
|
</SheetDescription>
|
|
<div className="mt-2 flex flex-wrap items-center gap-1.5">
|
|
<RuntimeBadge runtime={extension.runtime} />
|
|
<DetailPill>{extension.version}</DetailPill>
|
|
<StatusBadge extension={extension} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-7 space-y-6">
|
|
<DetailSection title={t("extensions.details.identity")}>
|
|
<dl className="grid grid-cols-2 gap-2">
|
|
<MetaItem label="ID" value={extension.id} />
|
|
<MetaItem
|
|
label={t("extensions.details.source")}
|
|
value={extension.source}
|
|
/>
|
|
<MetaItem
|
|
label={t("extensions.details.scope")}
|
|
value={extension.scope}
|
|
/>
|
|
<MetaItem
|
|
label={t("extensions.details.license")}
|
|
value={extension.license || "—"}
|
|
/>
|
|
</dl>
|
|
{extension.homepage ? (
|
|
<a
|
|
href={extension.homepage}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="mt-2 inline-flex items-center gap-1 text-[12px] text-link hover:underline"
|
|
>
|
|
{t("extensions.details.homepage")}
|
|
<ExternalLink className="h-3 w-3" aria-hidden />
|
|
</a>
|
|
) : null}
|
|
</DetailSection>
|
|
|
|
<NamedItems
|
|
title={t("extensions.details.contributions")}
|
|
rows={extension.contributions.map((item) => ({
|
|
name: item.name,
|
|
meta: item.kind,
|
|
}))}
|
|
/>
|
|
<NamedItems
|
|
title={t("extensions.details.dependencies")}
|
|
rows={extension.dependencies.map((item) => ({
|
|
name: item.name,
|
|
meta: `${item.kind}${item.specifier ? ` ${item.specifier}` : ""}`,
|
|
}))}
|
|
/>
|
|
|
|
<DetailSection title={t("extensions.details.permissions")}>
|
|
{extension.permissions.length ? (
|
|
<div className="space-y-2">
|
|
{extension.permissions.map((permission) => (
|
|
<div
|
|
key={permission.name}
|
|
className="flex items-start justify-between gap-3 rounded-[14px] bg-muted/35 px-3 py-2.5"
|
|
>
|
|
<div className="min-w-0">
|
|
<div className="text-[13px] font-medium text-foreground">
|
|
{permission.name}
|
|
</div>
|
|
{permission.reason ? (
|
|
<p className="mt-0.5 text-[12px] leading-5 text-muted-foreground">
|
|
{permission.reason}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
<span
|
|
className={cn(
|
|
"shrink-0 text-[11px]",
|
|
granted.has(permission.name)
|
|
? "text-emerald-600 dark:text-emerald-300"
|
|
: "text-muted-foreground",
|
|
)}
|
|
>
|
|
{granted.has(permission.name)
|
|
? t("extensions.permissionGranted")
|
|
: t("extensions.permissionPending")}
|
|
</span>
|
|
</div>
|
|
))}
|
|
{external ? (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={busy !== null}
|
|
onClick={() =>
|
|
void onAction("permissions", {
|
|
id: extension.id,
|
|
permissions: allGranted ? [] : [...requested],
|
|
})
|
|
}
|
|
className="rounded-full"
|
|
>
|
|
{allGranted
|
|
? t("extensions.revokePermissions")
|
|
: t("extensions.grantPermissions")}
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
) : (
|
|
<p className="text-[13px] text-muted-foreground">
|
|
{t("extensions.noPermissions")}
|
|
</p>
|
|
)}
|
|
</DetailSection>
|
|
|
|
{diagnostics.length ? (
|
|
<DetailSection title={t("extensions.details.diagnostics")}>
|
|
<div className="space-y-2">
|
|
{diagnostics.map((diagnostic, index) => (
|
|
<div
|
|
key={`${diagnostic.code}:${index}`}
|
|
className="rounded-[14px] bg-amber-500/10 px-3 py-2.5"
|
|
>
|
|
<div className="text-[12px] font-medium text-amber-700 dark:text-amber-300">
|
|
{diagnostic.code}
|
|
</div>
|
|
<p className="mt-0.5 text-[12px] leading-5 text-muted-foreground">
|
|
{diagnostic.message}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</DetailSection>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
{external ? (
|
|
<div className="flex flex-wrap items-center gap-2 border-t border-border/45 bg-background/95 px-5 py-4">
|
|
<Button
|
|
size="sm"
|
|
variant={extension.trusted ? "outline" : "default"}
|
|
disabled={busy !== null || (!extension.trusted && !allGranted)}
|
|
onClick={() =>
|
|
void onAction(extension.trusted ? "untrust" : "trust", {
|
|
id: extension.id,
|
|
})
|
|
}
|
|
className="rounded-full"
|
|
>
|
|
<ShieldCheck className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
|
{extension.trusted
|
|
? t("extensions.revokeTrust")
|
|
: t("extensions.trust")}
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
disabled={busy !== null || !extension.trusted}
|
|
onClick={() =>
|
|
void onAction(extension.enabled ? "disable" : "enable", {
|
|
id: extension.id,
|
|
})
|
|
}
|
|
className="rounded-full"
|
|
>
|
|
{extension.enabled
|
|
? t("extensions.disable")
|
|
: t("extensions.enable")}
|
|
</Button>
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
disabled={busy !== null}
|
|
aria-label={t("extensions.uninstall")}
|
|
title={t("extensions.uninstall")}
|
|
onClick={() => setUninstallOpen(true)}
|
|
className="ml-auto h-8 w-8 rounded-full text-muted-foreground hover:text-destructive"
|
|
>
|
|
<Trash2 className="h-4 w-4" aria-hidden />
|
|
</Button>
|
|
</div>
|
|
) : null}
|
|
</SheetContent>
|
|
</Sheet>
|
|
|
|
<AlertDialog open={uninstallOpen} onOpenChange={setUninstallOpen}>
|
|
<AlertDialogContent className="max-w-[26rem] rounded-[18px]">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>{t("extensions.uninstallTitle")}</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{t("extensions.uninstallDescription", { name: extension.name })}
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>{t("extensions.cancel")}</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => void onAction("uninstall", { id: extension.id })}
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
>
|
|
{t("extensions.uninstall")}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</>
|
|
);
|
|
}
|