import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { BookOpen, Bug, Check, Database, Download, ExternalLink, FlaskConical, Lightbulb, Pin, RotateCcw, Server, Settings, Trash2, TriangleAlert, } from "lucide-react"; import { Button } from "@/components/button"; import { Card } from "@/components/card"; import { Facts, PANEL } from "@/components/panel-layout"; import { FakeSwitch, SectionCard } from "@/components/section-card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/select"; import { getPluginData, type PluginDataSummary } from "@/api/plugins-api"; import { BetaBadge, CapabilityRow, DownloadCount, PluginIconBox, SourceBadge, UnverifiedBadge, } from "./plugin-bits"; import { betaFeedbackUrl, betaToTry, describeContributions, formatBytes, orderByRisk, reportIssueUrl, requestFeatureUrl, type PluginEntry, } from "./plugin-model"; import { openPluginSettings } from "./open-plugins"; import { hasSettingsPage } from "@/settings/settings-fields-util"; import { PluginReleaseNotes, PluginVideo } from "./PluginReleaseNotes"; import type { PluginsManager } from "./use-plugins-manager"; const LINK_ROW = "flex items-center gap-2 border border-border bg-card px-3 py-2.5 text-left text-[11px] text-muted-foreground transition-colors hover:border-accent-brand/40 hover:text-foreground"; export function PluginDetail({ plugin, manager, onUninstalled, }: { plugin: PluginEntry; manager: PluginsManager; onUninstalled: () => void; }) { const { t } = useTranslation(); const busy = manager.busy.has(plugin.id) || manager.busy.has("*"); const locked = manager.managedByServer; const running = plugin.status === "running"; const failed = plugin.status === "failed" || plugin.status === "blocked"; const compatible = plugin.versions.filter((v) => v.compatible); const [picked, setPicked] = useState( plugin.version ?? plugin.latestVersion ?? "", ); useEffect(() => { setPicked(plugin.version ?? plugin.latestVersion ?? ""); }, [plugin.version, plugin.latestVersion]); const contributions = describeContributions(plugin.contributes); // Only people who can manage plugins get here, and they see admin settings const hasSettings = plugin.installed && hasSettingsPage(plugin as Parameters[0], true); const issueUrl = reportIssueUrl( plugin, import.meta.env.VITE_APP_VERSION || undefined, ); const featureUrl = requestFeatureUrl(plugin); const feedbackUrl = plugin.isBeta ? betaFeedbackUrl(plugin, import.meta.env.VITE_APP_VERSION || undefined) : null; const tryableBeta = betaToTry(plugin); return (

{plugin.name}

{plugin.description} {plugin.unverified && } {plugin.isBeta && } {plugin.author && {plugin.author}} {plugin.version ? ( v{plugin.version} ) : ( plugin.latestVersion && v{plugin.latestVersion} )} {plugin.category && {plugin.category}} {plugin.pinnedVersion && ( {t("plugins.manager.pinnedTo", { version: plugin.pinnedVersion, })} )}
{plugin.installed && plugin.updateAvailable && !plugin.pinnedVersion && ( )} {tryableBeta && ( )} {plugin.installed ? ( <> {failed ? ( ) : null} ) : ( )}
{plugin.installed && failed && plugin.lastError && (
{plugin.status === "blocked" ? t("plugins.manager.status.blocked") : t("plugins.manager.status.failed")} {plugin.lastError}
)} {plugin.status === "server" && (
{t("plugins.manager.runsOnServerHint")}
)}
{plugin.features.length > 0 && (
    {plugin.features.map((feature) => (
  • {feature}
  • ))}
)}
{orderByRisk(plugin.capabilities).map((capability) => ( ))} {plugin.capabilities.length === 0 && (
{t("plugins.manager.consent.none")}
)}
{plugin.env.length > 0 && (
{plugin.env.map((v) => (
{v.name} {v.required && ( {t("plugins.manager.envRequired")} )} {v.default !== undefined && ( {t("plugins.manager.envDefault", { value: v.default, })} )}
{v.description}
))}
)} {plugin.installed && ( manager.removeData(plugin)} /> )}
{contributions.length > 0 && (
{contributions.map((c) => ( {t(`plugins.manager.contributes.${c.kind}`, { count: c.count, })} ))}
{t("plugins.manager.addsToTermixHint")}
)} {plugin.inRegistry && compatible.length > 0 && (
{plugin.installed ? t("plugins.manager.versionPickHintInstalled") : t("plugins.manager.versionPickHint")}
)} {plugin.installed && (
{t("plugins.manager.autoUpdate")} {t("plugins.manager.autoUpdateHint")}
void manager.setOptions(plugin, { autoUpdate: v }) } />
{t("plugins.manager.beta.channel")} {plugin.channel === "stable" && plugin.isBeta ? t("plugins.manager.beta.waitingForStable", { version: plugin.version, }) : t("plugins.manager.beta.channelHint")}
void (v ? manager.tryBeta(plugin) : manager.leaveBeta(plugin)) } />
{t("plugins.manager.pin")} {t("plugins.manager.pinHint", { version: plugin.version, })}
void manager.setOptions(plugin, { pinned: v }) } />
)} {plugin.docs && ( {t("plugins.manager.documentation")} )} {hasSettings && ( )} {issueUrl && ( {plugin.source === "official" || plugin.source === "bundled" || !plugin.author ? t("plugins.manager.reportIssue") : t("plugins.manager.reportTo", { author: plugin.author })} )} {feedbackUrl && ( {t("plugins.manager.beta.sendFeedback")} )} {featureUrl && ( {t("plugins.manager.requestFeature")} )} {plugin.repository && ( {plugin.repository.replace(/^https?:\/\//, "")} )}
); } function DataCard({ plugin, disabled, onDelete, }: { plugin: PluginEntry; disabled: boolean; onDelete: () => Promise; }) { const { t } = useTranslation(); const [data, setData] = useState(null); const [error, setError] = useState(false); const [version, setVersion] = useState(0); useEffect(() => { let cancelled = false; setError(false); getPluginData(plugin.id) .then((summary) => { if (!cancelled) setData(summary); }) .catch(() => { if (!cancelled) setError(true); }); return () => { cancelled = true; }; }, [plugin.id, plugin.version, version]); const settings = data ? data.settings.admin + data.settings.user + data.settings.host : 0; const rows = data?.tables.reduce((sum, table) => sum + (table.rows ?? 0), 0); return ( } action={ } > {error ? (
{t("plugins.manager.data.loadFailed")}
) : !data ? (
{t("plugins.manager.loading")}
) : (
{data.tables.length > 0 && (
{data.tables.map((table) => (
{table.name} {table.rows === null ? "-" : t("plugins.manager.data.rows", { count: table.rows })}
))}
)}
{t("plugins.manager.data.migrations", { count: data.migrations.length, })} {t("plugins.manager.data.grants", { count: data.grants.length })}
)}
); } function Stat({ label, value, hint, }: { label: string; value: string; hint?: string; }) { return (
{label} {value} {hint && ( {hint} )}
); }