jam411 il y a 3 ans
Parent
commit
a918fe15b2

+ 2 - 6
packages/app/src/components/Admin/PluginsExtension/Loading.js → packages/app/src/components/Admin/PluginsExtension/Loading.tsx

@@ -1,13 +1,9 @@
-import {
-  Spinner,
-} from 'reactstrap';
+import { Spinner } from 'reactstrap';
 
-const Loading = () => {
+export const Loading = (): JSX.Element => {
   return (
     <Spinner className='d-flex justify-content-center aligh-items-center'>
       Loading...
     </Spinner>
   );
 };
-
-export default Loading;

+ 86 - 26
packages/app/src/components/Admin/PluginsExtension/PluginCard.tsx

@@ -1,26 +1,99 @@
-// import { faCircleArrowDown, faCircleCheck } from '@fortawesome/free-solid-svg-icons';
-// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import React, { useState } from 'react';
 
 import Link from 'next/link';
 
-import styles from './PluginCard.module.scss';
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Post } from '~/client/util/apiv3-client';
+import { useSWRxPlugin } from '~/stores/plugin';
 
+import styles from './PluginCard.module.scss';
 
 type Props = {
+  id: string,
   name: string,
   url: string,
   description: string,
 }
 
 export const PluginCard = (props: Props): JSX.Element => {
+
   const {
-    name, url, description,
+    id, name, url, description,
   } = props;
-  // const [isEnabled, setIsEnabled] = useState(true);
 
-  // const checkboxHandler = useCallback(() => {
-  //   setIsEnabled(false);
-  // }, []);
+  const { data, mutate } = useSWRxPlugin(id);
+
+  if (data == null) {
+    return <></>;
+  }
+
+  const PluginCardButton = (): JSX.Element => {
+    const [isEnabled, setState] = useState<boolean>(data.data.isEnabled);
+
+    const onChangeHandler = async() => {
+      const reqUrl = '/plugins-extension/switch-isenabled';
+
+      try {
+        const res = await apiv3Post(reqUrl, { _id: id });
+        setState(res.data.isEnabled);
+        const pluginState = !isEnabled ? 'Enabled' : 'Disabled';
+        toastSuccess(`${pluginState} Plugin `);
+      }
+      catch (err) {
+        toastError('pluginIsEnabled', err);
+      }
+      finally {
+        mutate();
+      }
+    };
+
+    return (
+      <div className={`${styles.plugin_card}`}>
+        <div className="switch">
+          <label className="switch__label">
+            <input
+              type="checkbox"
+              className="switch__input"
+              onChange={() => onChangeHandler()}
+              checked={isEnabled}
+            />
+            <span className="switch__content"></span>
+            <span className="switch__circle"></span>
+          </label>
+        </div>
+      </div>
+    );
+  };
+
+  const PluginDeleteButton = (): JSX.Element => {
+
+    const onClickPluginDeleteBtnHandler = async() => {
+      const reqUrl = '/plugins-extension/deleted';
+
+      try {
+        await apiv3Post(reqUrl, { _id: id, name });
+        toastSuccess(`${name} Deleted`);
+      }
+      catch (err) {
+        toastError('pluginDelete', err);
+      }
+      finally {
+        mutate();
+      }
+    };
+
+    return (
+      <div className="">
+        <button
+          type="submit"
+          className="btn btn-primary"
+          onClick={() => onClickPluginDeleteBtnHandler()}
+        >
+          Delete
+        </button>
+      </div>
+    );
+  };
 
   return (
     <div className="card shadow border-0" key={name}>
@@ -33,25 +106,12 @@ export const PluginCard = (props: Props): JSX.Element => {
             <p className="card-text text-muted">{description}</p>
           </div>
           <div className='col-3'>
-            <div className={`${styles.plugin_card}`}>
-              <div className="switch">
-                <label className="switch__label">
-                  <input type="checkbox" className="switch__input" checked/>
-                  <span className="switch__content"></span>
-                  <span className="switch__circle"></span>
-                </label>
-              </div>
+            <div>
+              <PluginCardButton />
+            </div>
+            <div className="mt-4">
+              <PluginDeleteButton />
             </div>
-            {/* <div className="custom-control custom-switch custom-switch-lg custom-switch-slack">
-              <input
-                type="checkbox"
-                className="custom-control-input border-0"
-                checked={isEnabled}
-                onChange={checkboxHandler}
-              />
-              <label className="custom-control-label align-center"></label>
-            </div> */}
-            {/* <Image className="mx-auto" alt="GitHub avator image" src={owner.avatar_url} width={250} height={250} /> */}
           </div>
         </div>
         <div className="row">

+ 6 - 34
packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx

@@ -1,16 +1,11 @@
 import React, { useCallback } from 'react';
 
-import { useTranslation } from 'react-i18next';
-
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
-
-import AdminInstallButtonRow from '../Common/AdminUpdateButtonRow';
-// TODO: error notification (toast, loggerFactory)
+import { useSWRxPlugins } from '~/stores/plugin';
 // TODO: i18n
-
 export const PluginInstallerForm = (): JSX.Element => {
-  // const { t } = useTranslation('admin');
+  const { mutate } = useSWRxPlugins();
 
   const submitHandler = useCallback(async(e) => {
     e.preventDefault();
@@ -37,7 +32,10 @@ export const PluginInstallerForm = (): JSX.Element => {
       toastError(err);
       // logger.error(err);
     }
-  }, []);
+    finally {
+      mutate();
+    }
+  }, [mutate]);
 
   return (
     <form role="form" onSubmit={submitHandler}>
@@ -47,39 +45,13 @@ export const PluginInstallerForm = (): JSX.Element => {
           <input
             className="form-control"
             type="text"
-            // defaultValue={adminAppContainer.state.title || ''}
             name="pluginInstallerForm[url]"
             placeholder="https://github.com/weseek/growi-plugin-lsx"
             required
           />
           <p className="form-text text-muted">You can install plugins by inputting the GitHub URL.</p>
-          {/* <p className="form-text text-muted">{t('admin:app_setting.sitename_change')}</p> */}
-        </div>
-      </div>
-      {/* <div className='form-group row'>
-        <label className="text-left text-md-right col-md-3 col-form-label">branch</label>
-        <div className="col-md-6">
-          <input
-            className="form-control"
-            type="text"
-            name="pluginInstallerForm[ghBranch]"
-            placeholder="main"
-          />
-          <p className="form-text text-muted">branch name</p>
         </div>
       </div>
-      <div className='form-group row'>
-        <label className="text-left text-md-right col-md-3 col-form-label">tag</label>
-        <div className="col-md-6">
-          <input
-            className="form-control"
-            type="text"
-            name="pluginInstallerForm[ghTag]"
-            placeholder="tags"
-          />
-          <p className="form-text text-muted">tag name</p>
-        </div>
-      </div> */}
 
       <div className="row my-3">
         <div className="mx-auto">

+ 49 - 28
packages/app/src/components/Admin/PluginsExtension/PluginsExtensionPageContents.tsx

@@ -1,21 +1,38 @@
 import React from 'react';
 
-import type { SearchResultItem } from '~/interfaces/github-api';
-import { useInstalledPlugins } from '~/stores/useInstalledPlugins';
+import { useSWRxPlugins } from '~/stores/plugin';
 
-import Loading from './Loading';
+import { Loading } from './Loading';
 import { PluginCard } from './PluginCard';
 import { PluginInstallerForm } from './PluginInstallerForm';
-
-
 // TODO: i18n
 
 export const PluginsExtensionPageContents = (): JSX.Element => {
-  // const { data, error } = useInstalledPlugins();
+  const { data, mutate } = useSWRxPlugins();
+
+  if (data?.data?.plugins == null) {
+    return (
+      <>
+        <div className="row mb-5">
+          <div className="col-lg-12">
+            <h2 className="admin-setting-header">Plugin Installer</h2>
+            <PluginInstallerForm />
+          </div>
+        </div>
+        <div className="row mb-5">
+          <div className="col-lg-12">
+            <h2 className="admin-setting-header">Plugins
+              <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => mutate()}>
+                <i className="icon icon-reload"></i>
+              </button>
+            </h2>
+            <Loading />
+          </div>
+        </div>
+      </>
 
-  // if (data == null) {
-  //   return <Loading />;
-  // }
+    );
+  }
 
   return (
     <div>
@@ -29,26 +46,30 @@ export const PluginsExtensionPageContents = (): JSX.Element => {
 
       <div className="row mb-5">
         <div className="col-lg-12">
-          <h2 className="admin-setting-header">Plugins</h2>
+          <h2 className="admin-setting-header">Plugins
+            <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => mutate()}>
+              <i className="icon icon-reload"></i>
+            </button>
+          </h2>
           <div className="d-grid gap-5">
-            <PluginCard
-              name={'growi-plugin-templates-for-office'}
-              url={'https://github.com/weseek/growi-plugin-templates-for-office'}
-              description={'GROWI markdown templates for office.'}
-            />
-            {/* <PluginCard
-              name={'growi-plugin-theme-welcome-to-fumiya-room'}
-              url={'https://github.com/weseek/growi-plugin-theme-welcome-to-fumiya-room'}
-              description={'Welcome to fumiya\'s room! This is very very "latest" design...'}
-            /> */}
-            <PluginCard
-              name={'growi-plugin-copy-code-to-clipboard'}
-              url={'https://github.com/weseek/growi-plugin-copy-code-to-clipboard'}
-              description={'Add copy button on code blocks.'}
-            />
-            {/* {data?.items.map((item: SearchResultItem) => {
-              return <PluginCard key={item.name} {...item} />;
-            })} */}
+            { data?.data?.plugins.length === 0 && (
+              <div>Plugin is not installed</div>
+            )}
+            { data?.data?.plugins.map((item) => {
+              const pluginId = item[0]._id;
+              const pluginName = item[0].meta.name;
+              const pluginUrl = item[0].origin.url;
+              const pluginDiscription = item[0].meta.desc;
+              return (
+                <PluginCard
+                  key={pluginId}
+                  id={pluginId}
+                  name={pluginName}
+                  url={pluginUrl}
+                  description={pluginDiscription}
+                />
+              );
+            })}
           </div>
         </div>
       </div>

+ 138 - 0
packages/app/src/components/Page/RevisionLoader.jsx

@@ -0,0 +1,138 @@
+import React from 'react';
+
+import { useTranslation } from 'next-i18next';
+import PropTypes from 'prop-types';
+import { Waypoint } from 'react-waypoint';
+
+import { apiv3Get } from '~/client/util/apiv3-client';
+import loggerFactory from '~/utils/logger';
+
+import RevisionRenderer from './RevisionRenderer';
+
+
+/**
+ * Load data from server and render RevisionBody component
+ */
+class RevisionLoader extends React.Component {
+
+  constructor(props) {
+    super(props);
+    this.logger = loggerFactory('growi:Page:RevisionLoader');
+
+    this.state = {
+      markdown: null,
+      isLoading: false,
+      isLoaded: false,
+      errors: null,
+    };
+
+    this.loadData = this.loadData.bind(this);
+    this.onWaypointChange = this.onWaypointChange.bind(this);
+  }
+
+  UNSAFE_componentWillMount() {
+    if (!this.props.lazy) {
+      this.loadData();
+    }
+  }
+
+  async loadData() {
+    if (!this.state.isLoaded && !this.state.isLoading) {
+      this.setState({ isLoading: true });
+    }
+
+    const { pageId, revisionId } = this.props;
+
+
+    // load data with REST API
+    try {
+      const res = await apiv3Get(`/revisions/${revisionId}`, { pageId });
+
+      this.setState({
+        markdown: res.data?.revision?.body,
+        errors: null,
+      });
+
+      if (this.props.onRevisionLoaded != null) {
+        this.props.onRevisionLoaded(res.data.revision);
+      }
+    }
+    catch (errors) {
+      this.setState({ errors });
+    }
+    finally {
+      this.setState({ isLoaded: true, isLoading: false });
+    }
+
+  }
+
+  onWaypointChange(event) {
+    if (event.currentPosition === Waypoint.above || event.currentPosition === Waypoint.inside) {
+      this.loadData();
+    }
+  }
+
+  render() {
+    // ----- before load -----
+    if (this.props.lazy && !this.state.isLoaded) {
+      return (
+        <Waypoint onPositionChange={this.onWaypointChange} bottomOffset="-100px">
+          <div className="wiki"></div>
+        </Waypoint>
+      );
+    }
+
+    // ----- loading -----
+    if (this.state.isLoading) {
+      return (
+        <div className="wiki">
+          <div className="text-muted text-center">
+            <i className="fa fa-2x fa-spinner fa-pulse mr-1"></i>
+          </div>
+        </div>
+      );
+    }
+
+    // ----- after load -----
+    const isForbidden = this.state.errors != null && this.state.errors[0].code === 'forbidden-page';
+    let markdown = this.state.markdown;
+    if (isForbidden) {
+      markdown = `<i class="icon-exclamation p-1"></i>${this.props.t('not_allowed_to_see_this_page')}`;
+    }
+    else if (this.state.errors != null) {
+      const errorMessages = this.state.errors.map((error) => {
+        return `<i class="icon-exclamation p-1"></i><span class="text-muted"><em>${error.message}</em></span>`;
+      });
+      markdown = errorMessages.join('\n');
+    }
+
+    return (
+      <RevisionRenderer
+        rendererOptions={this.props.rendererOptions}
+        markdown={markdown}
+      />
+    );
+  }
+
+}
+
+
+RevisionLoader.propTypes = {
+  t: PropTypes.func.isRequired,
+
+  // eslint-disable-next-line no-undef
+  rendererOptions: PropTypes.instanceOf(RendererOptions).isRequired,
+  pageId: PropTypes.string.isRequired,
+  pagePath: PropTypes.string.isRequired,
+  revisionId: PropTypes.string.isRequired,
+  lazy: PropTypes.bool,
+  onRevisionLoaded: PropTypes.func,
+  highlightKeywords: PropTypes.arrayOf(PropTypes.string),
+};
+
+const RevisionLoaderWrapperFC = (props) => {
+  const { t } = useTranslation();
+  return <RevisionLoader t={t} {...props} />;
+};
+
+export default RevisionLoaderWrapperFC;

+ 6 - 0
packages/app/src/interfaces/github-api.ts

@@ -19,3 +19,9 @@ export type SearchResultItem = {
   homepage: string,
   stargazersCount: number,
 }
+
+export type PluginItem = {
+  name: string,
+  url: string,
+  description: string,
+}

+ 11 - 0
packages/app/src/models/SearchResult.ts

@@ -0,0 +1,11 @@
+import { SearchResultItem, PluginItem } from './SearchResultItem';
+
+export type SearchResult = {
+  total_count: number,
+  imcomplete_results: boolean,
+  items: SearchResultItem[];
+}
+
+export type PluginResult = {
+  items: PluginItem[];
+}

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

@@ -0,0 +1,358 @@
+import React from 'react';
+
+import { isClient, objectIdUtils } from '@growi/core';
+import mongoose from 'mongoose';
+import {
+  NextPage, GetServerSideProps, GetServerSidePropsContext,
+} from 'next';
+import { useTranslation } from 'next-i18next';
+import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
+import dynamic from 'next/dynamic';
+import { useRouter } from 'next/router';
+import { Container, Provider } from 'unstated';
+
+import AdminAppContainer from '~/client/services/AdminAppContainer';
+import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
+import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
+import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
+import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
+import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
+import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
+import AdminHomeContainer from '~/client/services/AdminHomeContainer';
+import AdminImportContainer from '~/client/services/AdminImportContainer';
+import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
+import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
+import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
+import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
+import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
+import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
+import AdminUsersContainer from '~/client/services/AdminUsersContainer';
+import { ActivatePluginService, GrowiPluginManifestEntries } from '~/client/services/activate-plugin';
+import { SupportedActionType } from '~/interfaces/activity';
+import { CrowiRequest } from '~/interfaces/crowi-request';
+import { GrowiPlugin } from '~/interfaces/plugin';
+import ConfigLoader from '~/server/service/config-loader';
+import {
+  useCurrentUser, /* useSearchServiceConfigured, */ useIsAclEnabled, useIsMailerSetup, useIsSearchServiceReachable, useSiteUrl,
+  useAuditLogEnabled, useAuditLogAvailableActions,
+} from '~/stores/context';
+import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
+
+import {
+  CommonProps, getServerSideCommonProps, getNextI18NextConfig,
+} from '../utils/commons';
+
+
+// import { useEnvVars } from '~/stores/admin-context';
+
+const AdminHome = dynamic(() => import('../../components/Admin/AdminHome/AdminHome'), { ssr: false });
+const AppSettingsPageContents = dynamic(() => import('../../components/Admin/App/AppSettingsPageContents'), { ssr: false });
+const SecurityManagement = dynamic(() => import('../../components/Admin/Security/SecurityManagement'), { ssr: false });
+const MarkDownSettingContents = dynamic(() => import('../../components/Admin/MarkdownSetting/MarkDownSettingContents'), { ssr: false });
+const CustomizeSettingContents = dynamic(() => import('../../components/Admin/Customize/Customize'), { ssr: false });
+const DataImportPageContents = dynamic(() => import('../../components/Admin/ImportData/ImportDataPageContents'), { ssr: false });
+const ExportArchiveDataPage = dynamic(() => import('../../components/Admin/ExportArchiveDataPage'), { ssr: false });
+const NotificationSetting = dynamic(() => import('../../components/Admin/Notification/NotificationSetting'), { ssr: false });
+const ManageGlobalNotification = dynamic(() => import('../../components/Admin/Notification/ManageGlobalNotification'), { ssr: false });
+const SlackIntegration = dynamic(() => import('../../components/Admin/SlackIntegration/SlackIntegration'), { ssr: false });
+const LegacySlackIntegration = dynamic(() => import('../../components/Admin/LegacySlackIntegration/LegacySlackIntegration'), { ssr: false });
+const UserManagement = dynamic(() => import('../../components/Admin/UserManagement'), { ssr: false });
+const ManageExternalAccount = dynamic(() => import('../../components/Admin/ManageExternalAccount'), { ssr: false });
+const ElasticsearchManagement = dynamic(() => import('../../components/Admin/ElasticsearchManagement/ElasticsearchManagement'), { ssr: false });
+const UserGroupDetailPage = dynamic(() => import('../../components/Admin/UserGroupDetail/UserGroupDetailPage'), { ssr: false });
+const AdminLayout = dynamic(() => import('../../components/Layout/AdminLayout'), { ssr: false });
+// named export
+const UserGroupPage = dynamic(() => import('../../components/Admin/UserGroup/UserGroupPage').then(mod => mod.UserGroupPage), { ssr: false });
+const AuditLogManagement = dynamic(() => import('../../components/Admin/AuditLogManagement').then(mod => mod.AuditLogManagement), { ssr: false });
+const PluginsExtensionPageContents = dynamic(() => import('../../components/Admin/PluginsExtension/PluginsExtensionPageContents')
+  .then(mod => mod.PluginsExtensionPageContents), { ssr: false });
+
+type Props = CommonProps & {
+  currentUser: any,
+
+  nodeVersion: string,
+  npmVersion: string,
+  yarnVersion: string,
+  installedPlugins: any,
+  envVars: any,
+  isAclEnabled: boolean,
+  isSearchServiceConfigured: boolean,
+  isSearchServiceReachable: boolean,
+  isMailerSetup: boolean,
+  auditLogEnabled: boolean,
+  auditLogAvailableActions: SupportedActionType[],
+
+  pluginManifestEntries: any,
+
+  siteUrl: string,
+};
+
+const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
+
+  const { t } = useTranslation('admin');
+  const router = useRouter();
+  const { path } = router.query;
+  const pagePathKeys: string[] = Array.isArray(path) ? path : ['home'];
+
+  /*
+  * Set userGroupId as a adminPagesMap key
+  * eg) In case that url is `/user-group-detail/62e8388a9a649bea5e703ef7`, userGroupId will be 62e8388a9a649bea5e703ef7
+  */
+  let userGroupId;
+  const [firstPath, secondPath] = pagePathKeys;
+  if (firstPath === 'user-group-detail') {
+    userGroupId = objectIdUtils.isValidObjectId(secondPath) ? secondPath : undefined;
+  }
+
+  // TODO: refactoring adminPagesMap => https://redmine.weseek.co.jp/issues/102694
+  const adminPagesMap = {
+    home: {
+      title:  t('wiki_management_home_page'),
+      component: <AdminHome
+        nodeVersion={props.nodeVersion}
+        npmVersion={props.npmVersion}
+        yarnVersion={props.yarnVersion}
+        // installedPlugins={props.installedPlugins}
+      />,
+    },
+    app: {
+      title: t('app_settings'),
+      component: <AppSettingsPageContents />,
+    },
+    security: {
+      title: t('security_settings.security_settings'),
+      component: <SecurityManagement />,
+    },
+    markdown: {
+      title: t('markdown_settings.markdown_settings'),
+      component: <MarkDownSettingContents />,
+    },
+    customize: {
+      title: t('customize_settings.customize_settings'),
+      component: <CustomizeSettingContents />,
+    },
+    importer: {
+      title: t('importer_management.import_data'),
+      component: <DataImportPageContents />,
+    },
+    export: {
+      title: t('export_archive_data'),
+      component: <ExportArchiveDataPage />,
+    },
+    notification: {
+      title: t('external_notification.external_notification'),
+      component: <NotificationSetting />,
+    },
+    'global-notification': {
+      new: {
+        title: t('external_notification.external_notification'),
+        component: <ManageGlobalNotification />,
+      },
+    },
+    'slack-integration': {
+      title: t('slack_integration.slack_integration'),
+      component: <SlackIntegration />,
+    },
+    'slack-integration-legacy': {
+      title: t('slack_integration_legacy.slack_integration_legacy'),
+      component: <LegacySlackIntegration />,
+    },
+    users: {
+      title: t('user_management.user_management'),
+      component: <UserManagement />,
+      'external-accounts': {
+        title: t('user_management.external_account'),
+        component: <ManageExternalAccount />,
+      },
+    },
+    'user-groups': {
+      title:  t('user_group_management.user_group_management'),
+      component: <UserGroupPage />,
+    },
+    'user-group-detail': {
+      [userGroupId]: {
+        title: t('user_group_management.user_group_management'),
+        component: <UserGroupDetailPage userGroupId={userGroupId} />,
+      },
+    },
+    search: {
+      title: t('full_text_search_management.full_text_search_management'),
+      component: <ElasticsearchManagement />,
+    },
+    'audit-log': {
+      title: t('audit_log_management.audit_log'),
+      component: <AuditLogManagement />,
+    },
+    'plugins-extension': {
+      title: 'Plugins Extension',
+      component: <PluginsExtensionPageContents />,
+    },
+  };
+
+  const getTargetPageToRender = (pagesMap, keys): {title: string, component: JSX.Element} => {
+    return keys.reduce((pagesMap, key) => {
+      return pagesMap[key];
+    }, pagesMap);
+  };
+
+  const targetPage = getTargetPageToRender(adminPagesMap, pagePathKeys);
+
+  useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
+  useIsMailerSetup(props.isMailerSetup);
+  useIsMaintenanceMode(props.isMaintenanceMode);
+
+  // useSearchServiceConfigured(props.isSearchServiceConfigured);
+  useIsSearchServiceReachable(props.isSearchServiceReachable);
+
+  useIsAclEnabled(props.isAclEnabled);
+  useSiteUrl(props.siteUrl);
+
+  // useEnvVars(props.envVars);
+
+  useAuditLogEnabled(props.auditLogEnabled);
+  useAuditLogAvailableActions(props.auditLogAvailableActions);
+
+  const injectableContainers: Container<any>[] = [];
+
+  if (isClient()) {
+    // Create unstated container instances (except Security)
+    const adminAppContainer = new AdminAppContainer();
+    const adminImportContainer = new AdminImportContainer();
+    const adminHomeContainer = new AdminHomeContainer();
+    const adminCustomizeContainer = new AdminCustomizeContainer();
+    const adminUsersContainer = new AdminUsersContainer();
+    const adminExternalAccountsContainer = new AdminExternalAccountsContainer();
+    const adminNotificationContainer = new AdminNotificationContainer();
+    const adminSlackIntegrationLegacyContainer = new AdminSlackIntegrationLegacyContainer();
+    const adminMarkDownContainer = new AdminMarkDownContainer();
+
+    injectableContainers.push(
+      adminAppContainer,
+      adminImportContainer,
+      adminHomeContainer,
+      adminCustomizeContainer,
+      adminUsersContainer,
+      adminExternalAccountsContainer,
+      adminNotificationContainer,
+      adminSlackIntegrationLegacyContainer,
+      adminMarkDownContainer,
+    );
+  }
+
+
+  const adminSecurityContainers: Container<any>[] = [];
+
+  if (isClient()) {
+    const adminSecuritySettingElem = document.getElementById('admin-security-setting');
+
+    if (adminSecuritySettingElem != null) {
+      // Create unstated container instances (Security)
+      const adminGeneralSecurityContainer = new AdminGeneralSecurityContainer();
+      const adminLocalSecurityContainer = new AdminLocalSecurityContainer();
+      const adminLdapSecurityContainer = new AdminLdapSecurityContainer();
+      const adminSamlSecurityContainer = new AdminSamlSecurityContainer();
+      const adminOidcSecurityContainer = new AdminOidcSecurityContainer();
+      const adminBasicSecurityContainer = new AdminBasicSecurityContainer();
+      const adminGoogleSecurityContainer = new AdminGoogleSecurityContainer();
+      const adminGitHubSecurityContainer = new AdminGitHubSecurityContainer();
+      const adminTwitterSecurityContainer = new AdminTwitterSecurityContainer();
+
+      adminSecurityContainers.push(
+        adminGeneralSecurityContainer,
+        adminLocalSecurityContainer,
+        adminLdapSecurityContainer,
+        adminSamlSecurityContainer,
+        adminOidcSecurityContainer,
+        adminBasicSecurityContainer,
+        adminGoogleSecurityContainer,
+        adminGitHubSecurityContainer,
+        adminTwitterSecurityContainer,
+      );
+    }
+  }
+
+
+  return (
+    <Provider inject={[...injectableContainers, ...adminSecurityContainers]}>
+      <AdminLayout title={targetPage.title} selectedNavOpt={firstPath}>
+        {targetPage.component}
+      </AdminLayout>
+    </Provider>
+  );
+};
+
+
+async function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi } = req;
+  const {
+    appService, mailService, aclService, searchService, activityService,
+  } = crowi;
+
+  props.siteUrl = appService.getSiteUrl();
+  props.nodeVersion = crowi.runtimeVersions.versions.node ? crowi.runtimeVersions.versions.node.version.version : null;
+  props.npmVersion = crowi.runtimeVersions.versions.npm ? crowi.runtimeVersions.versions.npm.version.version : null;
+  props.yarnVersion = crowi.runtimeVersions.versions.yarn ? crowi.runtimeVersions.versions.yarn.version.version : null;
+  // props.installedPlugins = crowi.pluginService.listPlugins();
+  props.envVars = await ConfigLoader.getEnvVarsForDisplay(true);
+  props.isAclEnabled = aclService.isAclEnabled();
+
+  props.isSearchServiceConfigured = searchService.isConfigured;
+  props.isSearchServiceReachable = searchService.isReachable;
+
+  props.isMailerSetup = mailService.isMailerSetup;
+
+  props.auditLogEnabled = crowi.configManager.getConfig('crowi', 'app:auditLogEnabled');
+  props.auditLogAvailableActions = activityService.getAvailableActions(false);
+}
+
+/**
+ * for Server Side Translations
+ * @param context
+ * @param props
+ * @param namespacesRequired
+ */
+async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
+  // preload all languages because of language lists in user setting
+  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired, true);
+  props._nextI18Next = nextI18NextConfig._nextI18Next;
+}
+
+async function getPluginProps(ctx, props): Promise<any> {
+  // const initialProps: DocumentInitialProps = await Document.getInitialProps(ctx);
+
+  const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
+  const growiPlugins = await GrowiPlugin.find({ isEnabled: true });
+  const pluginManifestEntries: GrowiPluginManifestEntries = await ActivatePluginService.retrievePluginManifests(growiPlugins);
+  props.pluginManifestEntries = JSON.parse(JSON.stringify(pluginManifestEntries));
+}
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const req: CrowiRequest = context.req as CrowiRequest;
+
+  const { user } = req;
+  const result = await getServerSideCommonProps(context);
+
+  // check for presence
+  // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
+  if (!('props' in result)) {
+    throw new Error('invalid getSSP result');
+  }
+  const props: Props = result.props as Props;
+  if (user != null) {
+    // props.currentUser = JSON.stringify(user.toObject());
+    props.currentUser = JSON.stringify(user);
+  }
+
+  injectServerConfigurations(context, props);
+  await injectNextI18NextConfigurations(context, props, ['admin']);
+  await getPluginProps(context, props);
+
+  return {
+    props,
+  };
+};
+
+export default AdminMarkdownSettingsPage;

+ 56 - 1
packages/app/src/server/routes/apiv3/plugins-extension.ts

@@ -10,6 +10,20 @@ module.exports = (crowi: Crowi) => {
   const router = express.Router();
   const { pluginService } = crowi;
 
+  router.get('/', async(req: any, res: any) => {
+    if (pluginService == null) {
+      return res.apiv3Err(400);
+    }
+
+    try {
+      const data = await pluginService.getPlugins();
+      return res.apiv3({ plugins: data });
+    }
+    catch (err) {
+      return res.apiv3Err(err, 400);
+    }
+  });
+
   router.post('/', async(req: PluginInstallerFormRequest, res: ApiV3Response) => {
     if (pluginService == null) {
       return res.apiv3Err(400);
@@ -20,7 +34,48 @@ module.exports = (crowi: Crowi) => {
       return res.apiv3({});
     }
     catch (err) {
-      // TODO: error handling
+      return res.apiv3Err(err, 400);
+    }
+  });
+
+  router.post('/get-isenabled', async(req: any, res: ApiV3Response) => {
+    if (pluginService == null) {
+      return res.apiv3Err(400);
+    }
+
+    try {
+      const pluginIsEnabled = await pluginService.getPluginIsEnabled(req.body._id);
+      return res.apiv3({ isEnabled: pluginIsEnabled });
+    }
+    catch (err) {
+      return res.apiv3Err(err, 400);
+    }
+  });
+
+  router.post('/switch-isenabled', async(req: any, res: ApiV3Response) => {
+    if (pluginService == null) {
+      return res.apiv3Err(400);
+    }
+
+    try {
+      const pluginIsEnabled = await pluginService.switchPluginIsEnabled(req.body._id);
+      return res.apiv3({ isEnabled: pluginIsEnabled });
+    }
+    catch (err) {
+      return res.apiv3Err(err, 400);
+    }
+  });
+
+  router.post('/deleted', async(req: any, res: ApiV3Response) => {
+    if (pluginService == null) {
+      return res.apiv3Err(400);
+    }
+
+    try {
+      await pluginService.pluginDeleted(req.body._id, req.body.name);
+      return res.apiv3();
+    }
+    catch (err) {
       return res.apiv3Err(err, 400);
     }
   });

+ 44 - 2
packages/app/src/server/service/plugin.ts

@@ -1,10 +1,10 @@
 import { execSync } from 'child_process';
-import fs from 'fs';
 import path from 'path';
 
 import mongoose from 'mongoose';
 
-import type { GrowiPlugin, GrowiPluginMeta, GrowiPluginOrigin } from '~/interfaces/plugin';
+import { ActivatePluginService, GrowiPluginManifestEntries } from '~/client/services/activate-plugin';
+import { GrowiPlugin, GrowiPluginOrigin } from '~/interfaces/plugin';
 import loggerFactory from '~/utils/logger';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 
@@ -61,6 +61,7 @@ export class PluginService {
     const plugins = await PluginService.detectPlugins(origin, installedPath);
     await this.savePluginMetaData(plugins);
 
+
     return;
   }
 
@@ -85,6 +86,13 @@ export class PluginService {
     await GrowiPlugin.insertMany(plugins);
   }
 
+  async getPlugins(): Promise<any> {
+    const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
+    const growiPlugins = await GrowiPlugin.find({});
+    const pluginManifestEntries: GrowiPluginManifestEntries = await ActivatePluginService.retrievePluginManifests(growiPlugins);
+    return JSON.parse(JSON.stringify(pluginManifestEntries));
+  }
+
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
   static async detectPlugins(origin: GrowiPluginOrigin, installedPath: string, parentPackageJson?: any): Promise<GrowiPlugin[]> {
     const packageJsonPath = path.resolve(pluginStoringPath, installedPath, 'package.json');
@@ -135,5 +143,39 @@ export class PluginService {
     return [];
   }
 
+  /**
+   * Get plugin isEnabled
+   */
+  async getPluginIsEnabled(targetPluginId: string): Promise<any> {
+    const GrowiPlugin = await mongoose.model<GrowiPlugin>('GrowiPlugin');
+    const growiPlugins = await GrowiPlugin.find({ _id: targetPluginId });
+    return growiPlugins[0].isEnabled;
+  }
+
+  /**
+   * Switch plugin enabled
+   */
+  async switchPluginIsEnabled(targetPluginId: string): Promise<any> {
+    const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
+    const growiPlugins = await GrowiPlugin.find({ _id: targetPluginId });
+    await growiPlugins[0].update(
+      { isEnabled: !growiPlugins[0].isEnabled },
+    );
+    return growiPlugins[0].isEnabled;
+  }
+
+  /**
+   * Delete plugin
+   */
+  async pluginDeleted(targetPluginId: string, targetPluginName: string): Promise<any> {
+    const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
+    const growiPlugins = await GrowiPlugin.find({ _id: targetPluginId });
+    growiPlugins[0].remove();
+    // TODO: Check remove
+    const ghOrganizationName = 'weseek';
+    const unzipTargetPath = path.join(pluginStoringPath, ghOrganizationName);
+    execSync(`rm -rf ${unzipTargetPath}/${targetPluginName}`);
+    return [];
+  }
 
 }

+ 41 - 0
packages/app/src/stores/plugin.tsx

@@ -0,0 +1,41 @@
+
+import useSWR, { SWRResponse } from 'swr';
+
+import { apiv3Get, apiv3Post } from '~/client/util/apiv3-client';
+
+// TODO: Correct types
+const pluginsFetcher = () => {
+  return async() => {
+    const reqUrl = '/plugins-extension';
+    try {
+      const data = await apiv3Get(reqUrl);
+      return data;
+    }
+    catch (err) {
+      // TODO: Error handling
+      console.log('err', err);
+    }
+  };
+};
+
+export const useSWRxPlugins = (): SWRResponse<any | null, Error> => {
+  return useSWR('/pluginsExtension', pluginsFetcher());
+};
+
+const pluginFetcher = (id: string) => {
+  return async() => {
+    const reqUrl = '/plugins-extension/get-isenabled';
+    try {
+      const data = await apiv3Post(reqUrl, { _id: id });
+      return data;
+    }
+    catch (err) {
+      // TODO: Error handling
+      console.log('pluginFetcher', err);
+    }
+  };
+};
+
+export const useSWRxPlugin = (_id: string): SWRResponse<any | null, Error> => {
+  return useSWR(`/plugin-${_id}`, pluginFetcher(_id));
+};