SlackIntegration.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import React, { type JSX, useCallback, useEffect, useState } from 'react';
  2. import { SlackbotType } from '@growi/slack';
  3. import { LoadingSpinner } from '@growi/ui/dist/components';
  4. import { useTranslation } from 'next-i18next';
  5. import {
  6. apiv3Delete,
  7. apiv3Get,
  8. apiv3Post,
  9. apiv3Put,
  10. } from '~/client/util/apiv3-client';
  11. import { toastError, toastSuccess } from '~/client/util/toastr';
  12. import { BotTypeCard } from './BotTypeCard';
  13. import ConfirmBotChangeModal from './ConfirmBotChangeModal';
  14. import CustomBotWithoutProxySettings from './CustomBotWithoutProxySettings';
  15. import CustomBotWithProxySettings from './CustomBotWithProxySettings';
  16. import { DeleteSlackBotSettingsModal } from './DeleteSlackBotSettingsModal';
  17. import OfficialBotSettings from './OfficialBotSettings';
  18. const botTypes = Object.values(SlackbotType);
  19. export const SlackIntegration = (): JSX.Element => {
  20. const { t } = useTranslation();
  21. const [currentBotType, setCurrentBotType] = useState<
  22. SlackbotType | undefined
  23. >();
  24. const [selectedBotType, setSelectedBotType] = useState<
  25. SlackbotType | undefined
  26. >();
  27. const [slackSigningSecret, setSlackSigningSecret] = useState(null);
  28. const [slackBotToken, setSlackBotToken] = useState(null);
  29. const [slackSigningSecretEnv, setSlackSigningSecretEnv] = useState('');
  30. const [slackBotTokenEnv, setSlackBotTokenEnv] = useState('');
  31. const [commandPermission, setCommandPermission] = useState(null);
  32. const [eventActionsPermission, setEventActionsPermission] = useState(null);
  33. const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] =
  34. useState(false);
  35. const [slackAppIntegrations, setSlackAppIntegrations] = useState();
  36. const [proxyServerUri, setProxyServerUri] = useState();
  37. const [connectionStatuses, setConnectionStatuses] = useState({});
  38. const [errorMsg, setErrorMsg] = useState(null);
  39. const [errorCode, setErrorCode] = useState(null);
  40. const [isLoading, setIsLoading] = useState(true);
  41. const fetchSlackIntegrationData = useCallback(async () => {
  42. try {
  43. const { data } = await apiv3Get('/slack-integration-settings');
  44. const {
  45. slackSigningSecret,
  46. slackBotToken,
  47. slackSigningSecretEnvVars,
  48. slackBotTokenEnvVars,
  49. slackAppIntegrations,
  50. proxyServerUri,
  51. commandPermission,
  52. eventActionsPermission,
  53. } = data.settings;
  54. setErrorMsg(data.errorMsg);
  55. setErrorCode(data.errorCode);
  56. setConnectionStatuses(data.connectionStatuses);
  57. setCurrentBotType(data.currentBotType);
  58. setSlackSigningSecret(slackSigningSecret);
  59. setSlackBotToken(slackBotToken);
  60. setSlackSigningSecretEnv(slackSigningSecretEnvVars);
  61. setSlackBotTokenEnv(slackBotTokenEnvVars);
  62. setSlackAppIntegrations(slackAppIntegrations);
  63. setProxyServerUri(proxyServerUri);
  64. setCommandPermission(commandPermission);
  65. setEventActionsPermission(eventActionsPermission);
  66. } catch (err) {
  67. toastError(err);
  68. } finally {
  69. setIsLoading(false);
  70. }
  71. }, []);
  72. const resetAllSettings = async () => {
  73. try {
  74. await apiv3Delete('/slack-integration-settings/bot-type');
  75. fetchSlackIntegrationData();
  76. toastSuccess(t('admin:slack_integration.bot_all_reset_successful'));
  77. } catch (error) {
  78. toastError(error);
  79. }
  80. };
  81. const createSlackIntegrationData = async () => {
  82. try {
  83. await apiv3Post('/slack-integration-settings/slack-app-integrations');
  84. fetchSlackIntegrationData();
  85. toastSuccess(
  86. t(
  87. 'admin:slack_integration.adding_slack_ws_integration_settings_successful',
  88. ),
  89. );
  90. } catch (error) {
  91. toastError(error);
  92. }
  93. };
  94. const changeSecretAndToken = (secret, token) => {
  95. setSlackSigningSecret(secret);
  96. setSlackBotToken(token);
  97. };
  98. useEffect(() => {
  99. fetchSlackIntegrationData();
  100. }, [fetchSlackIntegrationData]);
  101. const changeCurrentBotSettings = async (botType?: SlackbotType) => {
  102. try {
  103. await apiv3Put('/slack-integration-settings/bot-type', {
  104. currentBotType: botType,
  105. });
  106. setSelectedBotType(undefined);
  107. fetchSlackIntegrationData();
  108. } catch (err) {
  109. toastError(err);
  110. }
  111. };
  112. const botTypeSelectHandler = async (botType: SlackbotType) => {
  113. if (botType === currentBotType) {
  114. return;
  115. }
  116. if (currentBotType == null) {
  117. return changeCurrentBotSettings(botType);
  118. }
  119. setSelectedBotType(botType);
  120. };
  121. const changeCurrentBotSettingsHandler = async () => {
  122. changeCurrentBotSettings(selectedBotType);
  123. toastSuccess(t('admin:slack_integration.bot_reset_successful'));
  124. };
  125. const cancelBotChangeHandler = () => {
  126. setSelectedBotType(undefined);
  127. };
  128. let settingsComponent = <></>;
  129. switch (currentBotType) {
  130. case SlackbotType.OFFICIAL:
  131. settingsComponent = (
  132. <OfficialBotSettings
  133. slackAppIntegrations={slackAppIntegrations}
  134. onClickAddSlackWorkspaceBtn={createSlackIntegrationData}
  135. onPrimaryUpdated={fetchSlackIntegrationData}
  136. onDeleteSlackAppIntegration={fetchSlackIntegrationData}
  137. connectionStatuses={connectionStatuses}
  138. onUpdateTokens={fetchSlackIntegrationData}
  139. onSubmitForm={fetchSlackIntegrationData}
  140. />
  141. );
  142. break;
  143. case SlackbotType.CUSTOM_WITHOUT_PROXY:
  144. settingsComponent = (
  145. <CustomBotWithoutProxySettings
  146. slackBotTokenEnv={slackBotTokenEnv}
  147. slackBotToken={slackBotToken}
  148. slackSigningSecretEnv={slackSigningSecretEnv}
  149. slackSigningSecret={slackSigningSecret}
  150. onTestConnectionInvoked={fetchSlackIntegrationData}
  151. onUpdatedSecretToken={changeSecretAndToken}
  152. connectionStatuses={connectionStatuses}
  153. commandPermission={commandPermission}
  154. eventActionsPermission={eventActionsPermission}
  155. />
  156. );
  157. break;
  158. case SlackbotType.CUSTOM_WITH_PROXY:
  159. settingsComponent = (
  160. <CustomBotWithProxySettings
  161. slackAppIntegrations={slackAppIntegrations}
  162. proxyServerUri={proxyServerUri}
  163. onClickAddSlackWorkspaceBtn={createSlackIntegrationData}
  164. onPrimaryUpdated={fetchSlackIntegrationData}
  165. onDeleteSlackAppIntegration={fetchSlackIntegrationData}
  166. connectionStatuses={connectionStatuses}
  167. onUpdateTokens={fetchSlackIntegrationData}
  168. onSubmitForm={fetchSlackIntegrationData}
  169. />
  170. );
  171. break;
  172. }
  173. if (isLoading) {
  174. return (
  175. <div className="text-muted text-center">
  176. <LoadingSpinner className="me-1 fs-3" />
  177. </div>
  178. );
  179. }
  180. return (
  181. <div data-testid="admin-slack-integration">
  182. <ConfirmBotChangeModal
  183. isOpen={selectedBotType != null}
  184. onConfirmClick={changeCurrentBotSettingsHandler}
  185. onCancelClick={cancelBotChangeHandler}
  186. />
  187. <DeleteSlackBotSettingsModal
  188. isResetAll
  189. isOpen={isDeleteConfirmModalShown}
  190. onClose={() => setIsDeleteConfirmModalShown(false)}
  191. onClickDeleteButton={resetAllSettings}
  192. />
  193. <div className="selecting-bot-type mb-5">
  194. <h2 className="admin-setting-header mb-4">
  195. {t('admin:slack_integration.selecting_bot_types.slack_bot')}
  196. <a
  197. className="ms-2 btn-link small"
  198. href={t('admin:slack_integration.docs_url.slack_integration')}
  199. target="_blank"
  200. rel="noopener noreferrer"
  201. aria-label={t(
  202. 'admin:slack_integration.selecting_bot_types.slack_bot',
  203. )}
  204. >
  205. <span className="visually-hidden">
  206. {t('admin:slack_integration.selecting_bot_types.slack_bot')}
  207. </span>
  208. <span className="material-symbols-outlined ms-1" aria-hidden="true">
  209. help
  210. </span>
  211. </a>
  212. </h2>
  213. {errorCode && (
  214. <div className="alert alert-warning">
  215. <strong>ERROR: </strong>
  216. {errorMsg} ({errorCode})
  217. </div>
  218. )}
  219. <div className="d-flex justify-content-end">
  220. <button
  221. className="btn btn-outline-danger"
  222. type="button"
  223. onClick={() => setIsDeleteConfirmModalShown(true)}
  224. >
  225. {t('admin:slack_integration.reset_all_settings')}
  226. </button>
  227. </div>
  228. <div className="my-5 d-flex flex-wrap-reverse justify-content-center">
  229. {botTypes.map((botType) => {
  230. return (
  231. <div key={botType} className="m-3">
  232. <BotTypeCard
  233. botType={botType}
  234. isActive={currentBotType === botType}
  235. onBotTypeSelectHandler={botTypeSelectHandler}
  236. />
  237. </div>
  238. );
  239. })}
  240. </div>
  241. </div>
  242. {settingsComponent}
  243. </div>
  244. );
  245. };
  246. SlackIntegration.displayName = 'SlackIntegration';