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

Merge pull request #1535 from weseek/reactify-admin/home-stocks

Reactify admin/home stocks
Yuki Takei 6 лет назад
Родитель
Сommit
c3c2886803

+ 1 - 0
package.json

@@ -124,6 +124,7 @@
     "nodemailer-ses-transport": "~1.5.0",
     "npm-run-all": "^4.1.2",
     "openid-client": "=2.5.0",
+    "package-installed-version-sync": "^2.1.0",
     "passport": "^0.4.0",
     "passport-github": "^1.1.0",
     "passport-google-oauth20": "^2.0.0",

+ 5 - 0
src/client/js/app.jsx

@@ -34,6 +34,7 @@ import MyDraftList from './components/MyDraftList/MyDraftList';
 import UserPictureList from './components/User/UserPictureList';
 import TableOfContents from './components/TableOfContents';
 
+import AdminHome from './components/Admin/AdminHome/AdminHome';
 import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
 import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
 import UserManagement from './components/Admin/UserManagement';
@@ -50,6 +51,7 @@ import PageContainer from './services/PageContainer';
 import CommentContainer from './services/CommentContainer';
 import EditorContainer from './services/EditorContainer';
 import TagContainer from './services/TagContainer';
+import AdminHomeContainer from './services/AdminHomeContainer';
 import AdminCustomizeContainer from './services/AdminCustomizeContainer';
 import UserGroupDetailContainer from './services/UserGroupDetailContainer';
 import AdminUsersContainer from './services/AdminUsersContainer';
@@ -157,11 +159,13 @@ Object.keys(componentMappings).forEach((key) => {
 });
 
 // create unstated container instance for admin
+const adminHomeContainer = new AdminHomeContainer(appContainer);
 const adminCustomizeContainer = new AdminCustomizeContainer(appContainer);
 const adminUsersContainer = new AdminUsersContainer(appContainer);
 const adminExternalAccountsContainer = new AdminExternalAccountsContainer(appContainer);
 const adminMarkDownContainer = new AdminMarkDownContainer(appContainer);
 const adminContainers = {
+  'admin-home': adminHomeContainer,
   'admin-customize': adminCustomizeContainer,
   'admin-user-page': adminUsersContainer,
   'admin-external-account-setting': adminExternalAccountsContainer,
@@ -189,6 +193,7 @@ if (adminAppElem != null) {
  *  value: React Element
  */
 const adminComponentMappings = {
+  'admin-home': <AdminHome />,
   'admin-customize': <Customize />,
   'admin-user-page': <UserManagement />,
   'admin-external-account-setting': <ManageExternalAccount />,

+ 68 - 0
src/client/js/components/Admin/AdminHome/AdminHome.jsx

@@ -0,0 +1,68 @@
+import React, { Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import loggerFactory from '@alias/logger';
+
+import { toastError } from '../../../util/apiNotification';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+import AdminHomeContainer from '../../../services/AdminHomeContainer';
+import SystemInfomationTable from './SystemInfomationTable';
+import InstalledPluginTable from './InstalledPluginTable';
+
+const logger = loggerFactory('growi:admin');
+
+class AdminHome extends React.Component {
+
+  async componentDidMount() {
+    const { adminHomeContainer } = this.props;
+
+    try {
+      await adminHomeContainer.retrieveAdminHomeData();
+    }
+    catch (err) {
+      toastError(err);
+      adminHomeContainer.setState({ retrieveError: err });
+      logger.error(err);
+    }
+  }
+
+  render() {
+    const { t } = this.props;
+
+    return (
+      <Fragment>
+        <p>
+          {t('admin_top.wiki_administrator')}
+          <br></br>
+          {t('admin_top.assign_administrator')}
+        </p>
+
+        <div className="row mb-5">
+          <h2>{t('admin_top.System Information')}</h2>
+          <SystemInfomationTable />
+        </div>
+
+        <div className="row mb-5">
+          <h2>{t('admin_top.List of installed plugins')}</h2>
+          <InstalledPluginTable />
+
+        </div>
+      </Fragment>
+    );
+  }
+
+}
+
+const AdminHomeWrapper = (props) => {
+  return createSubscribedElement(AdminHome, props, [AppContainer, AdminHomeContainer]);
+};
+
+AdminHome.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
+};
+
+export default withTranslation()(AdminHomeWrapper);

+ 53 - 0
src/client/js/components/Admin/AdminHome/InstalledPluginTable.jsx

@@ -0,0 +1,53 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+import AdminHomeContainer from '../../../services/AdminHomeContainer';
+
+class InstalledPluginTable extends React.Component {
+
+  render() {
+    const { t, adminHomeContainer } = this.props;
+
+    return (
+      <table className="table table-bordered">
+        <thead>
+          <tr>
+            <th className="text-center">{ t('admin_top.Package name') }</th>
+            <th className="text-center">{ t('admin_top.Specified version') }</th>
+            <th className="text-center">{ t('admin_top.Installed version') }</th>
+          </tr>
+        </thead>
+        <tbody>
+          { adminHomeContainer.state.installedPlugins.map((plugin) => {
+            return (
+              <tr key={plugin.name}>
+                <td>{ plugin.name }</td>
+                <td className="text-center">{ plugin.requiredVersion }</td>
+                <td className="text-center">{ plugin.installedVersion }</td>
+              </tr>
+            );
+          }) }
+        </tbody>
+      </table>
+    );
+  }
+
+}
+
+InstalledPluginTable.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const InstalledPluginTableWrapper = (props) => {
+  return createSubscribedElement(InstalledPluginTable, props, [AppContainer, AdminHomeContainer]);
+};
+
+export default withTranslation()(InstalledPluginTableWrapper);

+ 53 - 0
src/client/js/components/Admin/AdminHome/SystemInfomationTable.jsx

@@ -0,0 +1,53 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import AppContainer from '../../../services/AppContainer';
+import AdminHomeContainer from '../../../services/AdminHomeContainer';
+
+class SystemInformationTable extends React.Component {
+
+  render() {
+    const { adminHomeContainer } = this.props;
+
+    return (
+      <table className="table table-bordered">
+        <tbody>
+          <tr>
+            <th className="col-sm-4">GROWI</th>
+            <td>{ adminHomeContainer.state.growiVersion }</td>
+          </tr>
+          <tr>
+            <th>node.js</th>
+            <td>{ adminHomeContainer.state.nodeVersion }</td>
+          </tr>
+          <tr>
+            <th>npm</th>
+            <td>{ adminHomeContainer.state.npmVersion }</td>
+          </tr>
+          <tr>
+            <th>yarn</th>
+            <td>{ adminHomeContainer.state.yarnVersion }</td>
+          </tr>
+        </tbody>
+      </table>
+    );
+  }
+
+}
+
+SystemInformationTable.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminHomeContainer: PropTypes.instanceOf(AdminHomeContainer).isRequired,
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const SystemInformationTableWrapper = (props) => {
+  return createSubscribedElement(SystemInformationTable, props, [AppContainer, AdminHomeContainer]);
+};
+
+export default withTranslation()(SystemInformationTableWrapper);

+ 61 - 0
src/client/js/services/AdminHomeContainer.js

@@ -0,0 +1,61 @@
+import { Container } from 'unstated';
+
+import loggerFactory from '@alias/logger';
+
+import { toastError } from '../util/apiNotification';
+
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:services:AdminHomeContainer');
+
+/**
+ * Service container for admin home page (AdminHome.jsx)
+ * @extends {Container} unstated Container
+ */
+export default class AdminHomeContainer extends Container {
+
+  constructor(appContainer) {
+    super();
+
+    this.appContainer = appContainer;
+
+    this.state = {
+      retrieveError: null,
+      growiVersion: '',
+      nodeVersion: '',
+      npmVersion: '',
+      yarnVersion: '',
+      installedPlugins: [],
+    };
+
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'AdminHomeContainer';
+  }
+
+  /**
+   * retrieve admin home data
+   */
+  async retrieveAdminHomeData() {
+    try {
+      const response = await this.appContainer.apiv3.get('/admin-home/');
+      const { adminHomeParams } = response.data;
+
+      this.setState({
+        growiVersion: adminHomeParams.growiVersion,
+        nodeVersion: adminHomeParams.nodeVersion,
+        npmVersion: adminHomeParams.npmVersion,
+        yarnVersion: adminHomeParams.yarnVersion,
+        installedPlugins: adminHomeParams.installedPlugins,
+      });
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(new Error('Failed to fetch data'));
+    }
+  }
+
+}

+ 13 - 9
src/server/plugins/plugin-utils.js

@@ -1,6 +1,7 @@
 const path = require('path');
 const fs = require('graceful-fs');
 const logger = require('@alias/logger')('growi:plugins:plugin-utils');
+const packageInstalledVersionSync = require('package-installed-version-sync');
 
 const PluginUtilsV2 = require('./plugin-utils-v2');
 
@@ -48,8 +49,8 @@ class PluginUtils {
    *
    * @returns array of objects
    *   [
-   *     { name: 'growi-plugin-...', version: '1.0.0' },
-   *     { name: 'growi-plugin-...', version: '1.0.0' },
+   *     { name: 'growi-plugin-...', requiredVersion: '^1.0.0', installedVersion: '1.0.0' },
+   *     { name: 'growi-plugin-...', requiredVersion: '^1.0.0', installedVersion: '1.0.0' },
    *     ...
    *   ]
    *
@@ -68,14 +69,17 @@ class PluginUtils {
     const json = JSON.parse(content);
     const deps = json.dependencies || {};
 
-    const objs = {};
-    Object.keys(deps).forEach((name) => {
-      if (/^(crowi|growi)-plugin-/.test(name)) {
-        objs[name] = deps[name];
-      }
+    const pluginNames = Object.keys(deps).filter((name) => {
+      return /^(crowi|growi)-plugin-/.test(name);
     });
 
-    return objs;
+    return pluginNames.map((name) => {
+      return {
+        name,
+        requiredVersion: deps[name],
+        installedVersion: packageInstalledVersionSync(name),
+      };
+    });
   }
 
   /**
@@ -87,7 +91,7 @@ class PluginUtils {
    */
   listPluginNames(rootDir) {
     const plugins = this.listPlugins(rootDir);
-    return Object.keys(plugins);
+    return plugins.map((plugin) => { return plugin.name });
   }
 
 }

+ 1 - 5
src/server/routes/admin.js

@@ -20,12 +20,10 @@ module.exports = function(crowi, app) {
   } = crowi;
 
   const recommendedWhitelist = require('@commons/service/xss/recommended-whitelist');
-  const PluginUtils = require('../plugins/plugin-utils');
   const ApiResponse = require('../util/apiResponse');
   const importer = require('../util/importer')(crowi);
 
   const searchEvent = crowi.event('search');
-  const pluginUtils = new PluginUtils();
 
   const MAX_PAGE_LIST = 50;
   const actions = {};
@@ -99,9 +97,7 @@ module.exports = function(crowi, app) {
   });
 
   actions.index = function(req, res) {
-    return res.render('admin/index', {
-      plugins: pluginUtils.listPlugins(crowi.rootDir),
-    });
+    return res.render('admin/index');
   };
 
   // app.get('/admin/app'                  , admin.app.index);

+ 77 - 0
src/server/routes/apiv3/admin-home.js

@@ -0,0 +1,77 @@
+const express = require('express');
+const PluginUtils = require('../../plugins/plugin-utils');
+
+const pluginUtils = new PluginUtils();
+
+const router = express.Router();
+
+/**
+ * @swagger
+ *  tags:
+ *    name: adminHome
+ */
+
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *      SystemInformationParams:
+ *        type: object
+ *        properties:
+ *          growiVersion:
+ *            type: string
+ *            description: version of growi
+ *          nodeVersion:
+ *            type: string
+ *            description: version of node
+ *          npmVersion:
+ *            type: string
+ *            description: version of npm
+ *          yarnVersion:
+ *            type: string
+ *            description: version of yarn
+ *      InstalledPluginsParams:
+ *        type: object
+ *        properties:
+ *          installedPlugins:
+ *            type: object
+ *            description: installed plugins
+ */
+
+module.exports = (crowi) => {
+  const loginRequiredStrictly = require('../../middleware/login-required')(crowi);
+  const adminRequired = require('../../middleware/admin-required')(crowi);
+
+  /**
+   * @swagger
+   *
+   *    /admin-home/:
+   *      get:
+   *        tags: [adminHome]
+   *        description: Get adminHome parameters
+   *        responses:
+   *          200:
+   *            description: params of adminHome
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    adminHomeParams:
+   *                      type: object
+   *                      description: adminHome params
+   */
+  router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
+    const adminHomeParams = {
+      growiVersion: crowi.version,
+      nodeVersion: crowi.runtimeVersions.versions.node ? crowi.runtimeVersions.versions.node.version.version : '-',
+      npmVersion: crowi.runtimeVersions.versions.npm ? crowi.runtimeVersions.versions.npm.version.version : '-',
+      yarnVersion: crowi.runtimeVersions.versions.yarn ? crowi.runtimeVersions.versions.yarn.version.version : '-',
+      installedPlugins: pluginUtils.listPlugins(crowi.rootDir),
+    };
+
+    return res.apiv3({ adminHomeParams });
+  });
+
+  return router;
+};

+ 1 - 1
src/server/routes/apiv3/customize-setting.js

@@ -136,7 +136,7 @@ module.exports = (crowi) => {
    *        tags: [CustomizeSetting, apiv3]
    *        operationId: getCustomizeSetting
    *        summary: /_api/v3/customize-setting
-   *        description: Get customize paramators
+   *        description: Get customize parameters
    *        responses:
    *          200:
    *            description: params of customize

+ 2 - 0
src/server/routes/apiv3/index.js

@@ -13,6 +13,8 @@ module.exports = (crowi) => {
 
   router.use('/healthcheck', require('./healthcheck')(crowi));
 
+  router.use('/admin-home', require('./admin-home')(crowi));
+
   router.use('/markdown-setting', require('./markdown-setting')(crowi));
 
   router.use('/app-settings', require('./app-settings')(crowi));

+ 1 - 1
src/server/routes/apiv3/markdown-setting.js

@@ -97,7 +97,7 @@ module.exports = (crowi) => {
    *        tags: [MarkDownSetting, apiv3]
    *        operationId: getMarkdownSetting
    *        summary: /_api/v3/markdown-setting
-   *        description: Get markdown paramators
+   *        description: Get markdown parameters
    *        responses:
    *          200:
    *            description: params of markdown

+ 1 - 47
src/server/views/admin/index.html

@@ -25,53 +25,7 @@
       {% include './widget/menu.html' %}
     </div>
     <div class="col-md-9">
-      <p> {{ t("admin_top.wiki_administrator") }}<br>
-      {{ t("admin_top.assign_administrator") }}
-      </p>
-
-      <legend>
-        <h2>{{ t('admin_top.System Information') }}</h2>
-      </legend>
-      <table class="table table-bordered">
-        <tr>
-          <th class="col-sm-4">GROWI</th>
-          <td>{{ growiVersion() }}</td>
-        </tr>
-        <tr>
-          <th>node.js</th>
-          <td>{{ nodeVersion() }}</td>
-        </tr>
-        <tr>
-          <th>npm</th>
-          <td>{{ npmVersion() }}</td>
-        </tr>
-        <tr>
-          <th>yarn</th>
-          <td>{{ yarnVersion() }}</td>
-        </tr>
-      </table>
-
-      <legend>
-        <h2>{{ t('admin_top.List of installed plugins') }}</h2>
-      </legend>
-      <table class="table table-bordered">
-        <th class="text-center">
-          {{ t('admin_top.Package name') }}
-        </th>
-        <th class="text-center">
-          {{ t('admin_top.Specified version') }}
-        </th>
-        <th class="text-center">
-          {{ t('admin_top.Installed version') }}
-        </th>
-        {% for pluginName in Object.keys(plugins) %}
-        <tr>
-          <td>{{ pluginName }}</td>
-          <td class="text-center">{{ plugins[pluginName] }}</td>
-          <td class="text-center"><span class="tbd">(TBD)</span></td>
-        </tr>
-        {% endfor %}
-      </table>
+      <div id="admin-home"></div>
     </div>
   </div>
 

+ 23 - 9
yarn.lock

@@ -1534,6 +1534,11 @@
   resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
   integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
 
+"@yarnpkg/lockfile@^1.1.0":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
+  integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
+
 JSONStream@^1.3.5:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
@@ -5288,6 +5293,14 @@ find-cache-dir@^3.0.0:
     make-dir "^3.0.0"
     pkg-dir "^4.1.0"
 
+find-up@4.1.0, find-up@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+  dependencies:
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
+
 find-up@^1.0.0:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
@@ -5307,14 +5320,6 @@ find-up@^3.0.0:
   dependencies:
     locate-path "^3.0.0"
 
-find-up@^4.0.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
-  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
-  dependencies:
-    locate-path "^5.0.0"
-    path-exists "^4.0.0"
-
 findup-sync@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1"
@@ -9553,6 +9558,15 @@ p-try@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
 
+package-installed-version-sync@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/package-installed-version-sync/-/package-installed-version-sync-2.1.0.tgz#db8d2cbee32bc91a36e100da9bda6743f956ac93"
+  integrity sha512-rhREjEXIJ0IurYS23PGmlL1T+6/wJL9Oev2WYztN+MYze6xpsFxUL3DaixlZglpHoYCPxu3tdCUO/AMoIVrCVg==
+  dependencies:
+    "@yarnpkg/lockfile" "^1.1.0"
+    find-up "4.1.0"
+    semver "^6.2.0"
+
 pako@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.3.tgz#5f515b0c6722e1982920ae8005eacb0b7ca73ccf"
@@ -11640,7 +11654,7 @@ semver@^6.1.1:
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.2.tgz#079960381376a3db62eb2edc8a3bfb10c7cfe318"
   integrity sha512-z4PqiCpomGtWj8633oeAdXm1Kn1W++3T8epkZYnwiVgIYIJ0QHszhInYSJTYxebByQH7KVCEAn8R9duzZW2PhQ==
 
-semver@^6.3.0:
+semver@^6.2.0, semver@^6.3.0:
   version "6.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==