mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat: show alert/prompt when settings changes require restart (#2401)
* fix: correct 'StatusChecker' typo * feat: add restart required check to StatusChecker * fix(perms): remove MANAGE_SETTINGS permission * fix: allow alert to be dismissed * fix(lang): add missing string in SettingsServices * fix(frontend): fix modal icon border * fix(frontend): un-dismiss alert if setting reverted not require server restart * fix(backend): restart flag only needs to track main settings * fix: rebase issue * refactor: appease Prettier * refactor: swap settings badge order * fix: type import for MainSettings * test: add cypress test for restart prompt
This commit is contained in:
@@ -130,7 +130,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative overflow-x-hidden sm:flex sm:items-center">
|
||||
<div className="relative overflow-x-hidden p-0.5 sm:flex sm:items-center">
|
||||
{iconSvg && <div className="modal-icon">{iconSvg}</div>}
|
||||
<div
|
||||
className={`mt-3 truncate text-center text-white sm:mt-0 sm:text-left ${
|
||||
@@ -149,7 +149,11 @@ const Modal: React.FC<ModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
{children && (
|
||||
<div className="relative mt-4 text-sm leading-5 text-gray-300">
|
||||
<div
|
||||
className={`relative mt-4 text-sm leading-5 text-gray-300 ${
|
||||
!(onCancel || onOk || onSecondary || onTertiary) ? 'mb-3' : ''
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
|
@@ -80,7 +80,8 @@ const SidebarLinks: SidebarLinkProps[] = [
|
||||
messagesKey: 'settings',
|
||||
svgIcon: <CogIcon className="mr-3 h-6 w-6" />,
|
||||
activeRegExp: /^\/settings/,
|
||||
requiredPermission: Permission.MANAGE_SETTINGS,
|
||||
requiredPermission: Permission.ADMIN,
|
||||
dataTestId: 'sidebar-menu-settings',
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -12,9 +12,6 @@ export const messages = defineMessages({
|
||||
users: 'Manage Users',
|
||||
usersDescription:
|
||||
'Grant permission to manage users. Users with this permission cannot modify users with or grant the Admin privilege.',
|
||||
settings: 'Manage Settings',
|
||||
settingsDescription:
|
||||
'Grant permission to modify global settings. A user must have this permission to grant it to others.',
|
||||
managerequests: 'Manage Requests',
|
||||
managerequestsDescription:
|
||||
'Grant permission to manage media requests. All requests made by a user with this permission will be automatically approved.',
|
||||
@@ -88,12 +85,6 @@ export const PermissionEdit: React.FC<PermissionEditProps> = ({
|
||||
description: intl.formatMessage(messages.adminDescription),
|
||||
permission: Permission.ADMIN,
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
name: intl.formatMessage(messages.settings),
|
||||
description: intl.formatMessage(messages.settingsDescription),
|
||||
permission: Permission.MANAGE_SETTINGS,
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
name: intl.formatMessage(messages.users),
|
||||
|
@@ -67,14 +67,9 @@ const PermissionOption: React.FC<PermissionOptionProps> = ({
|
||||
}
|
||||
|
||||
if (
|
||||
// Non-Admin users cannot modify the Admin permission
|
||||
(actingUser &&
|
||||
!hasPermission(Permission.ADMIN, actingUser.permissions) &&
|
||||
option.permission === Permission.ADMIN) ||
|
||||
// Users without the Manage Settings permission cannot modify/grant that permission
|
||||
(actingUser &&
|
||||
!hasPermission(Permission.MANAGE_SETTINGS, actingUser.permissions) &&
|
||||
option.permission === Permission.MANAGE_SETTINGS)
|
||||
// Only the owner can modify the Admin permission
|
||||
actingUser?.id !== 1 &&
|
||||
option.permission === Permission.ADMIN
|
||||
) {
|
||||
disabled = true;
|
||||
}
|
||||
|
@@ -41,8 +41,7 @@ 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',
|
||||
@@ -50,7 +49,7 @@ const messages = defineMessages({
|
||||
'Optimize and store all images locally (consumes 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',
|
||||
@@ -151,6 +150,7 @@ const SettingsMain: React.FC = () => {
|
||||
trustProxy: values.trustProxy,
|
||||
});
|
||||
mutate('/api/v1/settings/public');
|
||||
mutate('/api/v1/status');
|
||||
|
||||
if (setLocale) {
|
||||
setLocale(
|
||||
@@ -252,7 +252,12 @@ const SettingsMain: React.FC = () => {
|
||||
</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>
|
||||
<Badge badgeType="primary">
|
||||
{intl.formatMessage(globalMessages.restartRequired)}
|
||||
</Badge>
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.trustProxyTip)}
|
||||
</span>
|
||||
@@ -273,9 +278,12 @@ const SettingsMain: React.FC = () => {
|
||||
<span className="mr-2">
|
||||
{intl.formatMessage(messages.csrfProtection)}
|
||||
</span>
|
||||
<Badge badgeType="danger">
|
||||
<Badge badgeType="danger" className="mr-2">
|
||||
{intl.formatMessage(globalMessages.advanced)}
|
||||
</Badge>
|
||||
<Badge badgeType="primary">
|
||||
{intl.formatMessage(globalMessages.restartRequired)}
|
||||
</Badge>
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.csrfProtectionTip)}
|
||||
</span>
|
||||
|
@@ -43,6 +43,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 {
|
||||
@@ -256,7 +257,7 @@ const SettingsServices: React.FC = () => {
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Modal
|
||||
okText="Delete"
|
||||
okText={intl.formatMessage(globalMessages.delete)}
|
||||
okButtonType="danger"
|
||||
onOk={() => deleteServer()}
|
||||
onCancel={() =>
|
||||
@@ -266,7 +267,10 @@ const SettingsServices: React.FC = () => {
|
||||
type: 'radarr',
|
||||
})
|
||||
}
|
||||
title="Delete Server"
|
||||
title={intl.formatMessage(messages.deleteServer, {
|
||||
serverType:
|
||||
deleteServerModal.type === 'radarr' ? 'Radarr' : 'Sonarr',
|
||||
})}
|
||||
iconSvg={<TrashIcon />}
|
||||
>
|
||||
{intl.formatMessage(messages.deleteserverconfirm)}
|
||||
|
@@ -1,54 +0,0 @@
|
||||
import { SparklesIcon } from '@heroicons/react/outline';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
import type { StatusResponse } from '../../../server/interfaces/api/settingsInterfaces';
|
||||
import Modal from '../Common/Modal';
|
||||
import Transition from '../Transition';
|
||||
|
||||
const messages = defineMessages({
|
||||
newversionavailable: 'Application Update',
|
||||
newversionDescription:
|
||||
'Overseerr has been updated! Please click the button below to reload the page.',
|
||||
reloadOverseerr: 'Reload',
|
||||
});
|
||||
|
||||
const StatusChecker: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { data, error } = useSWR<StatusResponse>('/api/v1/status', {
|
||||
refreshInterval: 60 * 1000,
|
||||
});
|
||||
|
||||
if (!data && !error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
appear
|
||||
show={data.commitTag !== process.env.commitTag}
|
||||
>
|
||||
<Modal
|
||||
iconSvg={<SparklesIcon />}
|
||||
title={intl.formatMessage(messages.newversionavailable)}
|
||||
onOk={() => location.reload()}
|
||||
okText={intl.formatMessage(messages.reloadOverseerr)}
|
||||
backgroundClickable={false}
|
||||
>
|
||||
{intl.formatMessage(messages.newversionDescription)}
|
||||
</Modal>
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusChecker;
|
94
src/components/StatusChecker/index.tsx
Normal file
94
src/components/StatusChecker/index.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { RefreshIcon, SparklesIcon } from '@heroicons/react/outline';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
import type { StatusResponse } from '../../../server/interfaces/api/settingsInterfaces';
|
||||
import useSettings from '../../hooks/useSettings';
|
||||
import { Permission, useUser } from '../../hooks/useUser';
|
||||
import globalMessages from '../../i18n/globalMessages';
|
||||
import Modal from '../Common/Modal';
|
||||
import Transition from '../Transition';
|
||||
|
||||
const messages = defineMessages({
|
||||
appUpdated: '{applicationTitle} Updated',
|
||||
appUpdatedDescription:
|
||||
'Please click the button below to reload the application.',
|
||||
reloadApp: 'Reload {applicationTitle}',
|
||||
restartRequired: 'Server Restart Required',
|
||||
restartRequiredDescription:
|
||||
'Please restart the server to apply the updated settings.',
|
||||
});
|
||||
|
||||
const StatusChecker: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const settings = useSettings();
|
||||
const { hasPermission } = useUser();
|
||||
const { data, error } = useSWR<StatusResponse>('/api/v1/status', {
|
||||
refreshInterval: 60 * 1000,
|
||||
});
|
||||
const [alertDismissed, setAlertDismissed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data?.restartRequired) {
|
||||
setAlertDismissed(false);
|
||||
}
|
||||
}, [data?.restartRequired]);
|
||||
|
||||
if (!data && !error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition
|
||||
enter="opacity-0 transition duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="opacity-100 transition duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
appear
|
||||
show={
|
||||
!alertDismissed &&
|
||||
((hasPermission(Permission.ADMIN) && data.restartRequired) ||
|
||||
data.commitTag !== process.env.commitTag)
|
||||
}
|
||||
>
|
||||
{hasPermission(Permission.ADMIN) && data.restartRequired ? (
|
||||
<Modal
|
||||
iconSvg={<RefreshIcon />}
|
||||
title={intl.formatMessage(messages.restartRequired)}
|
||||
backgroundClickable={false}
|
||||
onOk={() => {
|
||||
setAlertDismissed(true);
|
||||
if (data.commitTag !== process.env.commitTag) {
|
||||
location.reload();
|
||||
}
|
||||
}}
|
||||
okText={intl.formatMessage(globalMessages.close)}
|
||||
>
|
||||
{intl.formatMessage(messages.restartRequiredDescription)}
|
||||
</Modal>
|
||||
) : (
|
||||
<Modal
|
||||
iconSvg={<SparklesIcon />}
|
||||
title={intl.formatMessage(messages.appUpdated, {
|
||||
applicationTitle: settings.currentSettings.applicationTitle,
|
||||
})}
|
||||
onOk={() => location.reload()}
|
||||
okText={intl.formatMessage(messages.reloadApp, {
|
||||
applicationTitle: settings.currentSettings.applicationTitle,
|
||||
})}
|
||||
backgroundClickable={false}
|
||||
>
|
||||
{intl.formatMessage(messages.appUpdatedDescription)}
|
||||
</Modal>
|
||||
)}
|
||||
</Transition>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusChecker;
|
@@ -336,7 +336,7 @@ const UserList: React.FC = () => {
|
||||
type="warning"
|
||||
/>
|
||||
)}
|
||||
{currentHasPermission(Permission.MANAGE_SETTINGS) &&
|
||||
{currentHasPermission(Permission.ADMIN) &&
|
||||
!passwordGenerationEnabled && (
|
||||
<Alert
|
||||
title={intl.formatMessage(
|
||||
|
@@ -54,10 +54,7 @@ const UserSettings: React.FC = ({ children }) => {
|
||||
regex: /\/settings\/password/,
|
||||
hidden:
|
||||
(!settings.currentSettings.localLogin &&
|
||||
!hasPermission(
|
||||
Permission.MANAGE_SETTINGS,
|
||||
currentUser?.permissions ?? 0
|
||||
)) ||
|
||||
!hasPermission(Permission.ADMIN, currentUser?.permissions ?? 0)) ||
|
||||
(currentUser?.id !== 1 &&
|
||||
currentUser?.id !== user?.id &&
|
||||
hasPermission(Permission.ADMIN, user?.permissions ?? 0)),
|
||||
|
Reference in New Issue
Block a user