mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-02 04:39:14 -05:00
Merge remote-tracking branch 'overseerr/develop' into develop
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
54
src/components/Settings/SettingsBadge.tsx
Normal file
54
src/components/Settings/SettingsBadge.tsx
Normal 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;
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user