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.
overseerr/src/components/Settings/SettingsServices.tsx

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-800 rounded-lg shadow ring-1 ring-gray-500">
<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-500">
<div className="flex -mt-px">
<div className="flex flex-1 w-0 border-r border-gray-500">
<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,
mutate: revalidateRadarr,
} = useSWR<RadarrSettings[]>('/api/v1/settings/radarr');
const {
data: sonarrData,
error: sonarrError,
mutate: 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-white">
{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-white">
{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;