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

Merge pull request #28 from weseek/support/integrate-plugin-cards

support: Integrate admin page plugin cards
ryoji-s 3 лет назад
Родитель
Сommit
373486421d

+ 3 - 3
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -38,7 +38,7 @@ const AdminNavigation = (props) => {
         { t('full_text_search_management.full_text_search_management') }</>;
       // TODO: Consider where to place the "AuditLog"
       case 'audit-log':                return <><i className="icon-fw icon-feed"></i>            { t('audit_log_management.audit_log')}</>;
-      case 'plugins-extention':        return <><i className="icon-fw fa fa-plug"></i>           Plugins Extention </>;
+      case 'plugins-extension':        return <><i className="icon-fw fa fa-plug"></i>           Plugins Extension </>;
       case 'cloud':                    return <><i className="icon-fw icon-share-alt"></i>       { t('to_cloud_settings')} </>;
       default:                         return <><i className="icon-fw icon-home"></i>            { t('wiki_management_home_page') }</>;
       /* eslint-enable no-multi-spaces */
@@ -95,7 +95,7 @@ const AdminNavigation = (props) => {
         <MenuLink menu="user-groups"  isListGroupItems isActive={isActiveMenu('/user-groups')} />
         <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />
         <MenuLink menu="audit-log"    isListGroupItems isActive={isActiveMenu('/audit-log')} />
-        <MenuLink menu="plugins-extention"    isListGroupItems isActive={isActiveMenu('/plugins-extention')} />
+        <MenuLink menu="plugins-extension"    isListGroupItems isActive={isActiveMenu('/plugins-extension')} />
         {/* {growiCloudUri != null && growiAppIdForGrowiCloud != null
           && (
             <a
@@ -144,7 +144,7 @@ const AdminNavigation = (props) => {
             {isActiveMenu('/user-groups') &&       <MenuLabel menu="user-groups" />}
             {isActiveMenu('/search') &&            <MenuLabel menu="search" />}
             {isActiveMenu('/audit-log') &&         <MenuLabel menu="audit-log" />}
-            {isActiveMenu('/plugins-extention') && <MenuLabel menu="plugins-extention" />}
+            {isActiveMenu('/plugins-extension') && <MenuLabel menu="plugins-extension" />}
             {/* eslint-enable no-multi-spaces */}
           </span>
         </button>

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

+ 49 - 30
packages/app/src/components/Admin/PluginsExtension/PluginCard.tsx

@@ -1,30 +1,67 @@
-// import { faCircleArrowDown, faCircleCheck } from '@fortawesome/free-solid-svg-icons';
-// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { useCallback, useState } from 'react';
 
-import Image from 'next/image';
+import React, { useState } from 'react';
+
 import Link from 'next/link';
 
-import { SearchResultItem } from '~/models/SearchResultItem';
+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);
+      }
+      catch (err) {
+        console.log('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>
+    );
+  };
 
   return (
     <div className="card shadow border-0" key={name}>
@@ -37,25 +74,7 @@ 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>
-            {/* <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} /> */}
+            <PluginCardButton />
           </div>
         </div>
         <div className="row">

+ 1 - 35
packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx

@@ -1,17 +1,9 @@
 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)
 // TODO: i18n
-
 export const PluginInstallerForm = (): JSX.Element => {
-  // const { t } = useTranslation('admin');
-
   const submitHandler = useCallback(async(e) => {
     e.preventDefault();
 
@@ -30,7 +22,7 @@ export const PluginInstallerForm = (): JSX.Element => {
     };
 
     try {
-      await apiv3Post('/plugins-extention', { pluginInstallerForm });
+      await apiv3Post('/plugins-extension', { pluginInstallerForm });
       toastSuccess('Plugin Install Successed!');
     }
     catch (err) {
@@ -47,39 +39,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">

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

@@ -1,21 +1,38 @@
 import React from 'react';
 
-import { SearchResultItem } from '~/models/SearchResultItem';
-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,27 @@ 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.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>

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

@@ -120,6 +120,7 @@ class RevisionLoader extends React.Component {
 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,

+ 5 - 1
packages/app/src/models/SearchResult.ts

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

+ 6 - 0
packages/app/src/models/SearchResultItem.ts

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

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

@@ -1,6 +1,7 @@
 import React from 'react';
 
 import { isClient, objectIdUtils } from '@growi/core';
+import mongoose from 'mongoose';
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
@@ -28,8 +29,10 @@ import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityConta
 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,
@@ -81,6 +84,8 @@ type Props = CommonProps & {
   auditLogEnabled: boolean,
   auditLogAvailableActions: SupportedActionType[],
 
+  pluginManifestEntries: any,
+
   siteUrl: string,
 };
 
@@ -180,8 +185,8 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
       title: t('audit_log_management.audit_log'),
       component: <AuditLogManagement />,
     },
-    'plugins-extention': {
-      title: 'Plugins Extention',
+    'plugins-extension': {
+      title: 'Plugins Extension',
       component: <PluginsExtensionPageContents />,
     },
   };
@@ -315,6 +320,15 @@ async function injectNextI18NextConfigurations(context: GetServerSidePropsContex
   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;
 
@@ -334,6 +348,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
   injectServerConfigurations(context, props);
   await injectNextI18NextConfigurations(context, props, ['admin']);
+  await getPluginProps(context, props);
 
   return {
     props,

+ 1 - 1
packages/app/src/server/routes/apiv3/index.js

@@ -99,7 +99,7 @@ module.exports = (crowi, app, isInstalled) => {
     userActivation.validateCompleteRegistration,
     userActivation.completeRegistrationAction(crowi));
 
-  router.use('/plugins-extention', require('./plugins-extention')(crowi));
+  router.use('/plugins-extension', require('./plugins-extension')(crowi));
 
   router.use('/user-ui-settings', require('./user-ui-settings')(crowi));
 

+ 70 - 0
packages/app/src/server/routes/apiv3/plugins-extension.ts

@@ -0,0 +1,70 @@
+import express, { Request } from 'express';
+
+import Crowi from '../../crowi';
+
+import { ApiV3Response } from './interfaces/apiv3-response';
+
+type PluginInstallerFormRequest = Request & { form: any };
+
+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);
+    }
+
+    try {
+      await pluginService.install(crowi, req.body.pluginInstallerForm);
+      return res.apiv3({});
+    }
+    catch (err) {
+      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);
+    }
+  });
+
+  return router;
+};

+ 0 - 29
packages/app/src/server/routes/apiv3/plugins-extention.ts

@@ -1,29 +0,0 @@
-import express, { Request } from 'express';
-
-import Crowi from '../../crowi';
-
-import { ApiV3Response } from './interfaces/apiv3-response';
-
-type PluginInstallerFormRequest = Request & { form: any };
-
-module.exports = (crowi: Crowi) => {
-  const router = express.Router();
-  const { pluginService } = crowi;
-
-  router.post('/', async(req: PluginInstallerFormRequest, res: ApiV3Response) => {
-    if (pluginService == null) {
-      return res.apiv3Err(400);
-    }
-
-    try {
-      await pluginService.install(crowi, req.body.pluginInstallerForm);
-      return res.apiv3({});
-    }
-    catch (err) {
-      // TODO: error handling
-      return res.apiv3Err(err, 400);
-    }
-  });
-
-  return router;
-};

+ 30 - 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 { 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,25 @@ 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;
+  }
 
 }

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