dataExport.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import {useCallback, useEffect, useRef, useState} from 'react';
  2. import debounce from 'lodash/debounce';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {Client} from 'sentry/api';
  5. import Feature from 'sentry/components/acl/feature';
  6. import Button from 'sentry/components/button';
  7. import {t} from 'sentry/locale';
  8. import {Organization} from 'sentry/types';
  9. import withApi from 'sentry/utils/withApi';
  10. import withOrganization from 'sentry/utils/withOrganization';
  11. // NOTE: Coordinate with other ExportQueryType (src/sentry/data_export/base.py)
  12. export enum ExportQueryType {
  13. IssuesByTag = 'Issues-by-Tag',
  14. Discover = 'Discover',
  15. }
  16. interface DataExportPayload {
  17. queryInfo: any;
  18. queryType: ExportQueryType; // TODO(ts): Formalize different possible payloads
  19. }
  20. interface DataExportProps {
  21. api: Client;
  22. organization: Organization;
  23. payload: DataExportPayload;
  24. children?: React.ReactNode;
  25. disabled?: boolean;
  26. icon?: React.ReactNode;
  27. }
  28. function DataExport({
  29. api,
  30. children,
  31. disabled,
  32. organization,
  33. payload,
  34. icon,
  35. }: DataExportProps): React.ReactElement {
  36. const unmountedRef = useRef(false);
  37. const [inProgress, setInProgress] = useState(false);
  38. // We clear the indicator if export props change so that the user
  39. // can fire another export without having to wait for the previous one to finish.
  40. useEffect(() => {
  41. if (inProgress) {
  42. setInProgress(false);
  43. }
  44. // We are skipping the inProgress dependency because it would have fired on each handleDataExport
  45. // call and would have immediately turned off the value giving users no feedback on their click action.
  46. // An alternative way to handle this would have probably been to key the component by payload/queryType,
  47. // but that seems like it can be a complex object so tracking changes could result in very brittle behavior.
  48. // eslint-disable-next-line react-hooks/exhaustive-deps
  49. }, [payload.queryType, payload.queryInfo]);
  50. // Tracking unmounting of the component to prevent setState call on unmounted component
  51. useEffect(() => {
  52. return () => {
  53. unmountedRef.current = true;
  54. };
  55. }, []);
  56. const handleDataExport = useCallback(() => {
  57. setInProgress(true);
  58. // This is a fire and forget request.
  59. api
  60. .requestPromise(`/organizations/${organization.slug}/data-export/`, {
  61. includeAllArgs: true,
  62. method: 'POST',
  63. data: {
  64. query_type: payload.queryType,
  65. query_info: payload.queryInfo,
  66. },
  67. })
  68. .then(([_data, _, response]) => {
  69. // If component has unmounted, don't do anything
  70. if (unmountedRef.current) {
  71. return;
  72. }
  73. addSuccessMessage(
  74. response?.status === 201
  75. ? t(
  76. "Sit tight. We'll shoot you an email when your data is ready for download."
  77. )
  78. : t("It looks like we're already working on it. Sit tight, we'll email you.")
  79. );
  80. })
  81. .catch(err => {
  82. // If component has unmounted, don't do anything
  83. if (unmountedRef.current) {
  84. return;
  85. }
  86. const message =
  87. err?.responseJSON?.detail ??
  88. t(
  89. "We tried our hardest, but we couldn't export your data. Give it another go."
  90. );
  91. addErrorMessage(message);
  92. setInProgress(false);
  93. });
  94. }, [payload.queryInfo, payload.queryType, organization.slug, api]);
  95. return (
  96. <Feature features={['organizations:discover-query']}>
  97. {inProgress ? (
  98. <Button
  99. size="sm"
  100. priority="default"
  101. title={t(
  102. "You can get on with your life. We'll email you when your data's ready."
  103. )}
  104. disabled
  105. icon={icon}
  106. >
  107. {t("We're working on it...")}
  108. </Button>
  109. ) : (
  110. <Button
  111. onClick={debounce(handleDataExport, 500)}
  112. disabled={disabled || false}
  113. size="sm"
  114. priority="default"
  115. title={t(
  116. "Put your data to work. Start your export and we'll email you when it's finished."
  117. )}
  118. icon={icon}
  119. >
  120. {children ? children : t('Export All to CSV')}
  121. </Button>
  122. )}
  123. </Feature>
  124. );
  125. }
  126. export {DataExport};
  127. export default withApi(withOrganization(DataExport));