Просмотр исходного кода

Merge pull request #6499 from weseek/feat/103073-rendering-maintenance-mode

feat: Rendering maintenance mode
Shun Miyazawa 3 лет назад
Родитель
Сommit
0be2d73ad6

+ 0 - 1
packages/app/_obsolete/src/client/app.jsx

@@ -20,7 +20,6 @@ import Fab from '../components/Fab';
 import ForbiddenPage from '../components/ForbiddenPage';
 import ForbiddenPage from '../components/ForbiddenPage';
 import RecentlyCreatedIcon from '../components/Icons/RecentlyCreatedIcon';
 import RecentlyCreatedIcon from '../components/Icons/RecentlyCreatedIcon';
 import InAppNotificationPage from '../components/InAppNotification/InAppNotificationPage';
 import InAppNotificationPage from '../components/InAppNotification/InAppNotificationPage';
-import MaintenanceModeContent from '../components/MaintenanceModeContent';
 import PersonalSettings from '../components/Me/PersonalSettings';
 import PersonalSettings from '../components/Me/PersonalSettings';
 import MyDraftList from '../components/MyDraftList/MyDraftList';
 import MyDraftList from '../components/MyDraftList/MyDraftList';
 import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSubNavigation';
 import GrowiContextualSubNavigation from '../components/Navbar/GrowiContextualSubNavigation';

+ 7 - 3
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -4,20 +4,21 @@ import { useTranslation } from 'next-i18next';
 
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
+import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 import { toArrayIfNot } from '~/utils/array-utils';
 import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 
 import AppSetting from './AppSetting';
 import AppSetting from './AppSetting';
 import FileUploadSetting from './FileUploadSetting';
 import FileUploadSetting from './FileUploadSetting';
 import MailSetting from './MailSetting';
 import MailSetting from './MailSetting';
-import MaintenanceMode from './MaintenanceMode';
+import { MaintenanceMode } from './MaintenanceMode';
 import PluginSetting from './PluginSetting';
 import PluginSetting from './PluginSetting';
 import SiteUrlSetting from './SiteUrlSetting';
 import SiteUrlSetting from './SiteUrlSetting';
 import V5PageMigration from './V5PageMigration';
 import V5PageMigration from './V5PageMigration';
 
 
+
 const logger = loggerFactory('growi:appSettings');
 const logger = loggerFactory('growi:appSettings');
 
 
 type Props = {
 type Props = {
@@ -27,6 +28,9 @@ type Props = {
 const AppSettingsPageContents = (props: Props) => {
 const AppSettingsPageContents = (props: Props) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   const { adminAppContainer } = props;
   const { adminAppContainer } = props;
+
+  const { data: isMaintenanceMode } = useIsMaintenanceMode();
+
   const { isV5Compatible } = adminAppContainer.state;
   const { isV5Compatible } = adminAppContainer.state;
 
 
   useEffect(() => {
   useEffect(() => {
@@ -48,7 +52,7 @@ const AppSettingsPageContents = (props: Props) => {
     <div data-testid="admin-app-settings">
     <div data-testid="admin-app-settings">
       {
       {
         // Alert message will be displayed in case that the GROWI is under maintenance
         // Alert message will be displayed in case that the GROWI is under maintenance
-        adminAppContainer.state.isMaintenanceMode && (
+        isMaintenanceMode && (
           <div className="alert alert-danger alert-link" role="alert">
           <div className="alert alert-danger alert-link" role="alert">
             <h3 className="alert-heading">
             <h3 className="alert-heading">
               {t('admin:maintenance_mode.maintenance_mode')}
               {t('admin:maintenance_mode.maintenance_mode')}

+ 14 - 19
packages/app/src/components/Admin/App/MaintenanceMode.tsx

@@ -1,41 +1,38 @@
 import React, { FC, useState, useCallback } from 'react';
 import React, { FC, useState, useCallback } from 'react';
+
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import { ConfirmModal } from './ConfirmModal';
 import { ConfirmModal } from './ConfirmModal';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import AdminAppContainer from '~/client/services/AdminAppContainer';
 
 
 const logger = loggerFactory('growi:maintenanceMode');
 const logger = loggerFactory('growi:maintenanceMode');
 
 
-type Props = {
-  adminAppContainer: AdminAppContainer,
-};
 
 
-const MaintenanceMode: FC<Props> = (props: Props) => {
+export const MaintenanceMode: FC = () => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { adminAppContainer } = props;
+
+  const {
+    data: isMaintenanceMode, start: startMaintenanceMode, end: endMaintenanceMode,
+  } = useIsMaintenanceMode();
 
 
   const [isModalOpen, setModalOpen] = useState<boolean>(false);
   const [isModalOpen, setModalOpen] = useState<boolean>(false);
-  const [isMaintenanceMode, setMaintenanceMode] = useState<boolean | undefined>(adminAppContainer.state.isMaintenanceMode);
 
 
-  const openModal = () => { setModalOpen(true) };
-  const closeModal = () => { setModalOpen(false) };
+  const openModal = useCallback(() => { setModalOpen(true) }, []);
+
+  const closeModal = useCallback(() => { setModalOpen(false) }, []);
 
 
   const onConfirmHandler = useCallback(async() => {
   const onConfirmHandler = useCallback(async() => {
     closeModal();
     closeModal();
 
 
     try {
     try {
       if (isMaintenanceMode) {
       if (isMaintenanceMode) {
-        await adminAppContainer.endMaintenanceMode();
-        setMaintenanceMode(false);
+        endMaintenanceMode();
       }
       }
       else {
       else {
-        await adminAppContainer.startMaintenanceMode();
-        setMaintenanceMode(true);
+        startMaintenanceMode();
       }
       }
     }
     }
     catch (err) {
     catch (err) {
@@ -44,7 +41,7 @@ const MaintenanceMode: FC<Props> = (props: Props) => {
 
 
     // eslint-disable-next-line max-len
     // eslint-disable-next-line max-len
     toastSuccess(isMaintenanceMode ? t('admin:maintenance_mode.successfully_ended_maintenance_mode') : t('admin:maintenance_mode.successfully_started_maintenance_mode'));
     toastSuccess(isMaintenanceMode ? t('admin:maintenance_mode.successfully_ended_maintenance_mode') : t('admin:maintenance_mode.successfully_started_maintenance_mode'));
-  }, [isMaintenanceMode, adminAppContainer, closeModal]);
+  }, [isMaintenanceMode, closeModal, startMaintenanceMode, endMaintenanceMode, t]);
 
 
   return (
   return (
     <div className="mb-5">
     <div className="mb-5">
@@ -76,5 +73,3 @@ const MaintenanceMode: FC<Props> = (props: Props) => {
     </div>
     </div>
   );
   );
 };
 };
-
-export default withUnstatedContainers(MaintenanceMode, [AdminAppContainer]);

+ 0 - 55
packages/app/src/components/MaintenanceModeContent.tsx

@@ -1,55 +0,0 @@
-import React from 'react';
-
-import { useTranslation } from 'next-i18next';
-
-import { toastError } from '~/client/util/apiNotification';
-import { apiv3Post } from '~/client/util/apiv3-client';
-import { useCurrentUser } from '~/stores/context';
-
-
-const MaintenanceModeContent = () => {
-  const { t } = useTranslation();
-
-  const { data: currentUser } = useCurrentUser();
-
-  const logoutHandler = async() => {
-    try {
-      await apiv3Post('/logout');
-      window.location.reload();
-    }
-    catch (err) {
-      toastError(err);
-    }
-
-  };
-
-  return (
-    <div className="text-left">
-      {currentUser?.admin
-      && (
-        <p>
-          <i className="icon-arrow-right"></i>
-          <a className="btn btn-link" href="/admin">{ t('maintenance_mode.admin_page') }</a>
-        </p>
-      )}
-      {currentUser != null
-        ? (
-          <p>
-            <i className="icon-arrow-right"></i>
-            <a className="btn btn-link" onClick={logoutHandler} id="maintanounse-mode-logout">{ t('maintenance_mode.logout') }</a>
-          </p>
-        )
-        : (
-          <p>
-            <i className="icon-arrow-right"></i>
-            <a className="btn btn-link" href="/login">{ t('maintenance_mode.login') }</a>
-          </p>
-        )
-      }
-    </div>
-  );
-
-};
-
-
-export default MaintenanceModeContent;

+ 9 - 1
packages/app/src/pages/[[...path]].page.tsx

@@ -577,7 +577,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
 
   const result = await getServerSideCommonProps(context);
   const result = await getServerSideCommonProps(context);
 
 
-
   // check for presence
   // check for presence
   // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
   // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
   if (!('props' in result)) {
   if (!('props' in result)) {
@@ -586,6 +585,15 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
 
   const props: Props = result.props as Props;
   const props: Props = result.props as Props;
 
 
+  if (props.redirectDestination != null) {
+    return {
+      redirect: {
+        permanent: false,
+        destination: props.redirectDestination,
+      },
+    };
+  }
+
   if (user != null) {
   if (user != null) {
     props.currentUser = user.toObject();
     props.currentUser = user.toObject();
   }
   }

+ 2 - 0
packages/app/src/pages/admin/[[...path]].page.tsx

@@ -36,6 +36,7 @@ import {
   useCurrentUser, /* useSearchServiceConfigured, */ useIsAclEnabled, useIsMailerSetup, useIsSearchServiceReachable, useSiteUrl,
   useCurrentUser, /* useSearchServiceConfigured, */ useIsAclEnabled, useIsMailerSetup, useIsSearchServiceReachable, useSiteUrl,
   useAuditLogEnabled, useAuditLogAvailableActions,
   useAuditLogEnabled, useAuditLogAvailableActions,
 } from '~/stores/context';
 } from '~/stores/context';
+import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 
 import {
 import {
   CommonProps, getServerSideCommonProps, getNextI18NextConfig,
   CommonProps, getServerSideCommonProps, getNextI18NextConfig,
@@ -192,6 +193,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
 
 
   useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
   useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
   useIsMailerSetup(props.isMailerSetup);
   useIsMailerSetup(props.isMailerSetup);
+  useIsMaintenanceMode(props.isMaintenanceMode);
 
 
   // useSearchServiceConfigured(props.isSearchServiceConfigured);
   // useSearchServiceConfigured(props.isSearchServiceConfigured);
   useIsSearchServiceReachable(props.isSearchServiceReachable);
   useIsSearchServiceReachable(props.isSearchServiceReachable);

+ 119 - 0
packages/app/src/pages/maintenance.page.tsx

@@ -0,0 +1,119 @@
+import {
+  IUser, IUserHasId,
+} from '@growi/core';
+import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+
+import { toastError } from '~/client/util/apiNotification';
+import { apiv3Post } from '~/client/util/apiv3-client';
+import { CrowiRequest } from '~/interfaces/crowi-request';
+import { useCurrentUser } from '~/stores/context';
+
+import {
+  CommonProps, getServerSideCommonProps, getNextI18NextConfig,
+} from './utils/commons';
+
+
+type Props = CommonProps & {
+  currentUser: IUser,
+};
+
+const MaintenancePage: NextPage<CommonProps> = (props: Props) => {
+  const { t } = useTranslation();
+
+  useCurrentUser(props.currentUser ?? null);
+
+  const logoutHandler = async() => {
+    try {
+      await apiv3Post('/logout');
+      window.location.reload();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  };
+
+  return (
+    <div id="content-main" className="content-main container-lg">
+      <div className="container">
+        <div className="row justify-content-md-center">
+          <div className="col-md-6 mt-5">
+            <div className="text-center">
+              <h1><i className="icon-exclamation large"></i></h1>
+              <h1 className="text-center">{ t('maintenance_mode.maintenance_mode') }</h1>
+              <h3>{ t('maintenance_mode.growi_is_under_maintenance') }</h3>
+              <hr />
+              <div className="text-left">
+                {props.currentUser?.admin
+              && (
+                <p>
+                  <i className="icon-arrow-right"></i>
+                  <a className="btn btn-link" href="/admin/home">{ t('maintenance_mode.admin_page') }</a>
+                </p>
+              )}
+                {props.currentUser != null
+                  ? (
+                    <p>
+                      <i className="icon-arrow-right"></i>
+                      <a className="btn btn-link" onClick={logoutHandler} id="maintanounse-mode-logout">{ t('maintenance_mode.logout') }</a>
+                    </p>
+                  )
+                  : (
+                    <p>
+                      <i className="icon-arrow-right"></i>
+                      <a className="btn btn-link" href="/login">{ t('maintenance_mode.login') }</a>
+                    </p>
+                  )
+                }
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
+  props._nextI18Next = nextI18NextConfig._nextI18Next;
+}
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+
+  const result = await getServerSideCommonProps(context);
+
+  if ('redirect' in result) {
+    return { redirect: result.redirect };
+  }
+
+  if (!('props' in result)) {
+    throw new Error('invalid getSSP result');
+  }
+
+  const props: Props = result.props as Props;
+
+  if (props.redirectDestination != null) {
+    return {
+      redirect: {
+        permanent: false,
+        destination: props.redirectDestination,
+      },
+    };
+  }
+
+  const { user } = req;
+  if (user != null) {
+    props.currentUser = user.toObject();
+  }
+
+  await injectNextI18NextConfigurations(context, props, ['translation']);
+
+  return {
+    props,
+  };
+};
+
+export default MaintenancePage;

+ 9 - 0
packages/app/src/pages/utils/commons.ts

@@ -19,6 +19,8 @@ export type CommonProps = {
   csrfToken: string,
   csrfToken: string,
   isContainerFluid: boolean,
   isContainerFluid: boolean,
   growiVersion: string,
   growiVersion: string,
+  isMaintenanceMode: boolean,
+  redirectDestination: string | null,
 } & Partial<SSRConfig>;
 } & Partial<SSRConfig>;
 
 
 // eslint-disable-next-line max-len
 // eslint-disable-next-line max-len
@@ -33,6 +35,11 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
   const url = new URL(context.resolvedUrl, 'http://example.com');
   const url = new URL(context.resolvedUrl, 'http://example.com');
   const currentPathname = decodeURI(url.pathname);
   const currentPathname = decodeURI(url.pathname);
 
 
+  const isMaintenanceMode = appService.isMaintenanceMode();
+
+  // eslint-disable-next-line max-len, no-nested-ternary
+  const redirectDestination = !isMaintenanceMode && currentPathname === '/maintenance' ? '/' : isMaintenanceMode && !currentPathname.match('/admin/*') && !(currentPathname === '/maintenance') ? '/maintenance' : null;
+
   const props: CommonProps = {
   const props: CommonProps = {
     namespacesRequired: ['translation'],
     namespacesRequired: ['translation'],
     currentPathname,
     currentPathname,
@@ -44,6 +51,8 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     csrfToken: req.csrfToken(),
     csrfToken: req.csrfToken(),
     isContainerFluid: configManager.getConfig('crowi', 'customize:isContainerFluid') ?? false,
     isContainerFluid: configManager.getConfig('crowi', 'customize:isContainerFluid') ?? false,
     growiVersion: crowi.version,
     growiVersion: crowi.version,
+    isMaintenanceMode,
+    redirectDestination,
   };
   };
 
 
   return { props };
   return { props };

+ 13 - 2
packages/app/src/server/middlewares/unavailable-when-maintenance-mode.ts

@@ -4,7 +4,16 @@ import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:middlewares:unavailable-when-maintenance-mode');
 const logger = loggerFactory('growi:middlewares:unavailable-when-maintenance-mode');
 
 
-export const generateUnavailableWhenMaintenanceModeMiddleware = crowi => async(req: Request, res: Response, next: NextFunction): Promise<void> => {
+type Crowi = {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  nextApp: any,
+}
+
+type CrowiReq = Request & {
+  crowi: Crowi,
+}
+
+export const generateUnavailableWhenMaintenanceModeMiddleware = crowi => async(req: CrowiReq, res: Response, next: NextFunction): Promise<void> => {
   const isMaintenanceMode = crowi.appService.isMaintenanceMode();
   const isMaintenanceMode = crowi.appService.isMaintenanceMode();
 
 
   if (!isMaintenanceMode) {
   if (!isMaintenanceMode) {
@@ -12,7 +21,9 @@ export const generateUnavailableWhenMaintenanceModeMiddleware = crowi => async(r
     return;
     return;
   }
   }
 
 
-  res.render('maintenance-mode');
+  const { nextApp } = crowi;
+  req.crowi = crowi;
+  nextApp.render(req, res, '/maintenance');
 };
 };
 
 
 export const generateUnavailableWhenMaintenanceModeMiddlewareForApi = crowi => async(req: Request, res: Response, next: NextFunction): Promise<void> => {
 export const generateUnavailableWhenMaintenanceModeMiddlewareForApi = crowi => async(req: Request, res: Response, next: NextFunction): Promise<void> => {

+ 31 - 0
packages/app/src/stores/maintenanceMode.tsx

@@ -0,0 +1,31 @@
+import { withUtils, SWRResponseWithUtils } from '@growi/core/src/utils/with-utils';
+
+import { apiv3Post } from '~/client/util/apiv3-client';
+
+import { useStaticSWR } from './use-static-swr';
+
+
+type maintenanceModeUtils = {
+  start(): Promise<void>,
+  end(): Promise<void>,
+}
+
+export const useIsMaintenanceMode = (initialData?: boolean): SWRResponseWithUtils<maintenanceModeUtils, boolean> => {
+  const swrResult = useStaticSWR<boolean, Error>('isMaintenanceMode', initialData, { fallbackData: false });
+
+  const utils = {
+    start: async() => {
+      const { mutate } = swrResult;
+      await apiv3Post('/app-settings/maintenance-mode', { flag: true });
+      mutate(true);
+    },
+
+    end: async() => {
+      const { mutate } = swrResult;
+      await apiv3Post('/app-settings/maintenance-mode', { flag: false });
+      mutate(false);
+    },
+  };
+
+  return withUtils(swrResult, utils);
+};