Proyecto LCX Dispatcharr multicuenta
Base Image Build / prepare (push) Has been cancelled
Build and Push Multi-Arch Docker Image / build-and-push (push) Has been cancelled
Frontend Tests / test (push) Has been cancelled
Base Image Build / docker (amd64, ubuntu-24.04) (push) Has been cancelled
Base Image Build / docker (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Base Image Build / create-manifest (push) Has been cancelled

This commit is contained in:
root
2026-05-09 21:24:50 +02:00
commit f56b088643
721 changed files with 177870 additions and 0 deletions
@@ -0,0 +1,769 @@
import React, { useState } from 'react';
import {
ActionIcon,
Avatar,
Badge,
Box,
Button,
Card,
Group,
Loader,
Modal,
Stack,
Switch,
Text,
Tooltip,
} from '@mantine/core';
import { AlertTriangle, Ban, Check, Download, FlaskConical, Info, RefreshCw, RotateCcw, ShieldAlert, ShieldCheck, Trash2 } from 'lucide-react';
import { useNavigate, useLocation } from 'react-router-dom';
import API from '../../api';
import { usePluginStore } from '../../store/plugins';
import PluginDetailPanel from '../PluginDetailPanel.jsx';
import { compareVersions } from '../pluginUtils.js';
const RepoBadge = ({ isOfficial, repoName, signatureVerified }) => {
if (isOfficial) {
const badge = (
<Badge
size="xs"
variant="filled"
style={{ backgroundColor: signatureVerified === false ? 'var(--mantine-color-red-9)' : '#14917E' }}
leftSection={signatureVerified != null ? (signatureVerified ? <ShieldCheck size={10} /> : <ShieldAlert size={10} />) : undefined}
>
Official Repo
</Badge>
);
return signatureVerified != null ? (
<Tooltip label={signatureVerified ? 'Verified Signature' : 'Invalid Signature'}>{badge}</Tooltip>
) : badge;
}
if (!repoName) return null;
const badge = (
<Badge
size="xs"
variant="filled"
color={signatureVerified === false ? 'red.9' : 'gray'}
leftSection={signatureVerified != null ? (signatureVerified ? <ShieldCheck size={10} /> : <ShieldAlert size={10} />) : undefined}
>
{repoName}
</Badge>
);
return signatureVerified != null ? (
<Tooltip label={signatureVerified ? 'Verified Signature' : 'Invalid Signature'}>{badge}</Tooltip>
) : badge;
};
const StatusBadge = ({ status, deprecated, isPrerelease, isLatestDowngrade, installedSourceRepoName }) => {
if (status === 'installed') {
const baseLabel = isPrerelease ? 'Prerelease' : 'Installed';
if (!deprecated) {
return (
<Badge size="xs" variant="light" color={isPrerelease ? 'violet' : 'green'} leftSection={isPrerelease ? <FlaskConical size={8} /> : <Check size={8} />}>
{baseLabel}
</Badge>
);
}
return (
<Tooltip label={`${isPrerelease ? 'Prerelease installed' : 'Installed'}, but this plugin has been deprecated by its maintainer`}>
<Badge size="xs" variant="light" color={isPrerelease ? 'red' : 'orange'} leftSection={<Ban size={8} />}>
{baseLabel} · Deprecated
</Badge>
</Tooltip>
);
}
if (status === 'update_available') {
const baseLabel = isLatestDowngrade ? 'Newer Installed' : 'Update Available';
if (!deprecated) {
return (
<Badge size="xs" variant="light" color={isLatestDowngrade ? 'orange' : 'yellow'} leftSection={isLatestDowngrade ? <AlertTriangle size={8} /> : <RefreshCw size={8} />}>
{baseLabel}
</Badge>
);
}
return (
<Tooltip label="Update available, but this plugin has been deprecated by its maintainer">
<Badge size="xs" variant="light" color="red" leftSection={<Ban size={8} />}>
{baseLabel} · Deprecated
</Badge>
</Tooltip>
);
}
if (status === 'unmanaged' || status === 'different_repo') {
const tooltip = status === 'unmanaged'
? (deprecated ? 'Installed manually (deprecated) - installing from this repo will take over management' : 'Installed manually - installing from this repo will take over management')
: `Managed by ${installedSourceRepoName || 'another repo'}${deprecated ? ' (deprecated)' : ''}`;
return (
<Tooltip label={tooltip}>
<Badge size="xs" variant="light" color={deprecated ? 'red' : 'orange'} leftSection={deprecated ? <Ban size={8} /> : <Check size={8} />}>
{deprecated ? 'Installed · Deprecated' : 'Installed'}
</Badge>
</Tooltip>
);
}
if (deprecated) {
return (
<Tooltip label="This plugin has been marked as deprecated by its maintainer">
<Badge size="xs" variant="light" color="red" leftSection={<Ban size={8} />}>
Deprecated
</Badge>
</Tooltip>
);
}
return null;
};
const AvailablePluginCard = ({ plugin, appVersion, multiRepo = false, autoOpenDetail = false, onDetailClose, onInstalled, onUninstalled, onBeforeInstall }) => {
const meetsMinVersion = !plugin.min_dispatcharr_version || compareVersions(appVersion, plugin.min_dispatcharr_version) >= 0;
const meetsMaxVersion = !plugin.max_dispatcharr_version || compareVersions(appVersion, plugin.max_dispatcharr_version) <= 0;
const meetsVersion = meetsMinVersion && meetsMaxVersion;
const [detailOpen, setDetailOpen] = useState(false);
const [detail, setDetail] = useState(null);
const [detailLoading, setDetailLoading] = useState(false);
const [selectedVersion, setSelectedVersion] = useState(null);
const [installing, setInstalling] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [restartPromptOpen, setRestartPromptOpen] = useState(false);
const [installAction, setInstallAction] = useState(null); // 'installed' | 'updated' | 'downgraded'
const [pendingInstall, setPendingInstall] = useState(null);
const [installedKey, setInstalledKey] = useState(null);
const [enableNow, setEnableNow] = useState(false);
const [enabling, setEnabling] = useState(false);
const [pluginIsDisabled, setPluginIsDisabled] = useState(false);
const [uninstallConfirmOpen, setUninstallConfirmOpen] = useState(false);
const [uninstalling, setUninstalling] = useState(false);
const [deprecationWarnOpen, setDeprecationWarnOpen] = useState(false);
const [pendingDeprecatedInstall, setPendingDeprecatedInstall] = useState(null);
const installPlugin = usePluginStore((s) => s.installPlugin);
const navigate = useNavigate();
const { pathname } = useLocation();
const onMyPlugins = pathname === '/plugins';
const isLatestDowngrade = plugin.install_status === 'update_available' &&
plugin.latest_version && plugin.installed_version &&
compareVersions(plugin.latest_version, plugin.installed_version) < 0;
const doInstall = (params) => {
if (plugin.deprecated) {
setPendingDeprecatedInstall(params);
setDeprecationWarnOpen(true);
return;
}
setPendingInstall(params);
setConfirmOpen(true);
};
const confirmDeprecatedInstall = () => {
setDeprecationWarnOpen(false);
if (pendingDeprecatedInstall) {
setPendingInstall(pendingDeprecatedInstall);
setPendingDeprecatedInstall(null);
setConfirmOpen(true);
}
};
const confirmAndInstall = () => {
setConfirmOpen(false);
if (pendingInstall) executeInstall(pendingInstall);
};
const executeInstall = async (params) => {
const wasInstalled = plugin.installed;
const wasDowngrade = plugin.installed_version && params.version &&
compareVersions(params.version, plugin.installed_version) < 0;
onBeforeInstall?.(plugin.slug);
setInstalling(true);
const result = await installPlugin(params);
setInstalling(false);
setPendingInstall(null);
if (result?.success) {
setInstallAction(wasDowngrade ? 'downgraded' : wasInstalled ? 'updated' : 'installed');
setInstalledKey(result.plugin?.key || params.slug);
setPluginIsDisabled(result.plugin?.enabled === false);
setEnableNow(false);
setRestartPromptOpen(true);
onInstalled?.(plugin.slug);
}
};
const [uninstallDoneOpen, setUninstallDoneOpen] = useState(false);
const handleDismissRestart = async (andNavigate = false) => {
if (enableNow && installedKey) {
setEnabling(true);
try {
await API.setPluginEnabled(installedKey, true);
} finally {
setEnabling(false);
}
}
setRestartPromptOpen(false);
if (andNavigate) navigate('/plugins');
};
const handleUninstall = async () => {
const key = plugin.key || installedKey;
if (!key) return;
setUninstalling(true);
try {
const resp = await API.deletePlugin(key);
if (resp?.success) {
onUninstalled?.(plugin.slug);
usePluginStore.getState().invalidatePlugins();
usePluginStore.getState().fetchAvailablePlugins();
setUninstallConfirmOpen(false);
setUninstallDoneOpen(true);
}
} finally {
setUninstalling(false);
}
};
const handleMoreInfo = async () => {
setDetailOpen(true);
if (detailLoading) return;
if (!plugin.manifest_url) {
// No per-plugin manifest — synthesize from top-level repo entry (latest only)
setDetail({
manifest: {
description: plugin.description,
author: plugin.author,
license: plugin.license,
versions: plugin.latest_version ? [{
version: plugin.latest_version,
url: plugin.latest_url,
checksum_sha256: plugin.latest_sha256,
min_dispatcharr_version: plugin.min_dispatcharr_version,
max_dispatcharr_version: plugin.max_dispatcharr_version,
build_timestamp: plugin.last_updated,
}] : [],
latest: plugin.latest_version ? { version: plugin.latest_version } : null,
},
signature_verified: plugin.signature_verified ?? null,
});
if (plugin.latest_version) setSelectedVersion(plugin.latest_version);
return;
}
setDetailLoading(true);
const result = await API.getPluginDetailManifest(plugin.repo_id, plugin.manifest_url);
if (result) {
setDetail(result);
if (result.manifest?.versions?.length) {
setSelectedVersion(result.manifest.versions[0].version);
}
}
setDetailLoading(false);
};
React.useEffect(() => {
if (autoOpenDetail) handleMoreInfo();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const latestInstallParams = {
repo_id: plugin.repo_id,
slug: plugin.slug,
version: plugin.latest_version,
download_url: plugin.latest_url,
sha256: plugin.latest_sha256,
min_dispatcharr_version: plugin.min_dispatcharr_version,
max_dispatcharr_version: plugin.max_dispatcharr_version,
};
return (
<Card
shadow="sm"
radius="md"
withBorder
style={{
display: 'flex',
flexDirection: 'column',
minHeight: 220,
backgroundColor: '#27272A',
...(multiRepo && plugin.is_official_repo ? { borderColor: '#0e6459' } : {}),
}}
>
<Group justify="space-between" mb="xs" align="flex-start" wrap="nowrap">
<Group gap="sm" align="flex-start" wrap="nowrap" style={{ minWidth: 0, flex: 1 }}>
<Avatar
src={plugin.icon_url}
radius="sm"
size={48}
alt={`${plugin.name} logo`}
>
{plugin.name?.[0]?.toUpperCase()}
</Avatar>
<Box style={{ minWidth: 0, flex: 1 }}>
<Text fw={600} lineClamp={1}>
{plugin.name}
</Text>
<Group gap={6} align="center" wrap="nowrap">
{plugin.author && (
<Text size="xs" c="dimmed" truncate style={{ minWidth: 0, maxWidth: '100%' }}>
{plugin.author}
</Text>
)}
<StatusBadge
status={plugin.install_status}
deprecated={plugin.deprecated}
isPrerelease={plugin.installed_version_is_prerelease}
isLatestDowngrade={isLatestDowngrade}
installedSourceRepoName={plugin.installed_source_repo_name}
/>
</Group>
</Box>
</Group>
<Group gap={4} wrap="nowrap" style={{ flexShrink: 0 }}>
<RepoBadge
isOfficial={plugin.is_official_repo}
repoName={plugin.repo_name}
signatureVerified={plugin.signature_verified}
/>
</Group>
</Group>
<Box style={{ flex: 1, minHeight: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<div style={{ overflow: 'hidden' }}>
<Text size="sm" c="dimmed" lineClamp={3} mb={0}>
{plugin.description}
</Text>
</div>
<Stack gap={2} mt="auto" pt={4} style={{ flexShrink: 0 }}>
<Group gap="xs" wrap="wrap">
{plugin.latest_version && (
<Badge size="xs" variant="default">
<span style={{ opacity: 0.5, marginRight: 4 }}>LATEST</span>
v{plugin.latest_version}
</Badge>
)}
{plugin.license && (
<Badge
size="xs"
variant="default"
component="a"
href={`https://spdx.org/licenses/${encodeURIComponent(plugin.license)}.html`}
target="_blank"
rel="noopener noreferrer"
style={{ cursor: 'pointer' }}
>
<span style={{ opacity: 0.5, marginRight: 4 }}>LICENSE</span>
{plugin.license}
</Badge>
)}
{plugin.min_dispatcharr_version && (
<Badge size="xs" variant="default">
<span style={{ opacity: 0.5, marginRight: 4 }}>MIN</span>
{plugin.min_dispatcharr_version}
</Badge>
)}
{plugin.max_dispatcharr_version && (
<Badge size="xs" variant="default">
<span style={{ opacity: 0.5, marginRight: 4 }}>MAX</span>
{plugin.max_dispatcharr_version}
</Badge>
)}
{plugin.last_updated && (
<Badge size="xs" variant="default">
<span style={{ opacity: 0.5, marginRight: 4 }}>UPDATED</span>
{new Date(plugin.last_updated).toLocaleDateString()}
</Badge>
)}
</Group>
</Stack>
</Box>
<Group justify="space-between" mt="sm" align="center" wrap="nowrap">
{!meetsVersion && (() => {
const parts = [];
if (!meetsMinVersion) parts.push(`${plugin.min_dispatcharr_version} or newer`);
if (!meetsMaxVersion) parts.push(`${plugin.max_dispatcharr_version} or older`);
const label = !meetsMinVersion
? `Min ${plugin.min_dispatcharr_version}`
: `Max ${plugin.max_dispatcharr_version}`;
return (
<Tooltip label={`Incompatible: requires Dispatcharr ${parts.join(' and ')} (you have v${appVersion})`}>
<Group gap={4} align="center" wrap="nowrap">
<AlertTriangle size={14} color="var(--mantine-color-yellow-6)" />
<Text size="xs" c="yellow">{label}</Text>
</Group>
</Tooltip>
);
})()}
{meetsVersion && <span />}
<Group gap="xs" wrap="nowrap">
<Button
size="xs"
variant="default"
leftSection={<Info size={14} />}
onClick={handleMoreInfo}
>
More Info
</Button>
{(plugin.install_status === 'unmanaged') && plugin.latest_version && plugin.latest_url && (
<Tooltip label="Installed manually - installing from this repo will take over management">
<Button
size="xs"
variant="filled"
color="orange"
leftSection={installing ? <Loader size={14} /> : <Download size={14} />}
disabled={!meetsVersion || installing}
onClick={() => doInstall(latestInstallParams)}
>
{installing ? 'Installing...' : 'Overwrite'}
</Button>
</Tooltip>
)}
{(plugin.install_status === 'different_repo') && plugin.latest_url && (
<Tooltip label={`Managed by ${plugin.installed_source_repo_name || 'another repo'} - installing will transfer management to this repo`}>
<Button
size="xs"
variant="filled"
color="orange"
leftSection={installing ? <Loader size={14} /> : <Download size={14} />}
disabled={!meetsVersion || installing}
onClick={() => doInstall(latestInstallParams)}
>
{installing ? 'Installing...' : 'Overwrite'}
</Button>
</Tooltip>
)}
{(plugin.install_status === 'installed') && (
<Button
size="xs"
variant="light"
color="red"
leftSection={<Trash2 size={14} />}
onClick={() => setUninstallConfirmOpen(true)}
>
Uninstall
</Button>
)}
{(plugin.install_status === 'update_available') && (
<Button
size="xs"
variant="filled"
color={isLatestDowngrade ? 'orange' : 'yellow'}
leftSection={installing ? <Loader size={14} /> : isLatestDowngrade ? <AlertTriangle size={14} /> : <RefreshCw size={14} />}
disabled={!meetsVersion || installing}
onClick={() => doInstall(latestInstallParams)}
>
{installing
? (isLatestDowngrade ? 'Downgrading...' : 'Updating...')
: (isLatestDowngrade ? 'Downgrade' : 'Update')}
</Button>
)}
{(!plugin.install_status || plugin.install_status === 'not_installed') && plugin.latest_url && (
<Button
size="xs"
variant="filled"
leftSection={installing ? <Loader size={14} /> : <Download size={14} />}
disabled={!meetsVersion || installing}
onClick={() => doInstall(latestInstallParams)}
>
{installing ? 'Installing...' : 'Install'}
</Button>
)}
</Group>
</Group>
{/* Detail Modal */}
<Modal
opened={detailOpen}
onClose={() => { setDetailOpen(false); onDetailClose?.(); }}
title={
<Group gap="xs" align="center">
<Avatar
src={plugin.icon_url}
radius="sm"
size={28}
alt={`${plugin.name} logo`}
>
{plugin.name?.[0]?.toUpperCase()}
</Avatar>
<Text fw={600}>{plugin.name}</Text>
<RepoBadge
isOfficial={plugin.is_official_repo}
repoName={plugin.repo_name}
signatureVerified={detail?.signature_verified ?? plugin.signature_verified}
/>
</Group>
}
size="lg"
>
<PluginDetailPanel
detail={detail}
detailLoading={detailLoading}
selectedVersion={selectedVersion}
onVersionChange={setSelectedVersion}
installedVersion={plugin.installed_version}
installedVersionIsPrerelease={!!plugin.installed_version_is_prerelease}
appVersion={appVersion}
installing={installing}
uninstalling={uninstalling}
onInstall={doInstall}
onUninstall={() => setUninstallConfirmOpen(true)}
installStatus={plugin.install_status}
installedSourceRepoName={plugin.installed_source_repo_name}
repoId={plugin.repo_id}
slug={plugin.slug}
/>
</Modal>
{/* Deprecation warning modal */}
<Modal
opened={deprecationWarnOpen}
onClose={() => { setDeprecationWarnOpen(false); setPendingDeprecatedInstall(null); }}
zIndex={300}
title={
<Group gap="xs" align="center">
<Ban size={18} color="var(--mantine-color-red-6)" />
<Text fw={600}>Deprecated Plugin</Text>
</Group>
}
size="sm"
>
<Stack gap="md">
<Text size="sm">
<b>{plugin.name}</b> has been marked as <b>deprecated</b> by its maintainer.
</Text>
<Text size="sm" c="dimmed">
Deprecated plugins may no longer receive updates or fixes, and could stop working with future
versions of Dispatcharr. It is recommended to look for an alternative.
</Text>
<Text size="sm" fw={500}>Do you still want to proceed?</Text>
<Group justify="flex-end" gap="xs">
<Button
size="xs"
variant="default"
onClick={() => { setDeprecationWarnOpen(false); setPendingDeprecatedInstall(null); }}
>
Cancel
</Button>
<Button
size="xs"
color="red"
leftSection={<Ban size={14} />}
onClick={confirmDeprecatedInstall}
>
Install Anyway
</Button>
</Group>
</Stack>
</Modal>
{/* Unified install confirmation modal */}
{(() => {
const isDowngrade = pendingInstall && plugin.installed_version &&
compareVersions(pendingInstall.version, plugin.installed_version) < 0;
const isUpdate = pendingInstall && plugin.installed_version &&
!isDowngrade &&
compareVersions(pendingInstall.version, plugin.installed_version) > 0;
const isBadSig = plugin.signature_verified === false;
const actionLabel = isDowngrade ? 'Downgrade' : isUpdate ? 'Update' : 'Install';
const btnColor = (isDowngrade && isBadSig) ? 'red' : isDowngrade ? 'orange' : isBadSig ? 'red' : undefined;
return (
<Modal
opened={confirmOpen}
onClose={() => { setConfirmOpen(false); setPendingInstall(null); }}
zIndex={300}
title={
<Group gap="xs" align="center">
{isBadSig
? <ShieldAlert size={18} color="var(--mantine-color-red-6)" />
: isDowngrade
? <AlertTriangle size={18} color="var(--mantine-color-orange-6)" />
: <Download size={18} />}
<Text fw={600}>Confirm {actionLabel}</Text>
</Group>
}
size="sm"
>
<Stack gap="md">
<Text size="sm">
You are about to {actionLabel.toLowerCase()} <b>{plugin.name}</b>{' '}
{isUpdate || isDowngrade
? <>from <b>v{plugin.installed_version}</b> to <b>v{pendingInstall?.version}</b></>
: <><b>v{pendingInstall?.version}</b></>}
{plugin.repo_name ? <> from <b>{plugin.repo_name}</b></> : ''}.
</Text>
<Text size="sm" c="dimmed">
Plugins run server-side code with full access to your Dispatcharr instance and its
data. Only install plugins from developers you trust. Malicious plugins could read
or modify data, call internal APIs, or perform unwanted actions.
</Text>
{isDowngrade && (
<Text size="sm" c="orange">
<b>Warning:</b> Downgrading may cause issues with saved settings or data.
</Text>
)}
{isBadSig && (
<Text size="sm" c="red">
<b>Warning:</b> This repository has an invalid or unverified signature.
Installing plugins from unverified sources may be risky.
</Text>
)}
{plugin.install_status === 'unmanaged' && (
<Text size="sm" c="orange">
<b>Note:</b> This plugin was installed manually. Installing from this repo
will bring it under repo management and enable future update checks.
</Text>
)}
{plugin.install_status === 'different_repo' && (
<Text size="sm" c="orange">
<b>Note:</b> This plugin is currently managed
by <b>{plugin.installed_source_repo_name || 'another repo'}</b>.
Installing will transfer management to this repo.
</Text>
)}
<Text size="sm" fw={500}>Are you sure you want to proceed?</Text>
<Group justify="flex-end" gap="xs">
<Button
size="xs"
variant="default"
onClick={() => { setConfirmOpen(false); setPendingInstall(null); }}
>
Cancel
</Button>
<Button
size="xs"
color={btnColor}
onClick={confirmAndInstall}
>
{actionLabel}
</Button>
</Group>
</Stack>
</Modal>
);
})()}
{/* Uninstall confirmation modal */}
<Modal
opened={uninstallConfirmOpen}
onClose={() => setUninstallConfirmOpen(false)}
zIndex={300}
title={
<Group gap="xs" align="center">
<Trash2 size={18} color="var(--mantine-color-red-6)" />
<Text fw={600}>Uninstall Plugin</Text>
</Group>
}
size="sm"
>
<Stack gap="md">
<Text size="sm">
Are you sure you want to uninstall <b>{plugin.name}</b>? This will
remove the plugin files and all associated settings.
</Text>
<Group justify="flex-end" gap="xs">
<Button
size="xs"
variant="default"
onClick={() => setUninstallConfirmOpen(false)}
>
Cancel
</Button>
<Button
size="xs"
color="red"
loading={uninstalling}
onClick={handleUninstall}
>
Uninstall
</Button>
</Group>
</Stack>
</Modal>
{/* Post-uninstall notice */}
<Modal
opened={uninstallDoneOpen}
onClose={() => setUninstallDoneOpen(false)}
zIndex={300}
title={
<Group gap="xs" align="center">
<Trash2 size={18} color="var(--mantine-color-green-6)" />
<Text fw={600}>Plugin Uninstalled</Text>
</Group>
}
size="sm"
>
<Stack gap="md">
<Text size="sm">
<b>{plugin.name}</b> has been uninstalled successfully.
</Text>
<Text size="sm">
A restart of Dispatcharr may be required to fully unload the plugin.
</Text>
<Group justify="flex-end">
<Button size="xs" variant="default" onClick={() => setUninstallDoneOpen(false)}>
Done
</Button>
</Group>
</Stack>
</Modal>
{/* Post-install restart prompt */}
<Modal
opened={restartPromptOpen}
onClose={() => setRestartPromptOpen(false)}
zIndex={300}
title={
<Group gap="xs" align="center">
<RotateCcw size={18} color="var(--mantine-color-blue-6)" />
<Text fw={600}>
Plugin {installAction === 'installed' ? 'Installed' : installAction === 'downgraded' ? 'Downgraded' : 'Updated'}
</Text>
</Group>
}
size="sm"
>
<Stack gap="md">
<Text size="sm">
<b>{plugin.name}</b> has been {installAction || 'installed'} successfully.
</Text>
<Text size="sm">
A restart of Dispatcharr may be required for the plugin to be fully loaded.
</Text>
{pluginIsDisabled && (
<>
<Text size="xs" c="dimmed">
This plugin is currently disabled. You can enable it now or at any time from My Plugins.
</Text>
<Group justify="space-between" align="center">
<Text size="sm">Enable plugin</Text>
<Switch
size="sm"
checked={enableNow}
onChange={(e) => setEnableNow(e.currentTarget.checked)}
/>
</Group>
</>
)}
<Group justify="flex-end" gap="xs">
<Button
size="xs"
variant="default"
loading={enabling}
onClick={() => handleDismissRestart(false)}
>
Done
</Button>
{!onMyPlugins && (
<Button
size="xs"
loading={enabling}
onClick={() => handleDismissRestart(true)}
>
Go to My Plugins
</Button>
)}
</Group>
</Stack>
</Modal>
</Card>
);
};
export default AvailablePluginCard;