Parcourir la source

show progress bar of ldap group sync

Futa Arai il y a 2 ans
Parent
commit
23f7d2673c

+ 3 - 2
apps/app/src/components/Admin/ElasticsearchManagement/RebuildIndexControls.jsx

@@ -3,6 +3,7 @@ import React from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
+import { SocketEventName } from '~/interfaces/websocket';
 import { useAdminSocket } from '~/stores/socket-io';
 import { useAdminSocket } from '~/stores/socket-io';
 
 
 import LabeledProgressBar from '../Common/LabeledProgressBar';
 import LabeledProgressBar from '../Common/LabeledProgressBar';
@@ -27,7 +28,7 @@ class RebuildIndexControls extends React.Component {
     const { socket } = this.props;
     const { socket } = this.props;
 
 
     if (socket != null) {
     if (socket != null) {
-      socket.on('addPageProgress', (data) => {
+      socket.on(SocketEventName.AddPageProgress, (data) => {
         this.setState({
         this.setState({
           total: data.totalCount,
           total: data.totalCount,
           current: data.count,
           current: data.count,
@@ -35,7 +36,7 @@ class RebuildIndexControls extends React.Component {
         });
         });
       });
       });
 
 
-      socket.on('finishAddPage', (data) => {
+      socket.on(SocketEventName.FinishAddPage, (data) => {
         this.setState({
         this.setState({
           total: data.totalCount,
           total: data.totalCount,
           current: data.count,
           current: data.count,

+ 47 - 0
apps/app/src/features/external-user-group/client/components/ExternalUserGroup/LdapGroupManagement.tsx

@@ -6,12 +6,22 @@ import { useTranslation } from 'react-i18next';
 
 
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
+import LabeledProgressBar from '~/components/Admin/Common/LabeledProgressBar';
+import { SocketEventName } from '~/interfaces/websocket';
+import { useAdminSocket } from '~/stores/socket-io';
 
 
 import { LdapGroupSyncSettingsForm } from './LdapGroupSyncSettingsForm';
 import { LdapGroupSyncSettingsForm } from './LdapGroupSyncSettingsForm';
 
 
 export const LdapGroupManagement: FC = () => {
 export const LdapGroupManagement: FC = () => {
   const [isUserBind, setIsUserBind] = useState(false);
   const [isUserBind, setIsUserBind] = useState(false);
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
+  const { data: socket } = useAdminSocket();
+
+  const [isSyncExecuting, setIsSyncExecuting] = useState(false);
+  const [progress, setProgress] = useState({
+    total: 0,
+    current: 0,
+  });
 
 
   useEffect(() => {
   useEffect(() => {
     const getIsUserBind = async() => {
     const getIsUserBind = async() => {
@@ -27,8 +37,26 @@ export const LdapGroupManagement: FC = () => {
     getIsUserBind();
     getIsUserBind();
   }, []);
   }, []);
 
 
+  useEffect(() => {
+    if (socket != null) {
+      socket.on(SocketEventName.GroupSyncProgress, (data) => {
+        setIsSyncExecuting(true);
+        setProgress({
+          total: data.totalCount,
+          current: data.count,
+        });
+      });
+
+      socket.on(SocketEventName.FinishGroupSync, () => {
+        setIsSyncExecuting(false);
+      });
+    }
+  }, [socket]);
+
   const onSyncBtnClick = useCallback(async(e) => {
   const onSyncBtnClick = useCallback(async(e) => {
     e.preventDefault();
     e.preventDefault();
+    setIsSyncExecuting(true);
+    setProgress({ total: 0, current: 0 });
     try {
     try {
       if (isUserBind) {
       if (isUserBind) {
         const password = e.target.password.value;
         const password = e.target.password.value;
@@ -44,10 +72,29 @@ export const LdapGroupManagement: FC = () => {
     }
     }
   }, [t, isUserBind]);
   }, [t, isUserBind]);
 
 
+  const renderProgressBar = () => {
+    if (!isSyncExecuting) return null;
+    const header = 'Processing..';
+
+    return (
+      <LabeledProgressBar
+        header={header}
+        currentCount={progress.current}
+        totalCount={progress.total}
+      />
+    );
+  };
+
   return (
   return (
     <>
     <>
       <LdapGroupSyncSettingsForm />
       <LdapGroupSyncSettingsForm />
       <h3 className="border-bottom mb-3">{t('external_user_group.execute_sync')}</h3>
       <h3 className="border-bottom mb-3">{t('external_user_group.execute_sync')}</h3>
+      <div className="row">
+        <div className="col-md-3"></div>
+        <div className="col-md-9">
+          {renderProgressBar()}
+        </div>
+      </div>
       <form onSubmit={onSyncBtnClick}>
       <form onSubmit={onSyncBtnClick}>
         {isUserBind && (
         {isUserBind && (
           <div className="row form-group">
           <div className="row form-group">

+ 26 - 4
apps/app/src/features/external-user-group/server/service/external-user-group-sync.ts

@@ -1,5 +1,6 @@
 import type { IUserHasId } from '@growi/core';
 import type { IUserHasId } from '@growi/core';
 
 
+import { SocketEventName } from '~/interfaces/websocket';
 import ExternalAccount from '~/server/models/external-account';
 import ExternalAccount from '~/server/models/external-account';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 import { batchProcessPromiseAll } from '~/utils/promise';
 import { batchProcessPromiseAll } from '~/utils/promise';
@@ -25,11 +26,14 @@ abstract class ExternalUserGroupSyncService<SyncParamsType = any> {
 
 
   authProviderType: string; // auth provider type (e.g: ldap, oidc)
   authProviderType: string; // auth provider type (e.g: ldap, oidc)
 
 
+  socketIoService: any;
+
   isExecutingSync = false;
   isExecutingSync = false;
 
 
-  constructor(groupProviderType: ExternalGroupProviderType, authProviderType: string) {
+  constructor(groupProviderType: ExternalGroupProviderType, authProviderType: string, socketIoService) {
     this.groupProviderType = groupProviderType;
     this.groupProviderType = groupProviderType;
     this.authProviderType = authProviderType;
     this.authProviderType = authProviderType;
+    this.socketIoService = socketIoService;
   }
   }
 
 
   /** External user group tree sync method
   /** External user group tree sync method
@@ -41,6 +45,7 @@ abstract class ExternalUserGroupSyncService<SyncParamsType = any> {
     if (this.isExecutingSync) throw new Error('External user group sync is already being executed');
     if (this.isExecutingSync) throw new Error('External user group sync is already being executed');
     this.isExecutingSync = true;
     this.isExecutingSync = true;
 
 
+    const preserveDeletedLdapGroups: boolean = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:preserveDeletedGroups`);
     const existingExternalUserGroupIds: string[] = [];
     const existingExternalUserGroupIds: string[] = [];
 
 
     const syncNode = async(node: ExternalUserGroupTreeNode, parentId?: string) => {
     const syncNode = async(node: ExternalUserGroupTreeNode, parentId?: string) => {
@@ -53,14 +58,20 @@ abstract class ExternalUserGroupSyncService<SyncParamsType = any> {
       }
       }
     };
     };
 
 
+    const socket = this.socketIoService.getAdminSocket();
+
     try {
     try {
       const trees = await this.generateExternalUserGroupTrees(params);
       const trees = await this.generateExternalUserGroupTrees(params);
+      const totalCount = trees.map(tree => this.getGroupCountOfTree(tree))
+        .reduce((sum, current) => sum + current);
+      let count = 0;
 
 
       await batchProcessPromiseAll(trees, TREES_BATCH_SIZE, (root) => {
       await batchProcessPromiseAll(trees, TREES_BATCH_SIZE, (root) => {
+        count += 1;
+        socket.emit(SocketEventName.GroupSyncProgress, { totalCount, count });
         return syncNode(root);
         return syncNode(root);
       });
       });
 
 
-      const preserveDeletedLdapGroups: boolean = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:preserveDeletedGroups`);
       if (!preserveDeletedLdapGroups) {
       if (!preserveDeletedLdapGroups) {
         await ExternalUserGroup.deleteMany({ _id: { $nin: existingExternalUserGroupIds }, groupProviderType: this.groupProviderType });
         await ExternalUserGroup.deleteMany({ _id: { $nin: existingExternalUserGroupIds }, groupProviderType: this.groupProviderType });
         await ExternalUserGroupRelation.removeAllInvalidRelations();
         await ExternalUserGroupRelation.removeAllInvalidRelations();
@@ -68,6 +79,7 @@ abstract class ExternalUserGroupSyncService<SyncParamsType = any> {
     }
     }
     finally {
     finally {
       this.isExecutingSync = false;
       this.isExecutingSync = false;
+      socket.emit(SocketEventName.FinishGroupSync);
     }
     }
   }
   }
 
 
@@ -79,7 +91,7 @@ abstract class ExternalUserGroupSyncService<SyncParamsType = any> {
    * @param {string} parentId Parent group id (id in GROWI) of the group we want to create/update
    * @param {string} parentId Parent group id (id in GROWI) of the group we want to create/update
    * @returns {Promise<IExternalUserGroupHasId>} ExternalUserGroup that was created/updated
    * @returns {Promise<IExternalUserGroupHasId>} ExternalUserGroup that was created/updated
   */
   */
-  async createUpdateExternalUserGroup(node: ExternalUserGroupTreeNode, parentId?: string): Promise<IExternalUserGroupHasId> {
+  private async createUpdateExternalUserGroup(node: ExternalUserGroupTreeNode, parentId?: string): Promise<IExternalUserGroupHasId> {
     const externalUserGroup = await ExternalUserGroup.findAndUpdateOrCreateGroup(
     const externalUserGroup = await ExternalUserGroup.findAndUpdateOrCreateGroup(
       node.name, node.id, this.groupProviderType, node.description, parentId,
       node.name, node.id, this.groupProviderType, node.description, parentId,
     );
     );
@@ -108,7 +120,7 @@ abstract class ExternalUserGroupSyncService<SyncParamsType = any> {
    * @param {ExternalUserInfo} externalUserInfo Search external app/server using this identifier
    * @param {ExternalUserInfo} externalUserInfo Search external app/server using this identifier
    * @returns {Promise<IUserHasId | null>} User when found or created, null when neither
    * @returns {Promise<IUserHasId | null>} User when found or created, null when neither
    */
    */
-  async getMemberUser(userInfo: ExternalUserInfo): Promise<IUserHasId | null> {
+  private async getMemberUser(userInfo: ExternalUserInfo): Promise<IUserHasId | null> {
     const autoGenerateUserOnGroupSync = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:autoGenerateUserOnGroupSync`);
     const autoGenerateUserOnGroupSync = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:autoGenerateUserOnGroupSync`);
 
 
     const getExternalAccount = async() => {
     const getExternalAccount = async() => {
@@ -128,6 +140,16 @@ abstract class ExternalUserGroupSyncService<SyncParamsType = any> {
     return null;
     return null;
   }
   }
 
 
+  getGroupCountOfTree(tree: ExternalUserGroupTreeNode): number {
+    if (tree.childGroupNodes.length === 0) return 1;
+
+    let count = 1;
+    tree.childGroupNodes.forEach((childGroup) => {
+      count += this.getGroupCountOfTree(childGroup);
+    });
+    return count;
+  }
+
   /** Method to generate external group tree structure
   /** Method to generate external group tree structure
    * 1. Fetch user group info from external app/server
    * 1. Fetch user group info from external app/server
    * 2. Convert each group tree structure to ExternalUserGroupTreeNode
    * 2. Convert each group tree structure to ExternalUserGroupTreeNode

+ 4 - 4
apps/app/src/features/external-user-group/server/service/ldap-user-group-sync.ts

@@ -27,8 +27,8 @@ class LdapUserGroupSyncService extends ExternalUserGroupSyncService<SyncParamsTy
   ldapService: LdapService;
   ldapService: LdapService;
 
 
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  constructor(passportService) {
-    super(ExternalGroupProviderType.ldap, 'ldap');
+  constructor(passportService, socketIoService) {
+    super(ExternalGroupProviderType.ldap, 'ldap', socketIoService);
     this.passportService = passportService;
     this.passportService = passportService;
     this.ldapService = new LdapService();
     this.ldapService = new LdapService();
   }
   }
@@ -161,6 +161,6 @@ class LdapUserGroupSyncService extends ExternalUserGroupSyncService<SyncParamsTy
 
 
 // eslint-disable-next-line import/no-mutable-exports
 // eslint-disable-next-line import/no-mutable-exports
 export let ldapUserGroupSyncService: LdapUserGroupSyncService | undefined; // singleton instance
 export let ldapUserGroupSyncService: LdapUserGroupSyncService | undefined; // singleton instance
-export function instanciate(passportService: PassportService): void {
-  ldapUserGroupSyncService = new LdapUserGroupSyncService(passportService);
+export function instanciate(passportService: PassportService, socketIoService): void {
+  ldapUserGroupSyncService = new LdapUserGroupSyncService(passportService, socketIoService);
 }
 }

+ 4 - 0
apps/app/src/interfaces/websocket.ts

@@ -17,6 +17,10 @@ export const SocketEventName = {
   FinishAddPage: 'finishAddPage',
   FinishAddPage: 'finishAddPage',
   RebuildingFailed: 'rebuildingFailed',
   RebuildingFailed: 'rebuildingFailed',
 
 
+  // External user group sync
+  GroupSyncProgress: 'groupSyncProgress',
+  FinishGroupSync: 'finishGroupSync',
+
   // Page Operation
   // Page Operation
   PageCreated: 'page:create',
   PageCreated: 'page:create',
   PageUpdated: 'page:update',
   PageUpdated: 'page:update',

+ 6 - 5
apps/app/src/server/crowi/index.js

@@ -154,14 +154,15 @@ Crowi.prototype.init = async function() {
     this.setupSyncPageStatusService(),
     this.setupSyncPageStatusService(),
     this.setupQuestionnaireService(),
     this.setupQuestionnaireService(),
     this.setUpCustomize(), // depends on pluginService
     this.setUpCustomize(), // depends on pluginService
-    this.setupExternalAccountService(),
-    this.setupLdapUserGroupSyncService(),
   ]);
   ]);
 
 
-  // globalNotification depends on slack and mailer
   await Promise.all([
   await Promise.all([
+    // globalNotification depends on slack and mailer
     this.setUpGlobalNotification(),
     this.setUpGlobalNotification(),
     this.setUpUserNotification(),
     this.setUpUserNotification(),
+    // depends on passport service
+    this.setupExternalAccountService(),
+    this.setupLdapUserGroupSyncService(),
   ]);
   ]);
 
 
   await this.autoInstall();
   await this.autoInstall();
@@ -789,9 +790,9 @@ Crowi.prototype.setupExternalAccountService = function() {
   instanciateExternalAccountService(this.passportService);
   instanciateExternalAccountService(this.passportService);
 };
 };
 
 
-// execute after setupPassport
+// execute after setupPassport and socketIoService
 Crowi.prototype.setupLdapUserGroupSyncService = function() {
 Crowi.prototype.setupLdapUserGroupSyncService = function() {
-  instanciateLdapUserGroupSyncService(this.passportService);
+  instanciateLdapUserGroupSyncService(this.passportService, this.socketIoService);
 };
 };
 
 
 export default Crowi;
 export default Crowi;