import { useAtomValue } from "@effect/atom-react"; import type { EnvironmentId, PluginState } from "@t3tools/contracts"; import { ChevronUpIcon, RotateCwIcon, SlidersHorizontalIcon, Trash2Icon } from "lucide-react"; import { useState } from "react"; import { pluginContributionKey, pluginPartNotesAtom, pluginSettingsPanelAtom, pluginWebFailuresAtom, } from "../../../plugins/pluginRegistry"; import { pluginEnvironment } from "../../../state/plugins"; import { useAtomCommand } from "../../../state/use-atom-command"; import { Button } from "../../ui/button"; import { Collapsible, CollapsiblePanel } from "../../ui/collapsible"; import { Popover, PopoverClose, PopoverPopup, PopoverTrigger } from "../../ui/popover"; import { Switch } from "../../ui/switch"; import { SettingsRow } from "../settingsLayout"; import { PluginSettingsRows } from "./PluginSettingsRows"; const FAILURE_STAGES = { discovery: "Not found", duplicate: "Duplicate id", manifest: "Invalid manifest", compatibility: "Incompatible", load: "Failed to load", activate: "Failed to start", crash: "Stopped the server", skipped: "Not started", } as const; export function PluginRow({ environmentId, plugin, }: { readonly environmentId: EnvironmentId; readonly plugin: PluginState; }) { const canManage = useAtomValue(pluginEnvironment.setPluginEnabled.permissionAtom(environmentId)); const setEnabled = useAtomCommand(pluginEnvironment.setPluginEnabled, "plugin enabled"); const reload = useAtomCommand(pluginEnvironment.reloadPlugin, "plugin reload"); const uninstall = useAtomCommand(pluginEnvironment.uninstallPlugin, "plugin removal"); const [busy, setBusy] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); const customSettings = useAtomValue( pluginSettingsPanelAtom(pluginContributionKey(environmentId, plugin.id)), ); const webFailure = useAtomValue(pluginWebFailuresAtom(plugin.id)).get(environmentId); const partNotes = useAtomValue(pluginPartNotesAtom(plugin.id)).get(environmentId) ?? []; const hasSettings = customSettings !== undefined || plugin.hasSettings; const whileBusy = async (action: () => Promise) => { setBusy(true); await action(); setBusy(false); }; const unlink = plugin.source === "dev"; // Removing it would delete data the id's other folders use, so its failure says what to delete. const duplicate = plugin.failure?.stage === "duplicate"; const { failure, serverFailure, folder } = plugin; const problems = [ ...(failure === undefined ? [] : [`${FAILURE_STAGES[failure.stage]}: ${failure.message}`]), ...(serverFailure === undefined ? [] : [ `Failed while running: ${serverFailure}`, // Turning a plugin off clears its failures on the server, so this one came after. ...(plugin.enabled ? [] : ["Some of its code is still running. Restarting T3 Code stops it."]), ]), ...(webFailure === undefined ? [] : [`Web part failed: ${webFailure}`]), ]; const row = ( {plugin.description} {plugin.permissions.length > 0 ? ( Says it uses: {plugin.permissions.join(", ")}. T3 Code doesn't enforce this. ) : null} {plugin.unknownManifestKeys === undefined ? null : ( T3 Code ignores these keys in its t3-plugin.json:{" "} {plugin.unknownManifestKeys.join(", ")}. Check them for typos. )} {partNotes.map((note) => ( {note} ))} } status={ problems.length === 0 ? undefined : (
{problems.map((problem) => ( {problem} ))}
) } control={ <> {hasSettings ? ( ) : null} {duplicate ? null : ( whileBusy(() => uninstall({ environmentId, input: { pluginId: plugin.id } })) } /> )} void whileBusy(() => setEnabled({ environmentId, input: { pluginId: plugin.id, enabled } }), ) } /> } /> ); if (!hasSettings) return row; return ( <> {row} {/* `contents`, so the card's line between rows falls inside the panel and slides in with it. */}
); } /** The remove button and the popup that confirms it, or Unlink for a plugin loaded from a folder. */ function RemovePluginPopover({ name, unlink, folder, disabled, onRemove, }: { readonly name: string; readonly unlink: boolean; /** The folder unlinked, named in the popup. */ readonly folder: string | undefined; readonly disabled: boolean; readonly onRemove: () => Promise; }) { const label = unlink ? "Unlink" : "Remove"; return ( } >
{folder === undefined ? null : (

Stops loading {folder}. The folder stays.

)}

This also deletes its settings, secrets and stored data, and can't be undone.

}> Cancel
); }