Merge remote-tracking branch 'overseerr/develop' into develop

This commit is contained in:
notfakie
2022-09-01 18:11:15 +12:00
473 changed files with 15548 additions and 8433 deletions

View File

@@ -1,5 +1,5 @@
import { ClipboardCopyIcon } from '@heroicons/react/solid';
import React, { useEffect } from 'react';
import { useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useClipboard from 'react-use-clipboard';
@@ -8,7 +8,7 @@ const messages = defineMessages({
copied: 'Copied API key to clipboard.',
});
const CopyButton: React.FC<{ textToCopy: string }> = ({ textToCopy }) => {
const CopyButton = ({ textToCopy }: { textToCopy: string }) => {
const intl = useIntl();
const [isCopied, setCopied] = useClipboard(textToCopy, {
successDuration: 1000,

View File

@@ -1,5 +1,4 @@
import { CheckIcon, XIcon } from '@heroicons/react/solid';
import React from 'react';
interface LibraryItemProps {
isEnabled?: boolean;
@@ -7,11 +6,7 @@ interface LibraryItemProps {
onToggle: () => void;
}
const LibraryItem: React.FC<LibraryItemProps> = ({
isEnabled,
name,
onToggle,
}) => {
const LibraryItem = ({ isEnabled, name, onToggle }: LibraryItemProps) => {
return (
<li className="col-span-1 flex rounded-md shadow-sm">
<div className="flex flex-1 items-center justify-between truncate rounded-md border-t border-b border-r border-gray-700 bg-gray-600">

View File

@@ -1,16 +1,16 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import { BeakerIcon, SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import React, { useState } from 'react';
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import useSettings from '../../../hooks/useSettings';
import globalMessages from '../../../i18n/globalMessages';
import Button from '../../Common/Button';
import LoadingSpinner from '../../Common/LoadingSpinner';
import NotificationTypeSelector from '../../NotificationTypeSelector';
const messages = defineMessages({
agentenabled: 'Enable Agent',
@@ -29,7 +29,7 @@ const messages = defineMessages({
enableMentions: 'Enable Mentions',
});
const NotificationsDiscord: React.FC = () => {
const NotificationsDiscord = () => {
const intl = useIntl();
const settings = useSettings();
const { addToast, removeToast } = useToasts();
@@ -168,18 +168,16 @@ const NotificationsDiscord: React.FC = () => {
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.webhookUrlTip, {
DiscordWebhookLink: function DiscordWebhookLink(msg) {
return (
<a
href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
DiscordWebhookLink: (msg: React.ReactNode) => (
<a
href="https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
</span>
</label>
@@ -192,9 +190,11 @@ const NotificationsDiscord: React.FC = () => {
inputMode="url"
/>
</div>
{errors.webhookUrl && touched.webhookUrl && (
<div className="error">{errors.webhookUrl}</div>
)}
{errors.webhookUrl &&
touched.webhookUrl &&
typeof errors.webhookUrl === 'string' && (
<div className="error">{errors.webhookUrl}</div>
)}
</div>
</div>
<div className="form-row">
@@ -210,9 +210,11 @@ const NotificationsDiscord: React.FC = () => {
placeholder={settings.currentSettings.applicationTitle}
/>
</div>
{errors.botUsername && touched.botUsername && (
<div className="error">{errors.botUsername}</div>
)}
{errors.botUsername &&
touched.botUsername &&
typeof errors.botUsername === 'string' && (
<div className="error">{errors.botUsername}</div>
)}
</div>
</div>
<div className="form-row">
@@ -228,9 +230,11 @@ const NotificationsDiscord: React.FC = () => {
inputMode="url"
/>
</div>
{errors.botAvatarUrl && touched.botAvatarUrl && (
<div className="error">{errors.botAvatarUrl}</div>
)}
{errors.botAvatarUrl &&
touched.botAvatarUrl &&
typeof errors.botAvatarUrl === 'string' && (
<div className="error">{errors.botAvatarUrl}</div>
)}
</div>
</div>
<div className="form-row">

View File

@@ -1,16 +1,16 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import SettingsBadge from '@app/components/Settings/SettingsBadge';
import globalMessages from '@app/i18n/globalMessages';
import { BeakerIcon, SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import React, { useState } from 'react';
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr';
import * as Yup from 'yup';
import globalMessages from '../../../i18n/globalMessages';
import Badge from '../../Common/Badge';
import Button from '../../Common/Button';
import LoadingSpinner from '../../Common/LoadingSpinner';
import SensitiveInput from '../../Common/SensitiveInput';
const messages = defineMessages({
validationSmtpHostRequired: 'You must provide a valid hostname or IP address',
@@ -47,7 +47,7 @@ const messages = defineMessages({
validationPgpPassword: 'You must provide a PGP password',
});
export function OpenPgpLink(msg: string): JSX.Element {
export function OpenPgpLink(msg: React.ReactNode) {
return (
<a href="https://www.openpgp.org/" target="_blank" rel="noreferrer">
{msg}
@@ -55,7 +55,7 @@ export function OpenPgpLink(msg: string): JSX.Element {
);
}
const NotificationsEmail: React.FC = () => {
const NotificationsEmail = () => {
const intl = useIntl();
const { addToast, removeToast } = useToasts();
const [isTesting, setIsTesting] = useState(false);
@@ -280,9 +280,11 @@ const NotificationsEmail: React.FC = () => {
inputMode="email"
/>
</div>
{errors.emailFrom && touched.emailFrom && (
<div className="error">{errors.emailFrom}</div>
)}
{errors.emailFrom &&
touched.emailFrom &&
typeof errors.emailFrom === 'string' && (
<div className="error">{errors.emailFrom}</div>
)}
</div>
</div>
<div className="form-row">
@@ -299,9 +301,11 @@ const NotificationsEmail: React.FC = () => {
inputMode="url"
/>
</div>
{errors.smtpHost && touched.smtpHost && (
<div className="error">{errors.smtpHost}</div>
)}
{errors.smtpHost &&
touched.smtpHost &&
typeof errors.smtpHost === 'string' && (
<div className="error">{errors.smtpHost}</div>
)}
</div>
</div>
<div className="form-row">
@@ -317,9 +321,11 @@ const NotificationsEmail: React.FC = () => {
inputMode="numeric"
className="short"
/>
{errors.smtpPort && touched.smtpPort && (
<div className="error">{errors.smtpPort}</div>
)}
{errors.smtpPort &&
touched.smtpPort &&
typeof errors.smtpPort === 'string' && (
<div className="error">{errors.smtpPort}</div>
)}
</div>
</div>
<div className="form-row">
@@ -391,9 +397,7 @@ const NotificationsEmail: React.FC = () => {
<span className="mr-2">
{intl.formatMessage(messages.pgpPrivateKey)}
</span>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.advanced)}
</Badge>
<SettingsBadge badgeType="advanced" />
<span className="label-tip">
{intl.formatMessage(messages.pgpPrivateKeyTip, {
OpenPgpLink: OpenPgpLink,
@@ -411,9 +415,11 @@ const NotificationsEmail: React.FC = () => {
className="font-mono text-xs"
/>
</div>
{errors.pgpPrivateKey && touched.pgpPrivateKey && (
<div className="error">{errors.pgpPrivateKey}</div>
)}
{errors.pgpPrivateKey &&
touched.pgpPrivateKey &&
typeof errors.pgpPrivateKey === 'string' && (
<div className="error">{errors.pgpPrivateKey}</div>
)}
</div>
</div>
<div className="form-row">
@@ -421,9 +427,7 @@ const NotificationsEmail: React.FC = () => {
<span className="mr-2">
{intl.formatMessage(messages.pgpPassword)}
</span>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.advanced)}
</Badge>
<SettingsBadge badgeType="advanced" />
<span className="label-tip">
{intl.formatMessage(messages.pgpPasswordTip, {
OpenPgpLink: OpenPgpLink,
@@ -439,9 +443,11 @@ const NotificationsEmail: React.FC = () => {
autoComplete="one-time-code"
/>
</div>
{errors.pgpPassword && touched.pgpPassword && (
<div className="error">{errors.pgpPassword}</div>
)}
{errors.pgpPassword &&
touched.pgpPassword &&
typeof errors.pgpPassword === 'string' && (
<div className="error">{errors.pgpPassword}</div>
)}
</div>
</div>
<div className="actions">

View File

@@ -1,15 +1,15 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import globalMessages from '@app/i18n/globalMessages';
import { BeakerIcon, SaveIcon } from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import React, { useState } from 'react';
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
agentenabled: 'Enable Agent',
@@ -26,7 +26,7 @@ const messages = defineMessages({
validationTypes: 'You must select at least one notification type',
});
const NotificationsGotify: React.FC = () => {
const NotificationsGotify = () => {
const intl = useIntl();
const { addToast, removeToast } = useToasts();
const [isTesting, setIsTesting] = useState(false);
@@ -173,9 +173,11 @@ const NotificationsGotify: React.FC = () => {
<div className="form-input-field">
<Field id="url" name="url" type="text" />
</div>
{errors.url && touched.url && (
<div className="error">{errors.url}</div>
)}
{errors.url &&
touched.url &&
typeof errors.url === 'string' && (
<div className="error">{errors.url}</div>
)}
</div>
</div>
<div className="form-row">
@@ -187,9 +189,11 @@ const NotificationsGotify: React.FC = () => {
<div className="form-input-field">
<Field id="token" name="token" type="text" />
</div>
{errors.token && touched.token && (
<div className="error">{errors.token}</div>
)}
{errors.token &&
touched.token &&
typeof errors.token === 'string' && (
<div className="error">{errors.token}</div>
)}
</div>
</div>
<NotificationTypeSelector

View File

@@ -1,15 +1,15 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import globalMessages from '@app/i18n/globalMessages';
import { BeakerIcon, SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import React, { useState } from 'react';
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
agentenabled: 'Enable Agent',
@@ -27,7 +27,7 @@ const messages = defineMessages({
validationTypes: 'You must select at least one notification type',
});
const NotificationsLunaSea: React.FC = () => {
const NotificationsLunaSea = () => {
const intl = useIntl();
const { addToast, removeToast } = useToasts();
const [isTesting, setIsTesting] = useState(false);
@@ -155,18 +155,16 @@ const NotificationsLunaSea: React.FC = () => {
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.webhookUrlTip, {
LunaSeaLink: function LunaSeaLink(msg) {
return (
<a
href="https://docs.lunasea.app/lunasea/notifications/overseerr"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
LunaSeaLink: (msg: React.ReactNode) => (
<a
href="https://docs.lunasea.app/lunasea/notifications/overseerr"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
</span>
</label>
@@ -179,9 +177,11 @@ const NotificationsLunaSea: React.FC = () => {
inputMode="url"
/>
</div>
{errors.webhookUrl && touched.webhookUrl && (
<div className="error">{errors.webhookUrl}</div>
)}
{errors.webhookUrl &&
touched.webhookUrl &&
typeof errors.webhookUrl === 'string' && (
<div className="error">{errors.webhookUrl}</div>
)}
</div>
</div>
<div className="form-row">
@@ -189,9 +189,9 @@ const NotificationsLunaSea: React.FC = () => {
{intl.formatMessage(messages.profileName)}
<span className="label-tip">
{intl.formatMessage(messages.profileNameTip, {
code: function code(msg) {
return <code className="bg-opacity-50">{msg}</code>;
},
code: (msg: React.ReactNode) => (
<code className="bg-opacity-50">{msg}</code>
),
})}
</span>
</label>

View File

@@ -1,16 +1,16 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import globalMessages from '@app/i18n/globalMessages';
import { BeakerIcon, SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import React, { useState } from 'react';
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import SensitiveInput from '../../../Common/SensitiveInput';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
agentEnabled: 'Enable Agent',
@@ -28,7 +28,7 @@ const messages = defineMessages({
validationTypes: 'You must select at least one notification type',
});
const NotificationsPushbullet: React.FC = () => {
const NotificationsPushbullet = () => {
const intl = useIntl();
const { addToast, removeToast } = useToasts();
const [isTesting, setIsTesting] = useState(false);
@@ -154,20 +154,16 @@ const NotificationsPushbullet: React.FC = () => {
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.accessTokenTip, {
PushbulletSettingsLink: function PushbulletSettingsLink(
msg
) {
return (
<a
href="https://www.pushbullet.com/#settings/account"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
PushbulletSettingsLink: (msg: React.ReactNode) => (
<a
href="https://www.pushbullet.com/#settings/account"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
</span>
</label>
@@ -180,9 +176,11 @@ const NotificationsPushbullet: React.FC = () => {
autoComplete="one-time-code"
/>
</div>
{errors.accessToken && touched.accessToken && (
<div className="error">{errors.accessToken}</div>
)}
{errors.accessToken &&
touched.accessToken &&
typeof errors.accessToken === 'string' && (
<div className="error">{errors.accessToken}</div>
)}
</div>
</div>
<div className="form-row">

View File

@@ -1,15 +1,15 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import globalMessages from '@app/i18n/globalMessages';
import { BeakerIcon, SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import React, { useState } from 'react';
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
agentenabled: 'Enable Agent',
@@ -29,7 +29,7 @@ const messages = defineMessages({
validationTypes: 'You must select at least one notification type',
});
const NotificationsPushover: React.FC = () => {
const NotificationsPushover = () => {
const intl = useIntl();
const { addToast, removeToast } = useToasts();
const [isTesting, setIsTesting] = useState(false);
@@ -172,19 +172,16 @@ const NotificationsPushover: React.FC = () => {
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.accessTokenTip, {
ApplicationRegistrationLink:
function ApplicationRegistrationLink(msg) {
return (
<a
href="https://pushover.net/api#registration"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
ApplicationRegistrationLink: (msg: React.ReactNode) => (
<a
href="https://pushover.net/api#registration"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
</span>
</label>
@@ -192,9 +189,11 @@ const NotificationsPushover: React.FC = () => {
<div className="form-input-field">
<Field id="accessToken" name="accessToken" type="text" />
</div>
{errors.accessToken && touched.accessToken && (
<div className="error">{errors.accessToken}</div>
)}
{errors.accessToken &&
touched.accessToken &&
typeof errors.accessToken === 'string' && (
<div className="error">{errors.accessToken}</div>
)}
</div>
</div>
<div className="form-row">
@@ -203,18 +202,16 @@ const NotificationsPushover: React.FC = () => {
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.userTokenTip, {
UsersGroupsLink: function UsersGroupsLink(msg) {
return (
<a
href="https://pushover.net/api#identifiers"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
UsersGroupsLink: (msg: React.ReactNode) => (
<a
href="https://pushover.net/api#identifiers"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
</span>
</label>
@@ -222,9 +219,11 @@ const NotificationsPushover: React.FC = () => {
<div className="form-input-field">
<Field id="userToken" name="userToken" type="text" />
</div>
{errors.userToken && touched.userToken && (
<div className="error">{errors.userToken}</div>
)}
{errors.userToken &&
touched.userToken &&
typeof errors.userToken === 'string' && (
<div className="error">{errors.userToken}</div>
)}
</div>
</div>
<NotificationTypeSelector

View File

@@ -1,15 +1,15 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import globalMessages from '@app/i18n/globalMessages';
import { BeakerIcon, SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import React, { useState } from 'react';
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const messages = defineMessages({
agentenabled: 'Enable Agent',
@@ -25,7 +25,7 @@ const messages = defineMessages({
validationTypes: 'You must select at least one notification type',
});
const NotificationsSlack: React.FC = () => {
const NotificationsSlack = () => {
const intl = useIntl();
const { addToast, removeToast } = useToasts();
const [isTesting, setIsTesting] = useState(false);
@@ -150,18 +150,16 @@ const NotificationsSlack: React.FC = () => {
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.webhookUrlTip, {
WebhookLink: function WebhookLink(msg) {
return (
<a
href="https://my.slack.com/services/new/incoming-webhook/"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
WebhookLink: (msg: React.ReactNode) => (
<a
href="https://my.slack.com/services/new/incoming-webhook/"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
</span>
</label>
@@ -174,9 +172,11 @@ const NotificationsSlack: React.FC = () => {
inputMode="url"
/>
</div>
{errors.webhookUrl && touched.webhookUrl && (
<div className="error">{errors.webhookUrl}</div>
)}
{errors.webhookUrl &&
touched.webhookUrl &&
typeof errors.webhookUrl === 'string' && (
<div className="error">{errors.webhookUrl}</div>
)}
</div>
</div>
<NotificationTypeSelector

View File

@@ -1,16 +1,16 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import globalMessages from '@app/i18n/globalMessages';
import { BeakerIcon, SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import React, { useState } from 'react';
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import globalMessages from '../../../i18n/globalMessages';
import Button from '../../Common/Button';
import LoadingSpinner from '../../Common/LoadingSpinner';
import SensitiveInput from '../../Common/SensitiveInput';
import NotificationTypeSelector from '../../NotificationTypeSelector';
const messages = defineMessages({
agentenabled: 'Enable Agent',
@@ -34,7 +34,7 @@ const messages = defineMessages({
sendSilentlyTip: 'Send notifications with no sound',
});
const NotificationsTelegram: React.FC = () => {
const NotificationsTelegram = () => {
const intl = useIntl();
const { addToast, removeToast } = useToasts();
const [isTesting, setIsTesting] = useState(false);
@@ -179,33 +179,29 @@ const NotificationsTelegram: React.FC = () => {
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.botApiTip, {
CreateBotLink: function CreateBotLink(msg) {
return (
<a
href="https://core.telegram.org/bots#6-botfather"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
GetIdBotLink: function GetIdBotLink(msg) {
return (
<a
href="https://telegram.me/get_id_bot"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
code: function code(msg) {
return <code className="bg-opacity-50">{msg}</code>;
},
CreateBotLink: (msg: React.ReactNode) => (
<a
href="https://core.telegram.org/bots#6-botfather"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
GetIdBotLink: (msg: React.ReactNode) => (
<a
href="https://telegram.me/get_id_bot"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
code: (msg: React.ReactNode) => (
<code className="bg-opacity-50">{msg}</code>
),
})}
</span>
</label>
@@ -218,9 +214,11 @@ const NotificationsTelegram: React.FC = () => {
autoComplete="one-time-code"
/>
</div>
{errors.botAPI && touched.botAPI && (
<div className="error">{errors.botAPI}</div>
)}
{errors.botAPI &&
touched.botAPI &&
typeof errors.botAPI === 'string' && (
<div className="error">{errors.botAPI}</div>
)}
</div>
</div>
<div className="form-row">
@@ -234,9 +232,11 @@ const NotificationsTelegram: React.FC = () => {
<div className="form-input-field">
<Field id="botUsername" name="botUsername" type="text" />
</div>
{errors.botUsername && touched.botUsername && (
<div className="error">{errors.botUsername}</div>
)}
{errors.botUsername &&
touched.botUsername &&
typeof errors.botUsername === 'string' && (
<div className="error">{errors.botUsername}</div>
)}
</div>
</div>
<div className="form-row">
@@ -245,20 +245,16 @@ const NotificationsTelegram: React.FC = () => {
<span className="label-required">*</span>
<span className="label-tip">
{intl.formatMessage(messages.chatIdTip, {
GetIdBotLink: function GetIdBotLink(msg) {
return (
<a
href="https://telegram.me/get_id_bot"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
code: function code(msg) {
return <code>{msg}</code>;
},
GetIdBotLink: (msg: React.ReactNode) => (
<a
href="https://telegram.me/get_id_bot"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
code: (msg: React.ReactNode) => <code>{msg}</code>,
})}
</span>
</label>
@@ -266,9 +262,11 @@ const NotificationsTelegram: React.FC = () => {
<div className="form-input-field">
<Field id="chatId" name="chatId" type="text" />
</div>
{errors.chatId && touched.chatId && (
<div className="error">{errors.chatId}</div>
)}
{errors.chatId &&
touched.chatId &&
typeof errors.chatId === 'string' && (
<div className="error">{errors.chatId}</div>
)}
</div>
</div>
<div className="form-row">

View File

@@ -1,14 +1,14 @@
import Alert from '@app/components/Common/Alert';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import globalMessages from '@app/i18n/globalMessages';
import { BeakerIcon, SaveIcon } from '@heroicons/react/outline';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr';
import globalMessages from '../../../../i18n/globalMessages';
import Alert from '../../../Common/Alert';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
const messages = defineMessages({
agentenabled: 'Enable Agent',
@@ -21,7 +21,7 @@ const messages = defineMessages({
'In order to receive web push notifications, Overseerr must be served over HTTPS.',
});
const NotificationsWebPush: React.FC = () => {
const NotificationsWebPush = () => {
const intl = useIntl();
const { addToast, removeToast } = useToasts();
const [isTesting, setIsTesting] = useState(false);

View File

@@ -1,20 +1,22 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import NotificationTypeSelector from '@app/components/NotificationTypeSelector';
import globalMessages from '@app/i18n/globalMessages';
import { BeakerIcon, SaveIcon } from '@heroicons/react/outline';
import { QuestionMarkCircleIcon, RefreshIcon } from '@heroicons/react/solid';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import React, { useState } from 'react';
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import globalMessages from '../../../../i18n/globalMessages';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import NotificationTypeSelector from '../../../NotificationTypeSelector';
const JSONEditor = dynamic(() => import('../../../JSONEditor'), { ssr: false });
const JSONEditor = dynamic(() => import('@app/components/JSONEditor'), {
ssr: false,
});
const defaultPayload = {
notification_type: '{{notification_type}}',
@@ -70,7 +72,7 @@ const messages = defineMessages({
validationTypes: 'You must select at least one notification type',
});
const NotificationsWebhook: React.FC = () => {
const NotificationsWebhook = () => {
const intl = useIntl();
const { addToast, removeToast } = useToasts();
const [isTesting, setIsTesting] = useState(false);
@@ -244,9 +246,11 @@ const NotificationsWebhook: React.FC = () => {
inputMode="url"
/>
</div>
{errors.webhookUrl && touched.webhookUrl && (
<div className="error">{errors.webhookUrl}</div>
)}
{errors.webhookUrl &&
touched.webhookUrl &&
typeof errors.webhookUrl === 'string' && (
<div className="error">{errors.webhookUrl}</div>
)}
</div>
</div>
<div className="form-row">
@@ -273,9 +277,11 @@ const NotificationsWebhook: React.FC = () => {
onBlur={() => setFieldTouched('jsonPayload')}
/>
</div>
{errors.jsonPayload && touched.jsonPayload && (
<div className="error">{errors.jsonPayload}</div>
)}
{errors.jsonPayload &&
touched.jsonPayload &&
typeof errors.jsonPayload === 'string' && (
<div className="error">{errors.jsonPayload}</div>
)}
<div className="mt-2">
<Button
buttonSize="sm"

View File

@@ -1,16 +1,15 @@
import { PencilIcon, PlusIcon } from '@heroicons/react/solid';
import Modal from '@app/components/Common/Modal';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import globalMessages from '@app/i18n/globalMessages';
import { Transition } from '@headlessui/react';
import type { RadarrSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Formik } from 'formik';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import Select from 'react-select';
import { useToasts } from 'react-toast-notifications';
import * as Yup from 'yup';
import type { RadarrSettings } from '../../../../server/lib/settings';
import globalMessages from '../../../i18n/globalMessages';
import Modal from '../../Common/Modal';
import SensitiveInput from '../../Common/SensitiveInput';
import Transition from '../../Transition';
type OptionType = {
value: number;
@@ -91,11 +90,7 @@ interface RadarrModalProps {
onSave: () => void;
}
const RadarrModal: React.FC<RadarrModalProps> = ({
onClose,
radarr,
onSave,
}) => {
const RadarrModal = ({ onClose, radarr, onSave }: RadarrModalProps) => {
const intl = useIntl();
const initialLoad = useRef(false);
const { addToast } = useToasts();
@@ -216,6 +211,7 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
return (
<Transition
as="div"
appear
show
enter="transition ease-in-out duration-300 transform opacity-0"
@@ -343,7 +339,6 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
values.is4k ? messages.edit4kradarr : messages.editradarr
)
}
iconSvg={!radarr ? <PlusIcon /> : <PencilIcon />}
>
<div className="mb-6">
<div className="form-row">
@@ -383,9 +378,11 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
}}
/>
</div>
{errors.name && touched.name && (
<div className="error">{errors.name}</div>
)}
{errors.name &&
touched.name &&
typeof errors.name === 'string' && (
<div className="error">{errors.name}</div>
)}
</div>
</div>
<div className="form-row">
@@ -410,9 +407,11 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
className="rounded-r-only"
/>
</div>
{errors.hostname && touched.hostname && (
<div className="error">{errors.hostname}</div>
)}
{errors.hostname &&
touched.hostname &&
typeof errors.hostname === 'string' && (
<div className="error">{errors.hostname}</div>
)}
</div>
</div>
<div className="form-row">
@@ -432,9 +431,11 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
setFieldValue('port', e.target.value);
}}
/>
{errors.port && touched.port && (
<div className="error">{errors.port}</div>
)}
{errors.port &&
touched.port &&
typeof errors.port === 'string' && (
<div className="error">{errors.port}</div>
)}
</div>
</div>
<div className="form-row">
@@ -471,9 +472,11 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
}}
/>
</div>
{errors.apiKey && touched.apiKey && (
<div className="error">{errors.apiKey}</div>
)}
{errors.apiKey &&
touched.apiKey &&
typeof errors.apiKey === 'string' && (
<div className="error">{errors.apiKey}</div>
)}
</div>
</div>
<div className="form-row">
@@ -493,9 +496,11 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
}}
/>
</div>
{errors.baseUrl && touched.baseUrl && (
<div className="error">{errors.baseUrl}</div>
)}
{errors.baseUrl &&
touched.baseUrl &&
typeof errors.baseUrl === 'string' && (
<div className="error">{errors.baseUrl}</div>
)}
</div>
</div>
<div className="form-row">
@@ -531,9 +536,11 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
))}
</Field>
</div>
{errors.activeProfileId && touched.activeProfileId && (
<div className="error">{errors.activeProfileId}</div>
)}
{errors.activeProfileId &&
touched.activeProfileId &&
typeof errors.activeProfileId === 'string' && (
<div className="error">{errors.activeProfileId}</div>
)}
</div>
</div>
<div className="form-row">
@@ -567,9 +574,11 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
))}
</Field>
</div>
{errors.rootFolder && touched.rootFolder && (
<div className="error">{errors.rootFolder}</div>
)}
{errors.rootFolder &&
touched.rootFolder &&
typeof errors.rootFolder === 'string' && (
<div className="error">{errors.rootFolder}</div>
)}
</div>
</div>
<div className="form-row">
@@ -673,9 +682,11 @@ const RadarrModal: React.FC<RadarrModalProps> = ({
inputMode="url"
/>
</div>
{errors.externalUrl && touched.externalUrl && (
<div className="error">{errors.externalUrl}</div>
)}
{errors.externalUrl &&
touched.externalUrl &&
typeof errors.externalUrl === 'string' && (
<div className="error">{errors.externalUrl}</div>
)}
</div>
</div>
<div className="form-row">

View File

@@ -1,14 +1,21 @@
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import Modal from '@app/components/Common/Modal';
import globalMessages from '@app/i18n/globalMessages';
import { Transition } from '@headlessui/react';
import { DocumentTextIcon } from '@heroicons/react/outline';
import React, { useState } from 'react';
import dynamic from 'next/dynamic';
import { Fragment, useState } from 'react';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
import ReactMarkdown from 'react-markdown';
import useSWR from 'swr';
import globalMessages from '../../../../i18n/globalMessages';
import Badge from '../../../Common/Badge';
import Button from '../../../Common/Button';
import LoadingSpinner from '../../../Common/LoadingSpinner';
import Modal from '../../../Common/Modal';
import Transition from '../../../Transition';
// dyanmic is having trouble extracting the props for react-markdown here so we are just ignoring it since its really
// only children we are using
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ReactMarkdown = dynamic<any>(() => import('react-markdown'), {
ssr: false,
});
const messages = defineMessages({
releases: 'Releases',
@@ -48,17 +55,14 @@ interface ReleaseProps {
currentVersion: string;
}
const Release: React.FC<ReleaseProps> = ({
currentVersion,
release,
isLatest,
}) => {
const Release = ({ currentVersion, release, isLatest }: ReleaseProps) => {
const intl = useIntl();
const [isModalOpen, setModalOpen] = useState(false);
return (
<div className="flex w-full flex-col space-y-3 rounded-md bg-gray-800 px-4 py-2 shadow-md ring-1 ring-gray-700 sm:flex-row sm:space-y-0 sm:space-x-3">
<Transition
as={Fragment}
enter="opacity-0 transition duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
@@ -69,7 +73,6 @@ const Release: React.FC<ReleaseProps> = ({
>
<Modal
onCancel={() => setModalOpen(false)}
iconSvg={<DocumentTextIcon />}
title={intl.formatMessage(messages.versionChangelog, {
version: release.name,
})}
@@ -120,7 +123,7 @@ interface ReleasesProps {
currentVersion: string;
}
const Releases: React.FC<ReleasesProps> = ({ currentVersion }) => {
const Releases = ({ currentVersion }: ReleasesProps) => {
const intl = useIntl();
const { data, error } = useSWR<GitHubRelease[]>(REPO_RELEASE_API);

View File

@@ -1,19 +1,18 @@
import Alert from '@app/components/Common/Alert';
import Badge from '@app/components/Common/Badge';
import List from '@app/components/Common/List';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import Releases from '@app/components/Settings/SettingsAbout/Releases';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import { InformationCircleIcon } from '@heroicons/react/solid';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import {
import type {
SettingsAboutResponse,
StatusResponse,
} from '../../../../server/interfaces/api/settingsInterfaces';
import globalMessages from '../../../i18n/globalMessages';
import Error from '../../../pages/_error';
import Alert from '../../Common/Alert';
import Badge from '../../Common/Badge';
import List from '../../Common/List';
import LoadingSpinner from '../../Common/LoadingSpinner';
import PageTitle from '../../Common/PageTitle';
import Releases from './Releases';
} from '@server/interfaces/api/settingsInterfaces';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
const messages = defineMessages({
about: 'About',
@@ -37,7 +36,7 @@ const messages = defineMessages({
'You are running the <code>develop</code> branch of Overseerr, which is only recommended for those contributing to development or assisting with bleeding-edge testing.',
});
const SettingsAbout: React.FC = () => {
const SettingsAbout = () => {
const intl = useIntl();
const { data, error } = useSWR<SettingsAboutResponse>(
'/api/v1/settings/about'
@@ -61,19 +60,19 @@ const SettingsAbout: React.FC = () => {
intl.formatMessage(globalMessages.settings),
]}
/>
<div className="mt-6 rounded-md bg-indigo-700 p-4">
<div className="mt-6 rounded-md border border-indigo-500 bg-indigo-400 bg-opacity-20 p-4 backdrop-blur">
<div className="flex">
<div className="flex-shrink-0">
<InformationCircleIcon className="h-5 w-5 text-white" />
<InformationCircleIcon className="h-5 w-5 text-gray-100" />
</div>
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm leading-5 text-white">
<p className="text-sm leading-5 text-gray-100">
{intl.formatMessage(messages.betawarning)}
</p>
<p className="mt-3 text-sm leading-5 md:mt-0 md:ml-6">
<a
href="http://github.com/fallenbagel/jellyseerr"
className="whitespace-nowrap font-medium text-indigo-100 transition duration-150 ease-in-out hover:text-white"
className="whitespace-nowrap font-medium text-gray-100 transition duration-150 ease-in-out hover:text-white"
target="_blank"
rel="noreferrer"
>
@@ -88,9 +87,9 @@ const SettingsAbout: React.FC = () => {
{data.version.startsWith('develop-') && (
<Alert
title={intl.formatMessage(messages.runningDevelop, {
code: function code(msg) {
return <code className="bg-opacity-50">{msg}</code>;
},
code: (msg: React.ReactNode) => (
<code className="bg-opacity-50">{msg}</code>
),
})}
/>
)}

View File

@@ -0,0 +1,54 @@
import Badge from '@app/components/Common/Badge';
import Tooltip from '@app/components/Common/Tooltip';
import globalMessages from '@app/i18n/globalMessages';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
advancedTooltip:
'Incorrectly configuring this setting may result in broken functionality',
experimentalTooltip:
'Enabling this setting may result in unexpected application behavior',
restartrequiredTooltip:
'Overseerr must be restarted for changes to this setting to take effect',
});
const SettingsBadge = ({
badgeType,
className,
}: {
badgeType: 'advanced' | 'experimental' | 'restartRequired';
className?: string;
}) => {
const intl = useIntl();
switch (badgeType) {
case 'advanced':
return (
<Tooltip content={intl.formatMessage(messages.advancedTooltip)}>
<Badge badgeType="danger" className={className}>
{intl.formatMessage(globalMessages.advanced)}
</Badge>
</Tooltip>
);
case 'experimental':
return (
<Tooltip content={intl.formatMessage(messages.experimentalTooltip)}>
<Badge badgeType="warning">
{intl.formatMessage(globalMessages.experimental)}
</Badge>
</Tooltip>
);
case 'restartRequired':
return (
<Tooltip content={intl.formatMessage(messages.restartrequiredTooltip)}>
<Badge badgeType="primary" className={className}>
{intl.formatMessage(globalMessages.restartRequired)}
</Badge>
</Tooltip>
);
default:
return null;
}
};
export default SettingsBadge;

View File

@@ -1,18 +1,19 @@
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import LibraryItem from '@app/components/Settings/LibraryItem';
import globalMessages from '@app/i18n/globalMessages';
import { SaveIcon } from '@heroicons/react/outline';
import type { JellyfinSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Formik } from 'formik';
import React, { useState } from 'react';
import getConfig from 'next/config';
import type React from 'react';
import { useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import { JellyfinSettings } from '../../../server/lib/settings';
import globalMessages from '../../i18n/globalMessages';
import Badge from '../Common/Badge';
import Button from '../Common/Button';
import LoadingSpinner from '../Common/LoadingSpinner';
import LibraryItem from './LibraryItem';
import getConfig from 'next/config';
const messages = defineMessages({
jellyfinsettings: '{mediaServerName} Settings',

View File

@@ -1,29 +1,25 @@
import Spinner from '@app/assets/spinner.svg';
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import Modal from '@app/components/Common/Modal';
import PageTitle from '@app/components/Common/PageTitle';
import Table from '@app/components/Common/Table';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import { formatBytes } from '@app/utils/numberHelpers';
import { Transition } from '@headlessui/react';
import { PlayIcon, StopIcon, TrashIcon } from '@heroicons/react/outline';
import { PencilIcon } from '@heroicons/react/solid';
import { MediaServerType } from '@server/constants/server';
import type { CacheItem } from '@server/interfaces/api/settingsInterfaces';
import type { JobId } from '@server/lib/settings';
import axios from 'axios';
import React, { useState } from 'react';
import {
defineMessages,
FormattedRelativeTime,
MessageDescriptor,
useIntl,
} from 'react-intl';
import { Fragment, useState } from 'react';
import type { MessageDescriptor } from 'react-intl';
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import { MediaServerType } from '../../../../server/constants/server';
import { CacheItem } from '../../../../server/interfaces/api/settingsInterfaces';
import { JobId } from '../../../../server/lib/settings';
import Spinner from '../../../assets/spinner.svg';
import useSettings from '../../../hooks/useSettings';
import globalMessages from '../../../i18n/globalMessages';
import { formatBytes } from '../../../utils/numberHelpers';
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 Table from '../../Common/Table';
import Transition from '../../Transition';
const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
jobsandcache: 'Jobs & Cache',
@@ -53,6 +49,7 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
unknownJob: 'Unknown Job',
'plex-recently-added-scan': 'Plex Recently Added Scan',
'plex-full-scan': 'Plex Full Library Scan',
'plex-watchlist-sync': 'Plex Watchlist Sync',
'jellyfin-recently-added-sync': 'Jellyfin Recently Added Scan',
'jellyfin-full-sync': 'Jellyfin Full Library Scan',
'radarr-scan': 'Radarr Scan',
@@ -78,7 +75,7 @@ interface Job {
running: boolean;
}
const SettingsJobs: React.FC = () => {
const SettingsJobs = () => {
const intl = useIntl();
const { addToast } = useToasts();
const {
@@ -195,6 +192,7 @@ const SettingsJobs: React.FC = () => {
]}
/>
<Transition
as={Fragment}
enter="opacity-0 transition duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
@@ -210,7 +208,6 @@ const SettingsJobs: React.FC = () => {
? intl.formatMessage(globalMessages.saving)
: intl.formatMessage(globalMessages.save)
}
iconSvg={<PencilIcon />}
onCancel={() => setJobEditModal({ isOpen: false })}
okDisabled={isSaving}
onOk={() => scheduleJob()}
@@ -332,7 +329,7 @@ const SettingsJobs: React.FC = () => {
}
>
<PencilIcon />
{intl.formatMessage(globalMessages.edit)}
<span>{intl.formatMessage(globalMessages.edit)}</span>
</Button>
)}
{job.running ? (
@@ -342,7 +339,7 @@ const SettingsJobs: React.FC = () => {
</Button>
) : (
<Button buttonType="primary" onClick={() => runJob(job)}>
<PlayIcon className="mr-1 h-5 w-5" />
<PlayIcon />
<span>{intl.formatMessage(messages.runnow)}</span>
</Button>
)}

View File

@@ -1,11 +1,12 @@
import PageTitle from '@app/components/Common/PageTitle';
import type { SettingsRoute } from '@app/components/Common/SettingsTabs';
import SettingsTabs from '@app/components/Common/SettingsTabs';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import { MediaServerType } from '@server/constants/server';
import getConfig from 'next/config';
import React from 'react';
import type React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { MediaServerType } from '../../../server/constants/server';
import useSettings from '../../hooks/useSettings';
import globalMessages from '../../i18n/globalMessages';
import PageTitle from '../Common/PageTitle';
import SettingsTabs, { SettingsRoute } from '../Common/SettingsTabs';
const messages = defineMessages({
menuGeneralSettings: 'General',
@@ -19,7 +20,11 @@ const messages = defineMessages({
menuAbout: 'About',
});
const SettingsLayout: React.FC = ({ children }) => {
type SettingsLayoutProps = {
children: React.ReactNode;
};
const SettingsLayout = ({ children }: SettingsLayoutProps) => {
const intl = useIntl();
const { publicRuntimeConfig } = getConfig();
const settings = useSettings();

View File

@@ -1,3 +1,14 @@
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import Modal from '@app/components/Common/Modal';
import PageTitle from '@app/components/Common/PageTitle';
import Table from '@app/components/Common/Table';
import Tooltip from '@app/components/Common/Tooltip';
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import { Transition } from '@headlessui/react';
import {
ChevronLeftIcon,
ChevronRightIcon,
@@ -7,26 +18,16 @@ import {
PauseIcon,
PlayIcon,
} from '@heroicons/react/solid';
import type {
LogMessage,
LogsResultsResponse,
} from '@server/interfaces/api/settingsInterfaces';
import copy from 'copy-to-clipboard';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import { Fragment, useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import {
LogMessage,
LogsResultsResponse,
} from '../../../../server/interfaces/api/settingsInterfaces';
import { useUpdateQueryParams } from '../../../hooks/useUpdateQueryParams';
import globalMessages from '../../../i18n/globalMessages';
import Error from '../../../pages/_error';
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 Table from '../../Common/Table';
import Transition from '../../Transition';
const messages = defineMessages({
logs: 'Logs',
@@ -47,18 +48,22 @@ const messages = defineMessages({
logDetails: 'Log Details',
extraData: 'Additional Data',
copiedLogMessage: 'Copied log message to clipboard.',
viewdetails: 'View Details',
});
type Filter = 'debug' | 'info' | 'warn' | 'error';
const SettingsLogs: React.FC = () => {
const SettingsLogs = () => {
const router = useRouter();
const intl = useIntl();
const { addToast } = useToasts();
const [currentFilter, setCurrentFilter] = useState<Filter>('debug');
const [currentPageSize, setCurrentPageSize] = useState(25);
const [refreshInterval, setRefreshInterval] = useState(5000);
const [activeLog, setActiveLog] = useState<LogMessage | null>(null);
const [activeLog, setActiveLog] = useState<{
isOpen: boolean;
log?: LogMessage;
}>({ isOpen: false });
const page = router.query.page ? Number(router.query.page) : 1;
const pageIndex = page - 1;
@@ -133,6 +138,7 @@ const SettingsLogs: React.FC = () => {
]}
/>
<Transition
as={Fragment}
enter="opacity-0 transition duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
@@ -140,14 +146,15 @@ const SettingsLogs: React.FC = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
appear
show={!!activeLog}
show={activeLog.isOpen}
>
<Modal
title={intl.formatMessage(messages.logDetails)}
iconSvg={<DocumentSearchIcon />}
onCancel={() => setActiveLog(null)}
onCancel={() => setActiveLog({ log: activeLog.log, isOpen: false })}
cancelText={intl.formatMessage(globalMessages.close)}
onOk={() => (activeLog ? copyLogString(activeLog) : undefined)}
onOk={() =>
activeLog.log ? copyLogString(activeLog.log) : undefined
}
okText={intl.formatMessage(messages.copyToClipboard)}
okButtonType="primary"
>
@@ -159,7 +166,7 @@ const SettingsLogs: React.FC = () => {
</div>
<div className="mb-1 text-sm font-medium leading-5 text-gray-400 sm:mt-2">
<div className="flex max-w-lg items-center">
{intl.formatDate(activeLog.timestamp, {
{intl.formatDate(activeLog.log?.timestamp, {
year: 'numeric',
month: 'short',
day: '2-digit',
@@ -178,16 +185,16 @@ const SettingsLogs: React.FC = () => {
<div className="flex max-w-lg items-center">
<Badge
badgeType={
activeLog.level === 'error'
activeLog.log?.level === 'error'
? 'danger'
: activeLog.level === 'warn'
: activeLog.log?.level === 'warn'
? 'warning'
: activeLog.level === 'info'
: activeLog.log?.level === 'info'
? 'success'
: 'default'
}
>
{activeLog.level.toUpperCase()}
{activeLog.log?.level.toUpperCase()}
</Badge>
</div>
</div>
@@ -198,7 +205,7 @@ const SettingsLogs: React.FC = () => {
</div>
<div className="mb-1 text-sm font-medium leading-5 text-gray-400 sm:mt-2">
<div className="flex max-w-lg items-center">
{activeLog.label}
{activeLog.log?.label}
</div>
</div>
</div>
@@ -208,18 +215,18 @@ const SettingsLogs: React.FC = () => {
</div>
<div className="col-span-2 mb-1 text-sm font-medium leading-5 text-gray-400 sm:mt-2">
<div className="flex max-w-lg items-center">
{activeLog.message}
{activeLog.log?.message}
</div>
</div>
</div>
{activeLog.data && (
{activeLog.log?.data && (
<div className="form-row">
<div className="text-label">
{intl.formatMessage(messages.extraData)}
</div>
<div className="col-span-2 mb-1 text-sm font-medium leading-5 text-gray-400 sm:mt-2">
<code className="block max-h-64 w-full overflow-auto whitespace-pre bg-gray-800 px-6 py-4 ring-1 ring-gray-700">
{JSON.stringify(activeLog.data, null, ' ')}
{JSON.stringify(activeLog.log?.data, null, ' ')}
</code>
</div>
</div>
@@ -232,9 +239,9 @@ const SettingsLogs: React.FC = () => {
<h3 className="heading">{intl.formatMessage(messages.logs)}</h3>
<p className="description">
{intl.formatMessage(messages.logsDescription, {
code: function code(msg) {
return <code className="bg-opacity-50">{msg}</code>;
},
code: (msg: React.ReactNode) => (
<code className="bg-opacity-50">{msg}</code>
),
appDataPath: appData ? appData.appDataPath : '/app/config',
})}
</p>
@@ -327,23 +334,33 @@ const SettingsLogs: React.FC = () => {
<Table.TD className="text-gray-300">{row.message}</Table.TD>
<Table.TD className="-m-1 flex flex-wrap items-center justify-end">
{row.data && (
<Tooltip
content={intl.formatMessage(messages.viewdetails)}
>
<Button
buttonType="primary"
buttonSize="sm"
onClick={() =>
setActiveLog({ log: row, isOpen: true })
}
className="m-1"
>
<DocumentSearchIcon className="icon-md" />
</Button>
</Tooltip>
)}
<Tooltip
content={intl.formatMessage(messages.copyToClipboard)}
>
<Button
buttonType="primary"
buttonSize="sm"
onClick={() => setActiveLog(row)}
onClick={() => copyLogString(row)}
className="m-1"
>
<DocumentSearchIcon className="icon-md" />
<ClipboardCopyIcon className="icon-md" />
</Button>
)}
<Button
buttonType="primary"
buttonSize="sm"
onClick={() => copyLogString(row)}
className="m-1"
>
<ClipboardCopyIcon className="icon-md" />
</Button>
</Tooltip>
</Table.TD>
</tr>
);
@@ -388,9 +405,9 @@ const SettingsLogs: React.FC = () => {
data.results.length
: (pageIndex + 1) * currentPageSize,
total: data.pageInfo.results,
strong: function strong(msg) {
return <span className="font-medium">{msg}</span>;
},
strong: (msg: React.ReactNode) => (
<span className="font-medium">{msg}</span>
),
})}
</p>
</div>

View File

@@ -1,29 +1,27 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import Tooltip from '@app/components/Common/Tooltip';
import LanguageSelector from '@app/components/LanguageSelector';
import RegionSelector from '@app/components/RegionSelector';
import CopyButton from '@app/components/Settings/CopyButton';
import SettingsBadge from '@app/components/Settings/SettingsBadge';
import type { AvailableLocale } from '@app/context/LanguageContext';
import { availableLanguages } from '@app/context/LanguageContext';
import useLocale from '@app/hooks/useLocale';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { SaveIcon } from '@heroicons/react/outline';
import { RefreshIcon } from '@heroicons/react/solid';
import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces';
import type { MainSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr';
import * as Yup from 'yup';
import { UserSettingsGeneralResponse } from '../../../server/interfaces/api/userSettingsInterfaces';
import type { MainSettings } from '../../../server/lib/settings';
import {
availableLanguages,
AvailableLocale,
} from '../../context/LanguageContext';
import useLocale from '../../hooks/useLocale';
import { Permission, useUser } from '../../hooks/useUser';
import globalMessages from '../../i18n/globalMessages';
import Badge from '../Common/Badge';
import Button from '../Common/Button';
import LoadingSpinner from '../Common/LoadingSpinner';
import PageTitle from '../Common/PageTitle';
import SensitiveInput from '../Common/SensitiveInput';
import LanguageSelector from '../LanguageSelector';
import RegionSelector from '../RegionSelector';
import CopyButton from './CopyButton';
const messages = defineMessages({
general: 'General',
@@ -43,16 +41,15 @@ const messages = defineMessages({
toastSettingsFailure: 'Something went wrong while saving settings.',
hideAvailable: 'Hide Available Media',
csrfProtection: 'Enable CSRF Protection',
csrfProtectionTip:
'Set external API access to read-only (requires HTTPS, and Overseerr must be reloaded for changes to take effect)',
csrfProtectionTip: 'Set external API access to read-only (requires HTTPS)',
csrfProtectionHoverTip:
'Do NOT enable this setting unless you understand what you are doing!',
cacheImages: 'Enable Image Caching',
cacheImagesTip:
'Optimize and store all images locally (consumes a significant amount of disk space)',
'Cache and serve optimized images (requires a significant amount of disk space)',
trustProxy: 'Enable Proxy Support',
trustProxyTip:
'Allow Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be reloaded for changes to take effect)',
'Allow Overseerr to correctly register client IP addresses behind a proxy',
validationApplicationTitle: 'You must provide an application title',
validationApplicationUrl: 'You must provide a valid URL',
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
@@ -60,7 +57,7 @@ const messages = defineMessages({
locale: 'Display Language',
});
const SettingsMain: React.FC = () => {
const SettingsMain = () => {
const { addToast } = useToasts();
const { user: currentUser, hasPermission: userHasPermission } = useUser();
const intl = useIntl();
@@ -136,6 +133,7 @@ const SettingsMain: React.FC = () => {
originalLanguage: data?.originalLanguage,
partialRequestsEnabled: data?.partialRequestsEnabled,
trustProxy: data?.trustProxy,
cacheImages: data?.cacheImages,
}}
enableReinitialize
validationSchema={MainSettingsSchema}
@@ -151,8 +149,10 @@ const SettingsMain: React.FC = () => {
originalLanguage: values.originalLanguage,
partialRequestsEnabled: values.partialRequestsEnabled,
trustProxy: values.trustProxy,
cacheImages: values.cacheImages,
});
mutate('/api/v1/settings/public');
mutate('/api/v1/status');
if (setLocale) {
setLocale(
@@ -229,9 +229,11 @@ const SettingsMain: React.FC = () => {
type="text"
/>
</div>
{errors.applicationTitle && touched.applicationTitle && (
<div className="error">{errors.applicationTitle}</div>
)}
{errors.applicationTitle &&
touched.applicationTitle &&
typeof errors.applicationTitle === 'string' && (
<div className="error">{errors.applicationTitle}</div>
)}
</div>
</div>
<div className="form-row">
@@ -247,14 +249,19 @@ const SettingsMain: React.FC = () => {
inputMode="url"
/>
</div>
{errors.applicationUrl && touched.applicationUrl && (
<div className="error">{errors.applicationUrl}</div>
)}
{errors.applicationUrl &&
touched.applicationUrl &&
typeof errors.applicationUrl === 'string' && (
<div className="error">{errors.applicationUrl}</div>
)}
</div>
</div>
<div className="form-row">
<label htmlFor="trustProxy" className="checkbox-label">
<span>{intl.formatMessage(messages.trustProxy)}</span>
<span className="mr-2">
{intl.formatMessage(messages.trustProxy)}
</span>
<SettingsBadge badgeType="restartRequired" />
<span className="label-tip">
{intl.formatMessage(messages.trustProxyTip)}
</span>
@@ -275,23 +282,49 @@ const SettingsMain: React.FC = () => {
<span className="mr-2">
{intl.formatMessage(messages.csrfProtection)}
</span>
<Badge badgeType="danger">
{intl.formatMessage(globalMessages.advanced)}
</Badge>
<SettingsBadge badgeType="advanced" className="mr-2" />
<SettingsBadge badgeType="restartRequired" />
<span className="label-tip">
{intl.formatMessage(messages.csrfProtectionTip)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="csrfProtection"
name="csrfProtection"
title={intl.formatMessage(
<Tooltip
content={intl.formatMessage(
messages.csrfProtectionHoverTip
)}
>
<Field
type="checkbox"
id="csrfProtection"
name="csrfProtection"
onChange={() => {
setFieldValue(
'csrfProtection',
!values.csrfProtection
);
}}
/>
</Tooltip>
</div>
</div>
<div className="form-row">
<label htmlFor="csrfProtection" className="checkbox-label">
<span className="mr-2">
{intl.formatMessage(messages.cacheImages)}
</span>
<SettingsBadge badgeType="experimental" />
<span className="label-tip">
{intl.formatMessage(messages.cacheImagesTip)}
</span>
</label>
<div className="form-input-area">
<Field
type="checkbox"
id="cacheImages"
name="cacheImages"
onChange={() => {
setFieldValue('csrfProtection', !values.csrfProtection);
setFieldValue('cacheImages', !values.cacheImages);
}}
/>
</div>
@@ -358,9 +391,7 @@ const SettingsMain: React.FC = () => {
<span className="mr-2">
{intl.formatMessage(messages.hideAvailable)}
</span>
<Badge badgeType="warning">
{intl.formatMessage(globalMessages.experimental)}
</Badge>
<SettingsBadge badgeType="experimental" />
</label>
<div className="form-input-area">
<Field

View File

@@ -1,16 +1,16 @@
import DiscordLogo from '@app/assets/extlogos/discord.svg';
import GotifyLogo from '@app/assets/extlogos/gotify.svg';
import LunaSeaLogo from '@app/assets/extlogos/lunasea.svg';
import PushbulletLogo from '@app/assets/extlogos/pushbullet.svg';
import PushoverLogo from '@app/assets/extlogos/pushover.svg';
import SlackLogo from '@app/assets/extlogos/slack.svg';
import TelegramLogo from '@app/assets/extlogos/telegram.svg';
import PageTitle from '@app/components/Common/PageTitle';
import type { SettingsRoute } from '@app/components/Common/SettingsTabs';
import SettingsTabs from '@app/components/Common/SettingsTabs';
import globalMessages from '@app/i18n/globalMessages';
import { CloudIcon, LightningBoltIcon, MailIcon } from '@heroicons/react/solid';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import DiscordLogo from '../../assets/extlogos/discord.svg';
import GotifyLogo from '../../assets/extlogos/gotify.svg';
import LunaSeaLogo from '../../assets/extlogos/lunasea.svg';
import PushbulletLogo from '../../assets/extlogos/pushbullet.svg';
import PushoverLogo from '../../assets/extlogos/pushover.svg';
import SlackLogo from '../../assets/extlogos/slack.svg';
import TelegramLogo from '../../assets/extlogos/telegram.svg';
import globalMessages from '../../i18n/globalMessages';
import PageTitle from '../Common/PageTitle';
import SettingsTabs, { SettingsRoute } from '../Common/SettingsTabs';
const messages = defineMessages({
notifications: 'Notifications',
@@ -22,7 +22,11 @@ const messages = defineMessages({
webpush: 'Web Push',
});
const SettingsNotifications: React.FC = ({ children }) => {
type SettingsNotificationsProps = {
children: React.ReactNode;
};
const SettingsNotifications = ({ children }: SettingsNotificationsProps) => {
const intl = useIntl();
const settingsRoutes: SettingsRoute[] = [

View File

@@ -1,26 +1,24 @@
import Alert from '@app/components/Common/Alert';
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import LibraryItem from '@app/components/Settings/LibraryItem';
import SettingsBadge from '@app/components/Settings/SettingsBadge';
import globalMessages from '@app/i18n/globalMessages';
import { SaveIcon } from '@heroicons/react/outline';
import { RefreshIcon, SearchIcon, XIcon } from '@heroicons/react/solid';
import type { PlexDevice } from '@server/interfaces/api/plexInterfaces';
import type { PlexSettings, TautulliSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Formik } from 'formik';
import { orderBy } from 'lodash';
import React, { useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR from 'swr';
import * as Yup from 'yup';
import type { PlexDevice } from '../../../server/interfaces/api/plexInterfaces';
import type {
PlexSettings,
TautulliSettings,
} from '../../../server/lib/settings';
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 PageTitle from '../Common/PageTitle';
import SensitiveInput from '../Common/SensitiveInput';
import LibraryItem from './LibraryItem';
const messages = defineMessages({
plex: 'Plex',
@@ -107,7 +105,7 @@ interface SettingsPlexProps {
onComplete?: () => void;
}
const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
const SettingsPlex = ({ onComplete }: SettingsPlexProps) => {
const [isSyncing, setIsSyncing] = useState(false);
const [isRefreshingPresets, setIsRefreshingPresets] = useState(false);
const [availableServers, setAvailableServers] = useState<PlexDevice[] | null>(
@@ -344,18 +342,16 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
<div className="section">
<Alert
title={intl.formatMessage(messages.settingUpPlexDescription, {
RegisterPlexTVLink: function RegisterPlexTVLink(msg) {
return (
<a
href="https://plex.tv"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
RegisterPlexTVLink: (msg: React.ReactNode) => (
<a
href="https://plex.tv"
className="text-white transition duration-300 hover:underline"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
type="info"
/>
@@ -517,9 +513,11 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
className="rounded-r-only"
/>
</div>
{errors.hostname && touched.hostname && (
<div className="error">{errors.hostname}</div>
)}
{errors.hostname &&
touched.hostname &&
typeof errors.hostname === 'string' && (
<div className="error">{errors.hostname}</div>
)}
</div>
</div>
<div className="form-row">
@@ -535,9 +533,11 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
name="port"
className="short"
/>
{errors.port && touched.port && (
<div className="error">{errors.port}</div>
)}
{errors.port &&
touched.port &&
typeof errors.port === 'string' && (
<div className="error">{errors.port}</div>
)}
</div>
</div>
<div className="form-row">
@@ -558,21 +558,17 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
<div className="form-row">
<label htmlFor="webAppUrl" className="text-label">
{intl.formatMessage(messages.webAppUrl, {
WebAppLink: function WebAppLink(msg) {
return (
<a
href="https://support.plex.tv/articles/200288666-opening-plex-web-app/"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
);
},
WebAppLink: (msg: React.ReactNode) => (
<a
href="https://support.plex.tv/articles/200288666-opening-plex-web-app/"
target="_blank"
rel="noreferrer"
>
{msg}
</a>
),
})}
<Badge badgeType="danger" className="ml-2">
{intl.formatMessage(globalMessages.advanced)}
</Badge>
<SettingsBadge badgeType="advanced" className="ml-2" />
<span className="label-tip">
{intl.formatMessage(messages.webAppUrlTip)}
</span>
@@ -587,9 +583,11 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
placeholder="https://app.plex.tv/desktop"
/>
</div>
{errors.webAppUrl && touched.webAppUrl && (
<div className="error">{errors.webAppUrl}</div>
)}
{errors.webAppUrl &&
touched.webAppUrl &&
typeof errors.webAppUrl === 'string' && (
<div className="error">{errors.webAppUrl}</div>
)}
</div>
</div>
<div className="actions">
@@ -803,9 +801,11 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
className="rounded-r-only"
/>
</div>
{errors.tautulliHostname && touched.tautulliHostname && (
<div className="error">{errors.tautulliHostname}</div>
)}
{errors.tautulliHostname &&
touched.tautulliHostname &&
typeof errors.tautulliHostname === 'string' && (
<div className="error">{errors.tautulliHostname}</div>
)}
</div>
</div>
<div className="form-row">
@@ -821,9 +821,11 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
name="tautulliPort"
className="short"
/>
{errors.tautulliPort && touched.tautulliPort && (
<div className="error">{errors.tautulliPort}</div>
)}
{errors.tautulliPort &&
touched.tautulliPort &&
typeof errors.tautulliPort === 'string' && (
<div className="error">{errors.tautulliPort}</div>
)}
</div>
</div>
<div className="form-row">
@@ -857,9 +859,11 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
name="tautulliUrlBase"
/>
</div>
{errors.tautulliUrlBase && touched.tautulliUrlBase && (
<div className="error">{errors.tautulliUrlBase}</div>
)}
{errors.tautulliUrlBase &&
touched.tautulliUrlBase &&
typeof errors.tautulliUrlBase === 'string' && (
<div className="error">{errors.tautulliUrlBase}</div>
)}
</div>
</div>
<div className="form-row">
@@ -876,9 +880,11 @@ const SettingsPlex: React.FC<SettingsPlexProps> = ({ onComplete }) => {
autoComplete="one-time-code"
/>
</div>
{errors.tautulliApiKey && touched.tautulliApiKey && (
<div className="error">{errors.tautulliApiKey}</div>
)}
{errors.tautulliApiKey &&
touched.tautulliApiKey &&
typeof errors.tautulliApiKey === 'string' && (
<div className="error">{errors.tautulliApiKey}</div>
)}
</div>
</div>
<div className="form-row">

View File

@@ -1,24 +1,21 @@
import RadarrLogo from '@app/assets/services/radarr.svg';
import SonarrLogo from '@app/assets/services/sonarr.svg';
import Alert from '@app/components/Common/Alert';
import Badge from '@app/components/Common/Badge';
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import Modal from '@app/components/Common/Modal';
import PageTitle from '@app/components/Common/PageTitle';
import RadarrModal from '@app/components/Settings/RadarrModal';
import SonarrModal from '@app/components/Settings/SonarrModal';
import globalMessages from '@app/i18n/globalMessages';
import { Transition } from '@headlessui/react';
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/solid';
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import axios from 'axios';
import React, { useState } from 'react';
import { Fragment, 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',
@@ -43,6 +40,7 @@ const messages = defineMessages({
'A 4K {serverType} server must be marked as default in order to enable users to submit 4K {mediaType} requests.',
mediaTypeMovie: 'movie',
mediaTypeSeries: 'series',
deleteServer: 'Delete {serverType} Server',
});
interface ServerInstanceProps {
@@ -59,7 +57,7 @@ interface ServerInstanceProps {
onDelete: () => void;
}
const ServerInstance: React.FC<ServerInstanceProps> = ({
const ServerInstance = ({
name,
hostname,
port,
@@ -71,7 +69,7 @@ const ServerInstance: React.FC<ServerInstanceProps> = ({
externalUrl,
onEdit,
onDelete,
}) => {
}: ServerInstanceProps) => {
const intl = useIntl();
const internalUrl =
@@ -160,7 +158,7 @@ const ServerInstance: React.FC<ServerInstanceProps> = ({
);
};
const SettingsServices: React.FC = () => {
const SettingsServices = () => {
const intl = useIntl();
const {
data: radarrData,
@@ -247,6 +245,7 @@ const SettingsServices: React.FC = () => {
/>
)}
<Transition
as={Fragment}
show={deleteServerModal.open}
enter="transition ease-in-out duration-300 transform opacity-0"
enterFrom="opacity-0"
@@ -256,7 +255,7 @@ const SettingsServices: React.FC = () => {
leaveTo="opacity-0"
>
<Modal
okText="Delete"
okText={intl.formatMessage(globalMessages.delete)}
okButtonType="danger"
onOk={() => deleteServer()}
onCancel={() =>
@@ -266,8 +265,10 @@ const SettingsServices: React.FC = () => {
type: 'radarr',
})
}
title="Delete Server"
iconSvg={<TrashIcon />}
title={intl.formatMessage(messages.deleteServer, {
serverType:
deleteServerModal.type === 'radarr' ? 'Radarr' : 'Sonarr',
})}
>
{intl.formatMessage(messages.deleteserverconfirm)}
</Modal>
@@ -290,13 +291,11 @@ const SettingsServices: React.FC = () => {
<Alert
title={intl.formatMessage(messages.noDefaultNon4kServer, {
serverType: 'Radarr',
strong: function strong(msg) {
return (
<strong className="font-semibold text-white">
{msg}
</strong>
);
},
strong: (msg: React.ReactNode) => (
<strong className="font-semibold text-white">
{msg}
</strong>
),
})}
/>
) : (
@@ -380,13 +379,11 @@ const SettingsServices: React.FC = () => {
<Alert
title={intl.formatMessage(messages.noDefaultNon4kServer, {
serverType: 'Sonarr',
strong: function strong(msg) {
return (
<strong className="font-semibold text-white">
{msg}
</strong>
);
},
strong: (msg: React.ReactNode) => (
<strong className="font-semibold text-white">
{msg}
</strong>
),
})}
/>
) : (

View File

@@ -1,20 +1,19 @@
import Button from '@app/components/Common/Button';
import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import PageTitle from '@app/components/Common/PageTitle';
import PermissionEdit from '@app/components/PermissionEdit';
import QuotaSelector from '@app/components/QuotaSelector';
import useSettings from '@app/hooks/useSettings';
import globalMessages from '@app/i18n/globalMessages';
import { SaveIcon } from '@heroicons/react/outline';
import { MediaServerType } from '@server/constants/server';
import type { MainSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Form, Formik } from 'formik';
import React from 'react';
import getConfig from 'next/config';
import { defineMessages, useIntl } from 'react-intl';
import { useToasts } from 'react-toast-notifications';
import useSWR, { mutate } from 'swr';
import { MediaServerType } from '../../../../server/constants/server';
import type { MainSettings } from '../../../../server/lib/settings';
import useSettings from '../../../hooks/useSettings';
import globalMessages from '../../../i18n/globalMessages';
import Button from '../../Common/Button';
import LoadingSpinner from '../../Common/LoadingSpinner';
import PageTitle from '../../Common/PageTitle';
import PermissionEdit from '../../PermissionEdit';
import QuotaSelector from '../../QuotaSelector';
import getConfig from 'next/config';
const messages = defineMessages({
users: 'Users',
@@ -34,7 +33,7 @@ const messages = defineMessages({
defaultPermissionsTip: 'Initial permissions assigned to new users',
});
const SettingsUsers: React.FC = () => {
const SettingsUsers = () => {
const { addToast } = useToasts();
const intl = useIntl();
const {

View File

@@ -1,16 +1,16 @@
import { PencilIcon, PlusIcon } from '@heroicons/react/solid';
import Modal from '@app/components/Common/Modal';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import globalMessages from '@app/i18n/globalMessages';
import { Transition } from '@headlessui/react';
import type { SonarrSettings } from '@server/lib/settings';
import axios from 'axios';
import { Field, Formik } from 'formik';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import Select, { OnChangeValue } from 'react-select';
import type { OnChangeValue } from 'react-select';
import Select from 'react-select';
import { useToasts } from 'react-toast-notifications';
import * as Yup from 'yup';
import type { SonarrSettings } from '../../../../server/lib/settings';
import globalMessages from '../../../i18n/globalMessages';
import Modal from '../../Common/Modal';
import SensitiveInput from '../../Common/SensitiveInput';
import Transition from '../../Transition';
type OptionType = {
value: number;
@@ -98,11 +98,7 @@ interface SonarrModalProps {
onSave: () => void;
}
const SonarrModal: React.FC<SonarrModalProps> = ({
onClose,
sonarr,
onSave,
}) => {
const SonarrModal = ({ onClose, sonarr, onSave }: SonarrModalProps) => {
const intl = useIntl();
const initialLoad = useRef(false);
const { addToast } = useToasts();
@@ -224,6 +220,7 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
return (
<Transition
as="div"
appear
show
enter="transition ease-in-out duration-300 transform opacity-0"
@@ -371,7 +368,6 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
values.is4k ? messages.edit4ksonarr : messages.editsonarr
)
}
iconSvg={!sonarr ? <PlusIcon /> : <PencilIcon />}
>
<div className="mb-6">
<div className="form-row">
@@ -411,9 +407,11 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
}}
/>
</div>
{errors.name && touched.name && (
<div className="error">{errors.name}</div>
)}
{errors.name &&
touched.name &&
typeof errors.name === 'string' && (
<div className="error">{errors.name}</div>
)}
</div>
</div>
<div className="form-row">
@@ -438,9 +436,11 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
className="rounded-r-only"
/>
</div>
{errors.hostname && touched.hostname && (
<div className="error">{errors.hostname}</div>
)}
{errors.hostname &&
touched.hostname &&
typeof errors.hostname === 'string' && (
<div className="error">{errors.hostname}</div>
)}
</div>
</div>
<div className="form-row">
@@ -460,9 +460,11 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
setFieldValue('port', e.target.value);
}}
/>
{errors.port && touched.port && (
<div className="error">{errors.port}</div>
)}
{errors.port &&
touched.port &&
typeof errors.port === 'string' && (
<div className="error">{errors.port}</div>
)}
</div>
</div>
<div className="form-row">
@@ -499,9 +501,11 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
}}
/>
</div>
{errors.apiKey && touched.apiKey && (
<div className="error">{errors.apiKey}</div>
)}
{errors.apiKey &&
touched.apiKey &&
typeof errors.apiKey === 'string' && (
<div className="error">{errors.apiKey}</div>
)}
</div>
</div>
<div className="form-row">
@@ -521,9 +525,11 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
}}
/>
</div>
{errors.baseUrl && touched.baseUrl && (
<div className="error">{errors.baseUrl}</div>
)}
{errors.baseUrl &&
touched.baseUrl &&
typeof errors.baseUrl === 'string' && (
<div className="error">{errors.baseUrl}</div>
)}
</div>
</div>
<div className="form-row">
@@ -559,9 +565,11 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
))}
</Field>
</div>
{errors.activeProfileId && touched.activeProfileId && (
<div className="error">{errors.activeProfileId}</div>
)}
{errors.activeProfileId &&
touched.activeProfileId &&
typeof errors.activeProfileId === 'string' && (
<div className="error">{errors.activeProfileId}</div>
)}
</div>
</div>
<div className="form-row">
@@ -595,9 +603,11 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
))}
</Field>
</div>
{errors.rootFolder && touched.rootFolder && (
<div className="error">{errors.rootFolder}</div>
)}
{errors.rootFolder &&
touched.rootFolder &&
typeof errors.rootFolder === 'string' && (
<div className="error">{errors.rootFolder}</div>
)}
</div>
</div>
<div className="form-row">
@@ -919,9 +929,11 @@ const SonarrModal: React.FC<SonarrModalProps> = ({
inputMode="url"
/>
</div>
{errors.externalUrl && touched.externalUrl && (
<div className="error">{errors.externalUrl}</div>
)}
{errors.externalUrl &&
touched.externalUrl &&
typeof errors.externalUrl === 'string' && (
<div className="error">{errors.externalUrl}</div>
)}
</div>
</div>
<div className="form-row">