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:
TheCatLady
2022-08-16 09:58:11 -07:00
committed by GitHub
parent 70dc4c4b3b
commit f3e56da3b7
40 changed files with 239 additions and 140 deletions

View File

@@ -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>
)}

View File

@@ -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',
},
];

View File

@@ -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),

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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)}

View File

@@ -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;

View 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;

View File

@@ -336,7 +336,7 @@ const UserList: React.FC = () => {
type="warning"
/>
)}
{currentHasPermission(Permission.MANAGE_SETTINGS) &&
{currentHasPermission(Permission.ADMIN) &&
!passwordGenerationEnabled && (
<Alert
title={intl.formatMessage(

View File

@@ -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)),