You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
450 lines
16 KiB
450 lines
16 KiB
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/solid';
|
|
import axios from 'axios';
|
|
import React, { useState } from 'react';
|
|
import { defineMessages, useIntl } from 'react-intl';
|
|
import useSWR, { mutate } from 'swr';
|
|
import type {
|
|
RadarrSettings,
|
|
SonarrSettings,
|
|
} from '../../../server/lib/settings';
|
|
import RadarrLogo from '../../assets/services/radarr.svg';
|
|
import SonarrLogo from '../../assets/services/sonarr.svg';
|
|
import globalMessages from '../../i18n/globalMessages';
|
|
import Alert from '../Common/Alert';
|
|
import Badge from '../Common/Badge';
|
|
import Button from '../Common/Button';
|
|
import LoadingSpinner from '../Common/LoadingSpinner';
|
|
import Modal from '../Common/Modal';
|
|
import PageTitle from '../Common/PageTitle';
|
|
import Transition from '../Transition';
|
|
import RadarrModal from './RadarrModal';
|
|
import SonarrModal from './SonarrModal';
|
|
|
|
const messages = defineMessages({
|
|
services: 'Services',
|
|
radarrsettings: 'Radarr Settings',
|
|
sonarrsettings: 'Sonarr Settings',
|
|
serviceSettingsDescription:
|
|
'Configure your {serverType} server(s) below. You can connect multiple {serverType} servers, but only two of them can be marked as defaults (one non-4K and one 4K). Administrators are able to override the server used to process new requests prior to approval.',
|
|
deleteserverconfirm: 'Are you sure you want to delete this server?',
|
|
ssl: 'SSL',
|
|
default: 'Default',
|
|
default4k: 'Default 4K',
|
|
is4k: '4K',
|
|
address: 'Address',
|
|
activeProfile: 'Active Profile',
|
|
addradarr: 'Add Radarr Server',
|
|
addsonarr: 'Add Sonarr Server',
|
|
noDefaultServer:
|
|
'At least one {serverType} server must be marked as default in order for {mediaType} requests to be processed.',
|
|
noDefaultNon4kServer:
|
|
'If you only have a single {serverType} server for both non-4K and 4K content (or if you only download 4K content), your {serverType} server should <strong>NOT</strong> be designated as a 4K server.',
|
|
noDefault4kServer:
|
|
'A 4K {serverType} server must be marked as default in order to enable users to submit 4K {mediaType} requests.',
|
|
mediaTypeMovie: 'movie',
|
|
mediaTypeSeries: 'series',
|
|
});
|
|
|
|
interface ServerInstanceProps {
|
|
name: string;
|
|
isDefault?: boolean;
|
|
is4k?: boolean;
|
|
hostname: string;
|
|
port: number;
|
|
isSSL?: boolean;
|
|
externalUrl?: string;
|
|
profileName: string;
|
|
isSonarr?: boolean;
|
|
onEdit: () => void;
|
|
onDelete: () => void;
|
|
}
|
|
|
|
const ServerInstance: React.FC<ServerInstanceProps> = ({
|
|
name,
|
|
hostname,
|
|
port,
|
|
profileName,
|
|
is4k = false,
|
|
isDefault = false,
|
|
isSSL = false,
|
|
isSonarr = false,
|
|
externalUrl,
|
|
onEdit,
|
|
onDelete,
|
|
}) => {
|
|
const intl = useIntl();
|
|
|
|
const internalUrl =
|
|
(isSSL ? 'https://' : 'http://') + hostname + ':' + String(port);
|
|
const serviceUrl = externalUrl ?? internalUrl;
|
|
|
|
return (
|
|
<li className="col-span-1 bg-gray-700 rounded-lg shadow">
|
|
<div className="flex items-center justify-between w-full p-6 space-x-6">
|
|
<div className="flex-1 truncate">
|
|
<div className="flex items-center mb-2 space-x-2">
|
|
<h3 className="font-medium leading-5 text-white truncate">
|
|
<a
|
|
href={serviceUrl}
|
|
className="transition duration-300 hover:underline hover:text-white"
|
|
>
|
|
{name}
|
|
</a>
|
|
</h3>
|
|
{isDefault && !is4k && (
|
|
<Badge>{intl.formatMessage(messages.default)}</Badge>
|
|
)}
|
|
{isDefault && is4k && (
|
|
<Badge>{intl.formatMessage(messages.default4k)}</Badge>
|
|
)}
|
|
{!isDefault && is4k && (
|
|
<Badge badgeType="warning">
|
|
{intl.formatMessage(messages.is4k)}
|
|
</Badge>
|
|
)}
|
|
{isSSL && (
|
|
<Badge badgeType="success">
|
|
{intl.formatMessage(messages.ssl)}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<p className="mt-1 text-sm leading-5 text-gray-300 truncate">
|
|
<span className="mr-2 font-bold">
|
|
{intl.formatMessage(messages.address)}
|
|
</span>
|
|
<a
|
|
href={internalUrl}
|
|
className="transition duration-300 hover:underline hover:text-white"
|
|
>
|
|
{internalUrl}
|
|
</a>
|
|
</p>
|
|
<p className="mt-1 text-sm leading-5 text-gray-300 truncate">
|
|
<span className="mr-2 font-bold">
|
|
{intl.formatMessage(messages.activeProfile)}
|
|
</span>
|
|
{profileName}
|
|
</p>
|
|
</div>
|
|
<a href={serviceUrl} className="opacity-50 hover:opacity-100">
|
|
{isSonarr ? (
|
|
<SonarrLogo className="flex-shrink-0 w-10 h-10" />
|
|
) : (
|
|
<RadarrLogo className="flex-shrink-0 w-10 h-10" />
|
|
)}
|
|
</a>
|
|
</div>
|
|
<div className="border-t border-gray-800">
|
|
<div className="flex -mt-px">
|
|
<div className="flex flex-1 w-0 border-r border-gray-800">
|
|
<button
|
|
onClick={() => onEdit()}
|
|
className="relative inline-flex items-center justify-center flex-1 w-0 py-4 -mr-px text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out border border-transparent rounded-bl-lg hover:text-white focus:outline-none focus:ring-blue focus:border-gray-500 focus:z-10"
|
|
>
|
|
<PencilIcon className="w-5 h-5 mr-2" />
|
|
<span>{intl.formatMessage(globalMessages.edit)}</span>
|
|
</button>
|
|
</div>
|
|
<div className="flex flex-1 w-0 -ml-px">
|
|
<button
|
|
onClick={() => onDelete()}
|
|
className="relative inline-flex items-center justify-center flex-1 w-0 py-4 text-sm font-medium leading-5 text-gray-200 transition duration-150 ease-in-out border border-transparent rounded-br-lg hover:text-white focus:outline-none focus:ring-blue focus:border-gray-500 focus:z-10"
|
|
>
|
|
<TrashIcon className="w-5 h-5 mr-2" />
|
|
<span>{intl.formatMessage(globalMessages.delete)}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
);
|
|
};
|
|
|
|
const SettingsServices: React.FC = () => {
|
|
const intl = useIntl();
|
|
const {
|
|
data: radarrData,
|
|
error: radarrError,
|
|
revalidate: revalidateRadarr,
|
|
} = useSWR<RadarrSettings[]>('/api/v1/settings/radarr');
|
|
const {
|
|
data: sonarrData,
|
|
error: sonarrError,
|
|
revalidate: revalidateSonarr,
|
|
} = useSWR<SonarrSettings[]>('/api/v1/settings/sonarr');
|
|
const [editRadarrModal, setEditRadarrModal] = useState<{
|
|
open: boolean;
|
|
radarr: RadarrSettings | null;
|
|
}>({
|
|
open: false,
|
|
radarr: null,
|
|
});
|
|
const [editSonarrModal, setEditSonarrModal] = useState<{
|
|
open: boolean;
|
|
sonarr: SonarrSettings | null;
|
|
}>({
|
|
open: false,
|
|
sonarr: null,
|
|
});
|
|
const [deleteServerModal, setDeleteServerModal] = useState<{
|
|
open: boolean;
|
|
type: 'radarr' | 'sonarr';
|
|
serverId: number | null;
|
|
}>({
|
|
open: false,
|
|
type: 'radarr',
|
|
serverId: null,
|
|
});
|
|
|
|
const deleteServer = async () => {
|
|
await axios.delete(
|
|
`/api/v1/settings/${deleteServerModal.type}/${deleteServerModal.serverId}`
|
|
);
|
|
setDeleteServerModal({ open: false, serverId: null, type: 'radarr' });
|
|
revalidateRadarr();
|
|
revalidateSonarr();
|
|
mutate('/api/v1/settings/public');
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<PageTitle
|
|
title={[
|
|
intl.formatMessage(messages.services),
|
|
intl.formatMessage(globalMessages.settings),
|
|
]}
|
|
/>
|
|
<div className="mb-6">
|
|
<h3 className="heading">
|
|
{intl.formatMessage(messages.radarrsettings)}
|
|
</h3>
|
|
<p className="description">
|
|
{intl.formatMessage(messages.serviceSettingsDescription, {
|
|
serverType: 'Radarr',
|
|
})}
|
|
</p>
|
|
</div>
|
|
{editRadarrModal.open && (
|
|
<RadarrModal
|
|
radarr={editRadarrModal.radarr}
|
|
onClose={() => setEditRadarrModal({ open: false, radarr: null })}
|
|
onSave={() => {
|
|
revalidateRadarr();
|
|
mutate('/api/v1/settings/public');
|
|
setEditRadarrModal({ open: false, radarr: null });
|
|
}}
|
|
/>
|
|
)}
|
|
{editSonarrModal.open && (
|
|
<SonarrModal
|
|
sonarr={editSonarrModal.sonarr}
|
|
onClose={() => setEditSonarrModal({ open: false, sonarr: null })}
|
|
onSave={() => {
|
|
revalidateSonarr();
|
|
mutate('/api/v1/settings/public');
|
|
setEditSonarrModal({ open: false, sonarr: null });
|
|
}}
|
|
/>
|
|
)}
|
|
<Transition
|
|
show={deleteServerModal.open}
|
|
enter="transition ease-in-out duration-300 transform opacity-0"
|
|
enterFrom="opacity-0"
|
|
enterTo="opacuty-100"
|
|
leave="transition ease-in-out duration-300 transform opacity-100"
|
|
leaveFrom="opacity-100"
|
|
leaveTo="opacity-0"
|
|
>
|
|
<Modal
|
|
okText="Delete"
|
|
okButtonType="danger"
|
|
onOk={() => deleteServer()}
|
|
onCancel={() =>
|
|
setDeleteServerModal({
|
|
open: false,
|
|
serverId: null,
|
|
type: 'radarr',
|
|
})
|
|
}
|
|
title="Delete Server"
|
|
iconSvg={<TrashIcon />}
|
|
>
|
|
{intl.formatMessage(messages.deleteserverconfirm)}
|
|
</Modal>
|
|
</Transition>
|
|
<div className="section">
|
|
{!radarrData && !radarrError && <LoadingSpinner />}
|
|
{radarrData && !radarrError && (
|
|
<>
|
|
{radarrData.length > 0 &&
|
|
(!radarrData.some((radarr) => radarr.isDefault) ? (
|
|
<Alert
|
|
title={intl.formatMessage(messages.noDefaultServer, {
|
|
serverType: 'Radarr',
|
|
mediaType: intl.formatMessage(messages.mediaTypeMovie),
|
|
})}
|
|
/>
|
|
) : !radarrData.some(
|
|
(radarr) => radarr.isDefault && !radarr.is4k
|
|
) ? (
|
|
<Alert
|
|
title={intl.formatMessage(messages.noDefaultNon4kServer, {
|
|
serverType: 'Radarr',
|
|
strong: function strong(msg) {
|
|
return (
|
|
<strong className="font-semibold text-yellow-100">
|
|
{msg}
|
|
</strong>
|
|
);
|
|
},
|
|
})}
|
|
/>
|
|
) : (
|
|
radarrData.some((radarr) => radarr.is4k) &&
|
|
!radarrData.some(
|
|
(radarr) => radarr.isDefault && radarr.is4k
|
|
) && (
|
|
<Alert
|
|
title={intl.formatMessage(messages.noDefault4kServer, {
|
|
serverType: 'Radarr',
|
|
mediaType: intl.formatMessage(messages.mediaTypeMovie),
|
|
})}
|
|
/>
|
|
)
|
|
))}
|
|
<ul className="grid max-w-6xl grid-cols-1 gap-6 lg:grid-cols-2 xl:grid-cols-3">
|
|
{radarrData.map((radarr) => (
|
|
<ServerInstance
|
|
key={`radarr-config-${radarr.id}`}
|
|
name={radarr.name}
|
|
hostname={radarr.hostname}
|
|
port={radarr.port}
|
|
profileName={radarr.activeProfileName}
|
|
isSSL={radarr.useSsl}
|
|
isDefault={radarr.isDefault}
|
|
is4k={radarr.is4k}
|
|
externalUrl={radarr.externalUrl}
|
|
onEdit={() => setEditRadarrModal({ open: true, radarr })}
|
|
onDelete={() =>
|
|
setDeleteServerModal({
|
|
open: true,
|
|
serverId: radarr.id,
|
|
type: 'radarr',
|
|
})
|
|
}
|
|
/>
|
|
))}
|
|
<li className="h-32 col-span-1 border-2 border-gray-400 border-dashed rounded-lg shadow sm:h-44">
|
|
<div className="flex items-center justify-center w-full h-full">
|
|
<Button
|
|
buttonType="ghost"
|
|
className="mt-3 mb-3"
|
|
onClick={() =>
|
|
setEditRadarrModal({ open: true, radarr: null })
|
|
}
|
|
>
|
|
<PlusIcon />
|
|
<span>{intl.formatMessage(messages.addradarr)}</span>
|
|
</Button>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</>
|
|
)}
|
|
</div>
|
|
<div className="mt-10 mb-6">
|
|
<h3 className="heading">
|
|
{intl.formatMessage(messages.sonarrsettings)}
|
|
</h3>
|
|
<p className="description">
|
|
{intl.formatMessage(messages.serviceSettingsDescription, {
|
|
serverType: 'Sonarr',
|
|
})}
|
|
</p>
|
|
</div>
|
|
<div className="section">
|
|
{!sonarrData && !sonarrError && <LoadingSpinner />}
|
|
{sonarrData && !sonarrError && (
|
|
<>
|
|
{sonarrData.length > 0 &&
|
|
(!sonarrData.some((sonarr) => sonarr.isDefault) ? (
|
|
<Alert
|
|
title={intl.formatMessage(messages.noDefaultServer, {
|
|
serverType: 'Sonarr',
|
|
mediaType: intl.formatMessage(messages.mediaTypeSeries),
|
|
})}
|
|
/>
|
|
) : !sonarrData.some(
|
|
(sonarr) => sonarr.isDefault && !sonarr.is4k
|
|
) ? (
|
|
<Alert
|
|
title={intl.formatMessage(messages.noDefaultNon4kServer, {
|
|
serverType: 'Sonarr',
|
|
strong: function strong(msg) {
|
|
return (
|
|
<strong className="font-semibold text-yellow-100">
|
|
{msg}
|
|
</strong>
|
|
);
|
|
},
|
|
})}
|
|
/>
|
|
) : (
|
|
sonarrData.some((sonarr) => sonarr.is4k) &&
|
|
!sonarrData.some(
|
|
(sonarr) => sonarr.isDefault && sonarr.is4k
|
|
) && (
|
|
<Alert
|
|
title={intl.formatMessage(messages.noDefault4kServer, {
|
|
serverType: 'Sonarr',
|
|
mediaType: intl.formatMessage(messages.mediaTypeSeries),
|
|
})}
|
|
/>
|
|
)
|
|
))}
|
|
<ul className="grid max-w-6xl grid-cols-1 gap-6 lg:grid-cols-2 xl:grid-cols-3">
|
|
{sonarrData.map((sonarr) => (
|
|
<ServerInstance
|
|
key={`sonarr-config-${sonarr.id}`}
|
|
name={sonarr.name}
|
|
hostname={sonarr.hostname}
|
|
port={sonarr.port}
|
|
profileName={sonarr.activeProfileName}
|
|
isSSL={sonarr.useSsl}
|
|
isSonarr
|
|
isDefault={sonarr.isDefault}
|
|
is4k={sonarr.is4k}
|
|
externalUrl={sonarr.externalUrl}
|
|
onEdit={() => setEditSonarrModal({ open: true, sonarr })}
|
|
onDelete={() =>
|
|
setDeleteServerModal({
|
|
open: true,
|
|
serverId: sonarr.id,
|
|
type: 'sonarr',
|
|
})
|
|
}
|
|
/>
|
|
))}
|
|
<li className="h-32 col-span-1 border-2 border-gray-400 border-dashed rounded-lg shadow sm:h-44">
|
|
<div className="flex items-center justify-center w-full h-full">
|
|
<Button
|
|
buttonType="ghost"
|
|
onClick={() =>
|
|
setEditSonarrModal({ open: true, sonarr: null })
|
|
}
|
|
>
|
|
<PlusIcon />
|
|
<span>{intl.formatMessage(messages.addsonarr)}</span>
|
|
</Button>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default SettingsServices;
|