SyncExecution.tsx 5.5 KB

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