SyncExecution.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. import type { FC, JSX } from 'react';
  2. import { useCallback, useEffect, useState } from 'react';
  3. import { useTranslation } from 'react-i18next';
  4. import { Modal, ModalBody, ModalHeader } from 'reactstrap';
  5. import LabeledProgressBar from '~/client/components/Admin/Common/LabeledProgressBar';
  6. import { apiv3Get } from '~/client/util/apiv3-client';
  7. import { toastError, toastSuccess } from '~/client/util/toastr';
  8. import type { ExternalGroupProviderType } from '~/features/external-user-group/interfaces/external-user-group';
  9. import { SocketEventName } from '~/interfaces/websocket';
  10. import { useAdminSocket } from '~/stores/socket-io';
  11. import { useSWRxExternalUserGroupList } from '../../stores/external-user-group';
  12. type SyncExecutionProps = {
  13. provider: ExternalGroupProviderType;
  14. requestSyncAPI: (e?: React.FormEvent<HTMLFormElement>) => Promise<void>;
  15. AdditionalForm?: FC;
  16. };
  17. enum SyncStatus {
  18. beforeSync,
  19. syncExecuting,
  20. syncCompleted,
  21. syncFailed,
  22. }
  23. export const SyncExecution = ({
  24. provider,
  25. requestSyncAPI,
  26. AdditionalForm = () => <></>,
  27. }: SyncExecutionProps): JSX.Element => {
  28. const { t } = useTranslation('admin');
  29. const { data: socket } = useAdminSocket();
  30. const { mutate: mutateExternalUserGroups } = useSWRxExternalUserGroupList();
  31. const [syncStatus, setSyncStatus] = useState<SyncStatus>(
  32. SyncStatus.beforeSync,
  33. );
  34. const [progress, setProgress] = useState({
  35. total: 0,
  36. current: 0,
  37. });
  38. const [isAlertModalOpen, setIsAlertModalOpen] = useState(false);
  39. // value to propagate the submit event of form to submit confirm modal
  40. const [currentSubmitEvent, setCurrentSubmitEvent] =
  41. useState<React.FormEvent<HTMLFormElement>>();
  42. useEffect(() => {
  43. if (socket == null) return;
  44. const eventName = SocketEventName.externalUserGroup[provider];
  45. socket.on(eventName.GroupSyncProgress, (data) => {
  46. setSyncStatus(SyncStatus.syncExecuting);
  47. setProgress({
  48. total: data.totalCount,
  49. current: data.count,
  50. });
  51. });
  52. socket.on(eventName.GroupSyncCompleted, () => {
  53. setSyncStatus(SyncStatus.syncCompleted);
  54. mutateExternalUserGroups();
  55. toastSuccess(t('external_user_group.sync_succeeded'));
  56. });
  57. socket.on(eventName.GroupSyncFailed, () => {
  58. setSyncStatus(SyncStatus.syncFailed);
  59. mutateExternalUserGroups();
  60. toastError(t('external_user_group.sync_failed'));
  61. });
  62. return () => {
  63. socket.off(eventName.GroupSyncProgress);
  64. socket.off(eventName.GroupSyncCompleted);
  65. socket.off(eventName.GroupSyncFailed);
  66. };
  67. }, [socket, mutateExternalUserGroups, t, provider]);
  68. // get sync status on load, since next socket data may take a while
  69. useEffect(() => {
  70. const getSyncStatus = async () => {
  71. const res = await apiv3Get(
  72. `/external-user-groups/${provider}/sync-status`,
  73. );
  74. if (res.data.isExecutingSync) {
  75. setSyncStatus(SyncStatus.syncExecuting);
  76. setProgress({ total: res.data.totalCount, current: res.data.count });
  77. }
  78. };
  79. getSyncStatus();
  80. }, [provider]);
  81. const onSyncBtnClick = (e: React.FormEvent<HTMLFormElement>) => {
  82. e.preventDefault();
  83. setCurrentSubmitEvent(e);
  84. setIsAlertModalOpen(true);
  85. };
  86. const onSyncExecConfirmBtnClick = useCallback(async () => {
  87. setIsAlertModalOpen(false);
  88. try {
  89. // set sync status before requesting to API, so that setting to syncFailed does not get overwritten
  90. setSyncStatus(SyncStatus.syncExecuting);
  91. setProgress({ total: 0, current: 0 });
  92. await requestSyncAPI(currentSubmitEvent);
  93. } catch (errs) {
  94. setSyncStatus(SyncStatus.syncFailed);
  95. toastError(t(errs[0]?.code));
  96. }
  97. }, [t, requestSyncAPI, currentSubmitEvent]);
  98. const renderProgressBar = () => {
  99. if (syncStatus === SyncStatus.beforeSync) return null;
  100. let header: string;
  101. if (syncStatus === SyncStatus.syncExecuting) {
  102. header = 'Processing..';
  103. } else if (syncStatus === SyncStatus.syncCompleted) {
  104. header = 'Completed';
  105. } else {
  106. header = 'Failed';
  107. }
  108. return (
  109. <LabeledProgressBar
  110. header={header}
  111. currentCount={progress.current}
  112. totalCount={progress.total}
  113. />
  114. );
  115. };
  116. return (
  117. <>
  118. <h3 className="border-bottom mb-3">
  119. {t('external_user_group.execute_sync')}
  120. </h3>
  121. <div className="row">
  122. <div className="col-md-3"></div>
  123. <div className="col-md-9">{renderProgressBar()}</div>
  124. </div>
  125. <form onSubmit={onSyncBtnClick}>
  126. <AdditionalForm />
  127. <div className="row">
  128. <div className="col-md-3"></div>
  129. <div className="col-md-6">
  130. <button className="btn btn-primary" type="submit">
  131. {t('external_user_group.sync')}
  132. </button>
  133. </div>
  134. </div>
  135. </form>
  136. <Modal
  137. isOpen={isAlertModalOpen}
  138. toggle={() => setIsAlertModalOpen(false)}
  139. >
  140. <ModalHeader
  141. tag="h4"
  142. toggle={() => setIsAlertModalOpen(false)}
  143. className="text-info"
  144. >
  145. <span className="material-symbols-outlined me-1 align-middle">
  146. error
  147. </span>
  148. <span className="align-middle">
  149. {t('external_user_group.confirmation_before_sync')}
  150. </span>
  151. </ModalHeader>
  152. <ModalBody>
  153. <ul>
  154. <li>{t('external_user_group.execution_time_warning')}</li>
  155. <li>{t('external_user_group.parallel_sync_forbidden')}</li>
  156. </ul>
  157. <div className="text-center">
  158. <button
  159. className="btn btn-primary"
  160. type="button"
  161. onClick={onSyncExecConfirmBtnClick}
  162. >
  163. {t('Execute')}
  164. </button>
  165. </div>
  166. </ModalBody>
  167. </Modal>
  168. </>
  169. );
  170. };