mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-20 16:51:10 +00:00
Slight component refactoring
This commit is contained in:
parent
18f7e8584a
commit
71f1c7d5e5
7 changed files with 146 additions and 239 deletions
|
|
@ -10,8 +10,8 @@ import {
|
|||
Text,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { BookOpen, Github, Heart, Users } from 'lucide-react';
|
||||
import { DiscordIcon } from './icons.jsx';
|
||||
import { BookOpen, Heart, Users } from 'lucide-react';
|
||||
import { DiscordIcon, GitHubIcon } from './icons.jsx';
|
||||
import logo from '../images/logo.png';
|
||||
import useSettingsStore from '../store/settings';
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ const AboutModal = ({ isOpen, onClose }) => {
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="default"
|
||||
leftSection={<Github size={15} />}
|
||||
leftSection={<GitHubIcon size={15} />}
|
||||
fullWidth
|
||||
>
|
||||
GitHub
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import {
|
|||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
TableTbody,
|
||||
TableTd,
|
||||
TableTr,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
|
|
@ -21,7 +24,11 @@ import {
|
|||
ShieldCheck,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { compareVersions } from './pluginUtils.js';
|
||||
import {
|
||||
buildCompatibilityTooltip,
|
||||
buildVersionSelectItems,
|
||||
compareVersions,
|
||||
} from '../utils/components/pluginUtils.js';
|
||||
import { formatKB } from '../utils/networkUtils.js';
|
||||
import { DiscordIcon, GitHubIcon } from './icons.jsx';
|
||||
|
||||
|
|
@ -294,42 +301,12 @@ const PluginDetailPanel = ({
|
|||
|
||||
{manifest.versions?.length > 0 &&
|
||||
(() => {
|
||||
const installedMissing =
|
||||
installedVersion &&
|
||||
!manifest.versions.some(
|
||||
(v) => compareVersions(v.version, installedVersion) === 0
|
||||
);
|
||||
const buildLabel = (v) =>
|
||||
`v${v.version}${v.prerelease ? ' (prerelease)' : ''}${v.version === manifest.latest?.version ? ' (latest)' : ''}${installedVersion && compareVersions(v.version, installedVersion) === 0 ? ' (installed)' : ''}`;
|
||||
|
||||
let versions = [...manifest.versions];
|
||||
if (installedVersionIsPrerelease) {
|
||||
const prereleases = versions.filter((v) => v.prerelease);
|
||||
const stable = versions.filter((v) => !v.prerelease);
|
||||
versions = [...prereleases, ...stable];
|
||||
}
|
||||
|
||||
const versionItems = versions.map((v) => ({
|
||||
value: v.version,
|
||||
label: buildLabel(v),
|
||||
disabled: false,
|
||||
}));
|
||||
if (installedMissing) {
|
||||
const ghostItem = {
|
||||
value: installedVersion,
|
||||
label: `v${installedVersion} (installed)`,
|
||||
disabled: true,
|
||||
};
|
||||
// Insert in sorted position (newest first, matching manifest order convention)
|
||||
const idx = versionItems.findIndex(
|
||||
(item) => compareVersions(installedVersion, item.value) > 0
|
||||
);
|
||||
if (idx === -1) {
|
||||
versionItems.push(ghostItem);
|
||||
} else {
|
||||
versionItems.splice(idx, 0, ghostItem);
|
||||
}
|
||||
}
|
||||
const versionItems = buildVersionSelectItems(
|
||||
manifest.versions,
|
||||
manifest.latest?.version,
|
||||
installedVersion,
|
||||
installedVersionIsPrerelease
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Group gap="xs" align="flex-end">
|
||||
|
|
@ -372,21 +349,17 @@ const PluginDetailPanel = ({
|
|||
selectedVersionData &&
|
||||
!isSelSame &&
|
||||
(() => {
|
||||
const parts = [];
|
||||
if (!selMeetsMin)
|
||||
parts.push(
|
||||
`${selectedVersionData.min_dispatcharr_version} or newer`
|
||||
);
|
||||
if (!selMeetsMax)
|
||||
parts.push(
|
||||
`${selectedVersionData.max_dispatcharr_version} or older`
|
||||
);
|
||||
const tooltip = buildCompatibilityTooltip(
|
||||
selMeetsMin,
|
||||
selectedVersionData,
|
||||
selMeetsMax
|
||||
);
|
||||
const label = !selMeetsMin
|
||||
? `Min ${selectedVersionData.min_dispatcharr_version}`
|
||||
: `Max ${selectedVersionData.max_dispatcharr_version}`;
|
||||
return (
|
||||
<Tooltip
|
||||
label={`Incompatible: requires Dispatcharr ${parts.join(' and ')} (you have v${appVersion})`}
|
||||
label={`Incompatible: requires Dispatcharr ${tooltip} (you have v${appVersion})`}
|
||||
>
|
||||
<Group gap={4} align="center" wrap="nowrap">
|
||||
<AlertTriangle
|
||||
|
|
@ -409,56 +382,56 @@ const PluginDetailPanel = ({
|
|||
highlightOnHover
|
||||
style={{ tableLayout: 'auto' }}
|
||||
>
|
||||
<Table.Tbody>
|
||||
<TableTbody>
|
||||
{selectedVersionData.build_timestamp && (
|
||||
<Table.Tr>
|
||||
<Table.Td fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||
<TableTr>
|
||||
<TableTd fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||
Built
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
{new Date(
|
||||
selectedVersionData.build_timestamp
|
||||
).toLocaleString()}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
{Number.isFinite(selectedVersionData.size) &&
|
||||
selectedVersionData.size > 0 && (
|
||||
<Table.Tr>
|
||||
<Table.Td fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||
<TableTr>
|
||||
<TableTd fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||
File Size
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
{formatKB(selectedVersionData.size)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
{selectedVersionData.min_dispatcharr_version && (
|
||||
<Table.Tr>
|
||||
<Table.Td fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||
<TableTr>
|
||||
<TableTd fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||
Min Version
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
{selectedVersionData.min_dispatcharr_version}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
{selectedVersionData.max_dispatcharr_version && (
|
||||
<Table.Tr>
|
||||
<Table.Td fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||
<TableTr>
|
||||
<TableTd fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||
Max Version
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
{selectedVersionData.max_dispatcharr_version}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
{selectedVersionData.commit_sha_short && (
|
||||
<Table.Tr>
|
||||
<Table.Td fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||
<TableTr>
|
||||
<TableTd fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||
Commit
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
{manifest.registry_url ? (
|
||||
<Text
|
||||
size="xs"
|
||||
|
|
@ -473,15 +446,15 @@ const PluginDetailPanel = ({
|
|||
) : (
|
||||
selectedVersionData.commit_sha_short
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
{selectedVersionData.url && (
|
||||
<Table.Tr>
|
||||
<Table.Td fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||
<TableTr>
|
||||
<TableTd fw={500} style={{ whiteSpace: 'nowrap' }}>
|
||||
Download
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
</TableTd>
|
||||
<TableTd>
|
||||
<Text
|
||||
size="xs"
|
||||
component="a"
|
||||
|
|
@ -492,10 +465,10 @@ const PluginDetailPanel = ({
|
|||
>
|
||||
{selectedVersionData.url.split('/').pop()}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</TableTbody>
|
||||
</Table>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
|
|
@ -25,16 +24,32 @@ import {
|
|||
SquarePlus,
|
||||
UploadCloud,
|
||||
} from 'lucide-react';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import API from '../../api';
|
||||
import ConfirmationDialog from '../ConfirmationDialog';
|
||||
import useLocalStorage from '../../hooks/useLocalStorage';
|
||||
import useWarningsStore from '../../store/warnings';
|
||||
import { CustomTable, useTable } from '../tables/CustomTable';
|
||||
import { validateCronExpression } from '../../utils/cronUtils';
|
||||
import ScheduleInput from '../forms/ScheduleInput';
|
||||
import { showNotification } from '../../utils/notificationUtils.js';
|
||||
import { formatBytes } from '../../utils/networkUtils.js';
|
||||
import {
|
||||
format,
|
||||
getDefaultTimeZone,
|
||||
useDateTimeFormat,
|
||||
} from '../../utils/dateTimeUtils.js';
|
||||
import {
|
||||
createBackup,
|
||||
DAYS_OF_WEEK,
|
||||
deleteBackup,
|
||||
downloadBackup,
|
||||
getBackupSchedule,
|
||||
listBackups,
|
||||
restoreBackup,
|
||||
to12Hour,
|
||||
to24Hour,
|
||||
updateBackupSchedule,
|
||||
uploadBackup,
|
||||
} from '../../utils/components/backups/BackupManagerUtils.js';
|
||||
|
||||
const RowActions = ({
|
||||
row,
|
||||
|
|
@ -81,58 +96,6 @@ const RowActions = ({
|
|||
);
|
||||
};
|
||||
|
||||
// Convert 24h time string to 12h format with period
|
||||
function to12Hour(time24) {
|
||||
if (!time24) return { time: '12:00', period: 'AM' };
|
||||
const [hours, minutes] = time24.split(':').map(Number);
|
||||
const period = hours >= 12 ? 'PM' : 'AM';
|
||||
const hours12 = hours % 12 || 12;
|
||||
return {
|
||||
time: `${hours12}:${String(minutes).padStart(2, '0')}`,
|
||||
period,
|
||||
};
|
||||
}
|
||||
|
||||
// Convert 12h time + period to 24h format
|
||||
function to24Hour(time12, period) {
|
||||
if (!time12) return '00:00';
|
||||
const [hours, minutes] = time12.split(':').map(Number);
|
||||
let hours24 = hours;
|
||||
if (period === 'PM' && hours !== 12) {
|
||||
hours24 = hours + 12;
|
||||
} else if (period === 'AM' && hours === 12) {
|
||||
hours24 = 0;
|
||||
}
|
||||
return `${String(hours24).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// Get default timezone (same as Settings page)
|
||||
function getDefaultTimeZone() {
|
||||
try {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
||||
} catch {
|
||||
return 'UTC';
|
||||
}
|
||||
}
|
||||
|
||||
const DAYS_OF_WEEK = [
|
||||
{ value: '0', label: 'Sunday' },
|
||||
{ value: '1', label: 'Monday' },
|
||||
{ value: '2', label: 'Tuesday' },
|
||||
{ value: '3', label: 'Wednesday' },
|
||||
{ value: '4', label: 'Thursday' },
|
||||
{ value: '5', label: 'Friday' },
|
||||
{ value: '6', label: 'Saturday' },
|
||||
];
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
export default function BackupManager() {
|
||||
const [backups, setBackups] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -147,18 +110,9 @@ export default function BackupManager() {
|
|||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
// Read user's preferences from settings
|
||||
const [timeFormat] = useLocalStorage('time-format', '12h');
|
||||
const [dateFormatSetting] = useLocalStorage('date-format', 'mdy');
|
||||
const { fullDateTimeFormat, timeFormatSetting } = useDateTimeFormat();
|
||||
const [userTimezone] = useLocalStorage('time-zone', getDefaultTimeZone());
|
||||
const is12Hour = timeFormat === '12h';
|
||||
|
||||
// Format date according to user preferences
|
||||
const formatDate = (dateString) => {
|
||||
const date = dayjs(dateString);
|
||||
const datePart = dateFormatSetting === 'mdy' ? 'MM/DD/YYYY' : 'DD/MM/YYYY';
|
||||
const timePart = is12Hour ? 'h:mm:ss A' : 'HH:mm:ss';
|
||||
return date.format(`${datePart}, ${timePart}`);
|
||||
};
|
||||
const is12Hour = timeFormatSetting === '12h';
|
||||
|
||||
// Warning suppression for confirmation dialogs
|
||||
const suppressWarning = useWarningsStore((s) => s.suppressWarning);
|
||||
|
|
@ -213,7 +167,7 @@ export default function BackupManager() {
|
|||
minSize: 180,
|
||||
cell: ({ cell }) => (
|
||||
<Text size="sm" style={{ whiteSpace: 'nowrap' }}>
|
||||
{formatDate(cell.getValue())}
|
||||
{format(cell.getValue(), fullDateTimeFormat)}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
|
|
@ -267,10 +221,10 @@ export default function BackupManager() {
|
|||
const loadBackups = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const backupList = await API.listBackups();
|
||||
const backupList = await listBackups();
|
||||
setBackups(backupList);
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
showNotification({
|
||||
title: 'Error',
|
||||
message: error?.message || 'Failed to load backups',
|
||||
color: 'red',
|
||||
|
|
@ -283,7 +237,7 @@ export default function BackupManager() {
|
|||
const loadSchedule = async () => {
|
||||
setScheduleLoading(true);
|
||||
try {
|
||||
const settings = await API.getBackupSchedule();
|
||||
const settings = await getBackupSchedule();
|
||||
|
||||
setSchedule(settings);
|
||||
setScheduleType(settings.cron_expression ? 'cron' : 'interval');
|
||||
|
|
@ -294,7 +248,7 @@ export default function BackupManager() {
|
|||
setTimePeriod(period);
|
||||
|
||||
setScheduleChanged(false);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Ignore errors on initial load - settings may not exist yet
|
||||
} finally {
|
||||
setScheduleLoading(false);
|
||||
|
|
@ -340,17 +294,17 @@ export default function BackupManager() {
|
|||
? schedule
|
||||
: { ...schedule, cron_expression: '' };
|
||||
|
||||
const updated = await API.updateBackupSchedule(scheduleToSave);
|
||||
const updated = await updateBackupSchedule(scheduleToSave);
|
||||
setSchedule(updated);
|
||||
setScheduleChanged(false);
|
||||
|
||||
notifications.show({
|
||||
showNotification({
|
||||
title: 'Success',
|
||||
message: 'Backup schedule saved',
|
||||
color: 'green',
|
||||
});
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
showNotification({
|
||||
title: 'Error',
|
||||
message: error?.message || 'Failed to save schedule',
|
||||
color: 'red',
|
||||
|
|
@ -363,15 +317,15 @@ export default function BackupManager() {
|
|||
const handleCreateBackup = async () => {
|
||||
setCreating(true);
|
||||
try {
|
||||
await API.createBackup();
|
||||
notifications.show({
|
||||
await createBackup();
|
||||
showNotification({
|
||||
title: 'Success',
|
||||
message: 'Backup created successfully',
|
||||
color: 'green',
|
||||
});
|
||||
await loadBackups();
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
showNotification({
|
||||
title: 'Error',
|
||||
message: error?.message || 'Failed to create backup',
|
||||
color: 'red',
|
||||
|
|
@ -384,14 +338,14 @@ export default function BackupManager() {
|
|||
const handleDownload = async (filename) => {
|
||||
setDownloading(filename);
|
||||
try {
|
||||
await API.downloadBackup(filename);
|
||||
notifications.show({
|
||||
await downloadBackup(filename);
|
||||
showNotification({
|
||||
title: 'Download Started',
|
||||
message: `Downloading ${filename}...`,
|
||||
color: 'blue',
|
||||
});
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
showNotification({
|
||||
title: 'Error',
|
||||
message: error?.message || 'Failed to download backup',
|
||||
color: 'red',
|
||||
|
|
@ -409,15 +363,15 @@ export default function BackupManager() {
|
|||
const handleDeleteConfirm = async () => {
|
||||
setDeleting(true);
|
||||
try {
|
||||
await API.deleteBackup(selectedBackup.name);
|
||||
notifications.show({
|
||||
await deleteBackup(selectedBackup.name);
|
||||
showNotification({
|
||||
title: 'Success',
|
||||
message: 'Backup deleted successfully',
|
||||
color: 'green',
|
||||
});
|
||||
await loadBackups();
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
showNotification({
|
||||
title: 'Error',
|
||||
message: error?.message || 'Failed to delete backup',
|
||||
color: 'red',
|
||||
|
|
@ -437,8 +391,8 @@ export default function BackupManager() {
|
|||
const handleRestoreConfirm = async () => {
|
||||
setRestoring(true);
|
||||
try {
|
||||
await API.restoreBackup(selectedBackup.name);
|
||||
notifications.show({
|
||||
await restoreBackup(selectedBackup.name);
|
||||
showNotification({
|
||||
title: 'Restore Complete',
|
||||
message:
|
||||
'Backup restored successfully. A restart is recommended to ensure all services are running against the restored data.',
|
||||
|
|
@ -446,7 +400,7 @@ export default function BackupManager() {
|
|||
});
|
||||
setTimeout(() => window.location.reload(), 4000);
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
showNotification({
|
||||
title: 'Error',
|
||||
message: error?.message || 'Failed to restore backup',
|
||||
color: 'red',
|
||||
|
|
@ -462,8 +416,8 @@ export default function BackupManager() {
|
|||
if (!uploadFile) return;
|
||||
|
||||
try {
|
||||
await API.uploadBackup(uploadFile);
|
||||
notifications.show({
|
||||
await uploadBackup(uploadFile);
|
||||
showNotification({
|
||||
title: 'Success',
|
||||
message: 'Backup uploaded successfully',
|
||||
color: 'green',
|
||||
|
|
@ -472,7 +426,7 @@ export default function BackupManager() {
|
|||
setUploadFile(null);
|
||||
await loadBackups();
|
||||
} catch (error) {
|
||||
notifications.show({
|
||||
showNotification({
|
||||
title: 'Error',
|
||||
message: error?.message || 'Failed to upload backup',
|
||||
color: 'red',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
ActionIcon,
|
||||
Avatar,
|
||||
Badge,
|
||||
Box,
|
||||
|
|
@ -33,8 +32,7 @@ import {
|
|||
PluginSecurityWarning,
|
||||
PluginSupportDisclaimer,
|
||||
} from '../PluginWarnings.jsx';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import API from '../../api';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { usePluginStore } from '../../store/plugins';
|
||||
import PluginDetailPanel from '../PluginDetailPanel.jsx';
|
||||
import {
|
||||
|
|
@ -43,6 +41,11 @@ import {
|
|||
getInstallInfo,
|
||||
} from '../../utils/components/pluginUtils.js';
|
||||
import SizedInstallButton from '../theme/SizedInstallButton.jsx';
|
||||
import {
|
||||
deletePluginByKey,
|
||||
getPluginDetailManifest,
|
||||
setPluginEnabled,
|
||||
} from '../../utils/pages/PluginsUtils.js';
|
||||
|
||||
const RepoBadge = ({ isOfficial, repoName, signatureVerified }) => {
|
||||
if (isOfficial) {
|
||||
|
|
@ -319,7 +322,7 @@ const AvailablePluginCard = ({
|
|||
if (enableNow && installedKey) {
|
||||
setEnabling(true);
|
||||
try {
|
||||
await API.setPluginEnabled(installedKey, true);
|
||||
await setPluginEnabled(installedKey, true);
|
||||
} finally {
|
||||
setEnabling(false);
|
||||
}
|
||||
|
|
@ -333,7 +336,7 @@ const AvailablePluginCard = ({
|
|||
if (!key) return;
|
||||
setUninstalling(true);
|
||||
try {
|
||||
const resp = await API.deletePlugin(key);
|
||||
const resp = await deletePluginByKey(key);
|
||||
if (resp?.success) {
|
||||
onUninstalled?.(plugin.slug);
|
||||
usePluginStore.getState().invalidatePlugins();
|
||||
|
|
@ -379,7 +382,7 @@ const AvailablePluginCard = ({
|
|||
return;
|
||||
}
|
||||
setDetailLoading(true);
|
||||
const result = await API.getPluginDetailManifest(
|
||||
const result = await getPluginDetailManifest(
|
||||
plugin.repo_id,
|
||||
plugin.manifest_url
|
||||
);
|
||||
|
|
@ -534,17 +537,17 @@ const AvailablePluginCard = ({
|
|||
<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 tooltip = buildCompatibilityTooltip(
|
||||
meetsMinVersion,
|
||||
plugin,
|
||||
meetsMaxVersion
|
||||
);
|
||||
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})`}
|
||||
label={`Incompatible: requires Dispatcharr ${tooltip} (you have v${appVersion})`}
|
||||
>
|
||||
<Group gap={4} align="center" wrap="nowrap">
|
||||
<AlertTriangle
|
||||
|
|
@ -775,16 +778,10 @@ const AvailablePluginCard = ({
|
|||
|
||||
{/* 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 { isDowngrade, isUpdate, isBadSig } = getInstallInfo(
|
||||
pendingInstall,
|
||||
plugin
|
||||
);
|
||||
const actionLabel = isDowngrade
|
||||
? 'Downgrade'
|
||||
: isUpdate
|
||||
|
|
|
|||
|
|
@ -51,12 +51,11 @@ const RedisStatus = ({ tls }) => {
|
|||
: 'The connection is encrypted but the server identity is not verified';
|
||||
}
|
||||
|
||||
let mtlsColor = 'gray';
|
||||
let mtlsTooltip = 'Mutual authentication is not active';
|
||||
if (enabled && mtls) {
|
||||
mtlsColor = 'green';
|
||||
mtlsTooltip = 'Both client and server verify each other with certificates';
|
||||
}
|
||||
const mtlsColor = enabled && mtls ? 'green' : 'gray';
|
||||
const mtlsTooltip =
|
||||
enabled && mtls
|
||||
? 'Both client and server verify each other with certificates'
|
||||
: 'Mutual authentication is not active';
|
||||
|
||||
return (
|
||||
<TlsServiceCard serviceName="Redis">
|
||||
|
|
@ -94,11 +93,10 @@ const PostgresStatus = ({ tls }) => {
|
|||
const sslMode = tls?.ssl_mode;
|
||||
const mtls = tls?.mtls ?? false;
|
||||
|
||||
const modeBadge = enabled && sslMode ? sslMode : 'Off';
|
||||
let modeColor = 'gray';
|
||||
let modeTooltip = 'Verification mode is not active — TLS is disabled';
|
||||
let modeBadge = 'Off';
|
||||
if (enabled && sslMode) {
|
||||
modeBadge = sslMode;
|
||||
if (sslMode === 'verify-full') {
|
||||
modeColor = 'green';
|
||||
modeTooltip = 'Server certificate and hostname are both verified';
|
||||
|
|
@ -112,12 +110,11 @@ const PostgresStatus = ({ tls }) => {
|
|||
}
|
||||
}
|
||||
|
||||
let mtlsColor = 'gray';
|
||||
let mtlsTooltip = 'Mutual authentication is not active';
|
||||
if (enabled && mtls) {
|
||||
mtlsColor = 'green';
|
||||
mtlsTooltip = 'Both client and server verify each other with certificates';
|
||||
}
|
||||
const mtlsColor = enabled && mtls ? 'green' : 'gray';
|
||||
const mtlsTooltip =
|
||||
enabled && mtls
|
||||
? 'Both client and server verify each other with certificates'
|
||||
: 'Mutual authentication is not active';
|
||||
|
||||
return (
|
||||
<TlsServiceCard serviceName="PostgreSQL">
|
||||
|
|
|
|||
|
|
@ -2,15 +2,7 @@ import useSettingsStore from '../../../store/settings.jsx';
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { updateSetting } from '../../../utils/pages/SettingsUtils.js';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Flex,
|
||||
NumberInput,
|
||||
Stack,
|
||||
TextInput,
|
||||
Checkbox,
|
||||
} from '@mantine/core';
|
||||
import { Alert, Button, Flex, Stack, Checkbox } from '@mantine/core';
|
||||
import { USER_LIMITS_OPTIONS } from '../../../constants.js';
|
||||
|
||||
const USER_LIMIT_DEFAULTS = Object.keys(USER_LIMITS_OPTIONS).reduce(
|
||||
|
|
@ -77,20 +69,14 @@ const UserLimitsForm = React.memo(({ active }) => {
|
|||
></Alert>
|
||||
)}
|
||||
|
||||
{Object.keys(USER_LIMITS_OPTIONS).reduce((acc, key) => {
|
||||
const option = USER_LIMITS_OPTIONS[key];
|
||||
acc.push(
|
||||
<Checkbox
|
||||
key={key}
|
||||
label={option.label}
|
||||
description={option.description}
|
||||
{...userLimitSettingsForm.getInputProps(key, {
|
||||
type: 'checkbox',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
return acc;
|
||||
}, [])}
|
||||
{Object.entries(USER_LIMITS_OPTIONS).map(([key, option]) => (
|
||||
<Checkbox
|
||||
key={key}
|
||||
label={option.label}
|
||||
description={option.description}
|
||||
{...userLimitSettingsForm.getInputProps(key, { type: 'checkbox' })}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Flex mih={50} gap="xs" justify="space-between" align="flex-end">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import useSettingsStore from '../store/settings.jsx';
|
|||
import AvailablePluginCard from '../components/cards/AvailablePluginCard.jsx';
|
||||
import { showNotification } from '../utils/notificationUtils.js';
|
||||
import { reloadPlugins } from '../utils/pages/PluginsUtils.js';
|
||||
import { compareVersions } from '../components/pluginUtils.js';
|
||||
import { compareVersions } from '../utils/components/pluginUtils.js';
|
||||
|
||||
export default function PluginBrowsePage() {
|
||||
const repos = usePluginStore((s) => s.repos);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue