jam411 3 лет назад
Родитель
Сommit
c6c41570a6

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

@@ -20,7 +20,6 @@ import Fab from '../components/Fab';
 import ForbiddenPage from '../components/ForbiddenPage';
 import RecentlyCreatedIcon from '../components/Icons/RecentlyCreatedIcon';
 import InAppNotificationPage from '../components/InAppNotification/InAppNotificationPage';
-import MaintenanceModeContent from '../components/MaintenanceModeContent';
 import PersonalSettings from '../components/Me/PersonalSettings';
 import MyDraftList from '../components/MyDraftList/MyDraftList';
 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 { toastError } from '~/client/util/apiNotification';
+import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 import { toArrayIfNot } from '~/utils/array-utils';
 import loggerFactory from '~/utils/logger';
 
-
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import AppSetting from './AppSetting';
 import FileUploadSetting from './FileUploadSetting';
 import MailSetting from './MailSetting';
-import MaintenanceMode from './MaintenanceMode';
+import { MaintenanceMode } from './MaintenanceMode';
 import PluginSetting from './PluginSetting';
 import SiteUrlSetting from './SiteUrlSetting';
 import V5PageMigration from './V5PageMigration';
 
+
 const logger = loggerFactory('growi:appSettings');
 
 type Props = {
@@ -27,6 +28,9 @@ type Props = {
 const AppSettingsPageContents = (props: Props) => {
   const { t } = useTranslation('admin');
   const { adminAppContainer } = props;
+
+  const { data: isMaintenanceMode } = useIsMaintenanceMode();
+
   const { isV5Compatible } = adminAppContainer.state;
 
   useEffect(() => {
@@ -48,7 +52,7 @@ const AppSettingsPageContents = (props: Props) => {
     <div data-testid="admin-app-settings">
       {
         // 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">
             <h3 className="alert-heading">
               {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 { useTranslation } from 'next-i18next';
 
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 import loggerFactory from '~/utils/logger';
 
-import { withUnstatedContainers } from '../../UnstatedUtils';
 import { ConfirmModal } from './ConfirmModal';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-
-import AdminAppContainer from '~/client/services/AdminAppContainer';
 
 const logger = loggerFactory('growi:maintenanceMode');
 
-type Props = {
-  adminAppContainer: AdminAppContainer,
-};
 
-const MaintenanceMode: FC<Props> = (props: Props) => {
+export const MaintenanceMode: FC = () => {
   const { t } = useTranslation();
-  const { adminAppContainer } = props;
+
+  const {
+    data: isMaintenanceMode, start: startMaintenanceMode, end: endMaintenanceMode,
+  } = useIsMaintenanceMode();
 
   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() => {
     closeModal();
 
     try {
       if (isMaintenanceMode) {
-        await adminAppContainer.endMaintenanceMode();
-        setMaintenanceMode(false);
+        endMaintenanceMode();
       }
       else {
-        await adminAppContainer.startMaintenanceMode();
-        setMaintenanceMode(true);
+        startMaintenanceMode();
       }
     }
     catch (err) {
@@ -44,7 +41,7 @@ const MaintenanceMode: FC<Props> = (props: Props) => {
 
     // eslint-disable-next-line max-len
     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 (
     <div className="mb-5">
@@ -76,5 +73,3 @@ const MaintenanceMode: FC<Props> = (props: Props) => {
     </div>
   );
 };
-
-export default withUnstatedContainers(MaintenanceMode, [AdminAppContainer]);

+ 1 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupPageList.tsx

@@ -6,7 +6,7 @@ import { toastError } from '~/client/util/apiNotification';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { IPageHasId } from '~/interfaces/page';
 
-import PageListItemS from '../../PageList/PageListItemS';
+import { PageListItemS } from '../../PageList/PageListItemS';
 import PaginationWrapper from '../../PaginationWrapper';
 
 const pagingLimit = 10;

+ 1 - 1
packages/app/src/components/ContentLinkButtons.tsx

@@ -1,9 +1,9 @@
 import React, { useCallback, useMemo } from 'react';
 
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
+import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 import { usePageUser } from '~/stores/context';
 
-import { RecentlyCreatedIcon } from './Icons/RecentlyCreatedIcon';
 
 const WIKI_HEADER_LINK = 120;
 

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

+ 15 - 28
packages/app/src/components/PageList/BookmarkList.jsx → packages/app/src/components/PageList/BookmarkList.tsx

@@ -1,27 +1,27 @@
 import React, { useState, useCallback, useEffect } from 'react';
 
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 
 import { toastError } from '~/client/util/apiNotification';
 import { apiv3Get } from '~/client/util/apiv3-client';
+import { MyBookmarkList } from '~/interfaces/bookmark-info';
 import loggerFactory from '~/utils/logger';
 
 import PaginationWrapper from '../PaginationWrapper';
 
-
-import PageListItemS from './PageListItemS';
-
+import { PageListItemS } from './PageListItemS';
 
 const logger = loggerFactory('growi:BookmarkList');
 
-const BookmarkList = (props) => {
-  const { t } = useTranslation();
+type BookmarkListProps = {
+  userId: string
+}
 
+export const BookmarkList = (props: BookmarkListProps): JSX.Element => {
   const { userId } = props;
 
-  const [pages, setPages] = useState([]);
-
+  const { t } = useTranslation();
+  const [pages, setPages] = useState<MyBookmarkList>([]);
   const [activePage, setActivePage] = useState(1);
   const [totalItemsCount, setTotalItemsCount] = useState(0);
   const [pagingLimit, setPagingLimit] = useState(10);
@@ -51,24 +51,18 @@ const BookmarkList = (props) => {
     getMyBookmarkList();
   }, [getMyBookmarkList]);
 
-  /**
-   * generate Elements of Page
-   *
-   * @param {any} pages Array of pages Model Obj
-   *
-   */
-  const generatePageList = pages.map(page => (
-    <li key={`my-bookmarks:${page._id}`} className="mt-4">
-      <PageListItemS page={page.page} />
-    </li>
-  ));
-
   return (
     <div className="bookmarks-list-container">
       {pages.length === 0 ? t('No bookmarks yet') : (
         <>
           <ul className="page-list-ul page-list-ul-flat mb-3">
-            {generatePageList}
+
+            {pages.map(page => (
+              <li key={`my-bookmarks:${page._id}`} className="mt-4">
+                <PageListItemS page={page.page} />
+              </li>
+            ))}
+
           </ul>
           <PaginationWrapper
             activePage={activePage}
@@ -82,11 +76,4 @@ const BookmarkList = (props) => {
       )}
     </div>
   );
-
 };
-
-BookmarkList.propTypes = {
-  userId: PropTypes.string.isRequired,
-};
-
-export default BookmarkList;

+ 0 - 39
packages/app/src/components/PageList/PageListItemS.jsx

@@ -1,39 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
-
-
-export default class PageListItemS extends React.Component {
-
-  render() {
-    const {
-      page, noLink,
-    } = this.props;
-
-    let pagePathElem = <PagePathLabel path={page.path} additionalClassNames={['mx-1']} />;
-    if (!noLink) {
-      pagePathElem = <a className="text-break" href={page.path}>{pagePathElem}</a>;
-    }
-
-    return (
-      <>
-        <UserPicture user={page.lastUpdateUser} noLink={noLink} />
-        {pagePathElem}
-        <span className="ml-2">
-          <PageListMeta page={page} />
-        </span>
-      </>
-    );
-  }
-
-}
-
-PageListItemS.propTypes = {
-  page: PropTypes.object.isRequired,
-  noLink: PropTypes.bool,
-};
-
-PageListItemS.defaultProps = {
-  noLink: false,
-};

+ 32 - 0
packages/app/src/components/PageList/PageListItemS.tsx

@@ -0,0 +1,32 @@
+import React from 'react';
+
+import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
+
+import { IPageHasId } from '~/interfaces/page';
+
+
+type PageListItemSProps = {
+  page: IPageHasId,
+  noLink?: boolean,
+}
+
+export const PageListItemS = (props: PageListItemSProps): JSX.Element => {
+
+  const { page, noLink = false } = props;
+
+  let pagePathElement = <PagePathLabel path={page.path} additionalClassNames={['mx-1']} />;
+  if (!noLink) {
+    pagePathElement = <a className="text-break" href={page.path}>{pagePathElement}</a>;
+  }
+
+  return (
+    <>
+      <UserPicture user={page.lastUpdateUser} noLink={noLink} />
+      {pagePathElement}
+      <span className="ml-2">
+        <PageListMeta page={page} />
+      </span>
+    </>
+  );
+
+};

+ 1 - 1
packages/app/src/components/RecentCreated/RecentCreated.tsx

@@ -5,7 +5,7 @@ import { apiv3Get } from '~/client/util/apiv3-client';
 import { IPageHasId } from '~/interfaces/page';
 import loggerFactory from '~/utils/logger';
 
-import PageListItemS from '../PageList/PageListItemS';
+import { PageListItemS } from '../PageList/PageListItemS';
 import PaginationWrapper from '../PaginationWrapper';
 
 const logger = loggerFactory('growi:RecentCreated');

+ 2 - 0
packages/app/src/components/UsersHomePageFooter.module.scss

@@ -1,3 +1,5 @@
+@use '~/styles/molecules/page_list';
+
 .user-page-footer :global {
   .grw-user-page-list-m {
     svg {

+ 3 - 4
packages/app/src/components/UsersHomePageFooter.tsx

@@ -3,7 +3,7 @@ import React from 'react';
 import { useTranslation } from 'next-i18next';
 
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
-import BookmarkList from '~/components/PageList/BookmarkList';
+import { BookmarkList } from '~/components/PageList/BookmarkList';
 import { RecentCreated } from '~/components/RecentCreated/RecentCreated';
 import styles from '~/components/UsersHomePageFooter.module.scss';
 
@@ -22,7 +22,7 @@ export const UsersHomePageFooter = (props: UsersHomePageFooterProps): JSX.Elemen
           <i style={{ fontSize: '1.3em' }} className="fa fa-fw fa-bookmark-o"></i>
           {t('footer.bookmarks')}
         </h2>
-        <div id="user-bookmark-list" className="page-list">
+        <div id="user-bookmark-list" className={`page-list ${styles['page-list']}`}>
           <BookmarkList userId={creatorId} />
         </div>
       </div>
@@ -31,11 +31,10 @@ export const UsersHomePageFooter = (props: UsersHomePageFooterProps): JSX.Elemen
           <i id="recent-created-icon" className="mr-1"><RecentlyCreatedIcon /></i>
           {t('footer.recently_created')}
         </h2>
-        <div id="user-created-list" className="page-list">
+        <div id="user-created-list" className={`page-list ${styles['page-list']}`}>
           <RecentCreated userId={creatorId} />
         </div>
       </div>
     </div>
   );
-
 };

+ 12 - 0
packages/app/src/interfaces/bookmark-info.ts

@@ -1,3 +1,6 @@
+import { Ref } from '@growi/core';
+
+import { IPageHasId } from '~/interfaces/page';
 import { IUser } from '~/interfaces/user';
 
 export type IBookmarkInfo = {
@@ -5,3 +8,12 @@ export type IBookmarkInfo = {
   isBookmarked: boolean,
   bookmarkedUsers: IUser[]
 };
+
+type BookmarkedPage = {
+  _id: string,
+  page: IPageHasId,
+  user: Ref<IUser>,
+  createdAt: Date,
+}
+
+export type MyBookmarkList = BookmarkedPage[]

+ 0 - 1
packages/app/src/pages/[[...path]].page.module.scss

@@ -1 +0,0 @@
-@use '~/styles/molecules/page_list';

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

@@ -333,7 +333,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
               { !isTopPagePath && (<Comments pageId={pageId} />) }
               { (pageWithMeta != null && isUsersHomePage(pageWithMeta.data.path)) && (
                 <UsersHomePageFooter creatorId={pageWithMeta.data.creator._id}/>
-              )}
+              ) }
               <PageContentFooter />
             </footer>
           )}
@@ -548,7 +548,6 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
   const result = await getServerSideCommonProps(context);
 
-
   // check for presence
   // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
   if (!('props' in result)) {
@@ -557,6 +556,15 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
   const props: Props = result.props as Props;
 
+  if (props.redirectDestination != null) {
+    return {
+      redirect: {
+        permanent: false,
+        destination: props.redirectDestination,
+      },
+    };
+  }
+
   if (user != null) {
     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,
   useAuditLogEnabled, useAuditLogAvailableActions,
 } from '~/stores/context';
+import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 import {
   CommonProps, getServerSideCommonProps, getNextI18NextConfig,
@@ -192,6 +193,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
 
   useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
   useIsMailerSetup(props.isMailerSetup);
+  useIsMaintenanceMode(props.isMaintenanceMode);
 
   // useSearchServiceConfigured(props.isSearchServiceConfigured);
   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,
   isContainerFluid: boolean,
   growiVersion: string,
+  isMaintenanceMode: boolean,
+  redirectDestination: string | null,
 } & Partial<SSRConfig>;
 
 // 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 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 = {
     namespacesRequired: ['translation'],
     currentPathname,
@@ -44,6 +51,8 @@ export const getServerSideCommonProps: GetServerSideProps<CommonProps> = async(c
     csrfToken: req.csrfToken(),
     isContainerFluid: configManager.getConfig('crowi', 'customize:isContainerFluid') ?? false,
     growiVersion: crowi.version,
+    isMaintenanceMode,
+    redirectDestination,
   };
 
   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');
 
-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();
 
   if (!isMaintenanceMode) {
@@ -12,7 +21,9 @@ export const generateUnavailableWhenMaintenanceModeMiddleware = crowi => async(r
     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> => {

+ 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);
+};