dataExport.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  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. "We tried our hardest, but we couldn't export your data. Give it another go.";
  89. addErrorMessage(t(message));
  90. setInProgress(false);
  91. });
  92. }, [payload.queryInfo, payload.queryType, organization.slug, api]);
  93. return (
  94. <Feature features={['organizations:discover-query']}>
  95. {inProgress ? (
  96. <Button
  97. size="sm"
  98. priority="default"
  99. title="You can get on with your life. We'll email you when your data's ready."
  100. disabled
  101. icon={icon}
  102. >
  103. {t("We're working on it...")}
  104. </Button>
  105. ) : (
  106. <Button
  107. onClick={debounce(handleDataExport, 500)}
  108. disabled={disabled || false}
  109. size="sm"
  110. priority="default"
  111. title="Put your data to work. Start your export and we'll email you when it's finished."
  112. icon={icon}
  113. >
  114. {children ? children : t('Export All to CSV')}
  115. </Button>
  116. )}
  117. </Feature>
  118. );
  119. }
  120. export {DataExport};
  121. export default withApi(withOrganization(DataExport));