tableActions.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import {Fragment} from 'react';
  2. import type {Location} from 'history';
  3. import Feature from 'sentry/components/acl/feature';
  4. import FeatureDisabled from 'sentry/components/acl/featureDisabled';
  5. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  6. import {Button} from 'sentry/components/button';
  7. import DataExport, {ExportQueryType} from 'sentry/components/dataExport';
  8. import {InvestigationRuleCreation} from 'sentry/components/dynamicSampling/investigationRule';
  9. import {Hovercard} from 'sentry/components/hovercard';
  10. import {IconDownload, IconSliders, IconTag} from 'sentry/icons';
  11. import {t} from 'sentry/locale';
  12. import type {OrganizationSummary} from 'sentry/types/organization';
  13. import {trackAnalytics} from 'sentry/utils/analytics';
  14. import {parseCursor} from 'sentry/utils/cursor';
  15. import type {TableData} from 'sentry/utils/discover/discoverQuery';
  16. import type EventView from 'sentry/utils/discover/eventView';
  17. import {SavedQueryDatasets} from 'sentry/utils/discover/types';
  18. import {useLocation} from 'sentry/utils/useLocation';
  19. import useOrganization from 'sentry/utils/useOrganization';
  20. import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
  21. import {downloadAsCsv} from '../utils';
  22. type Props = {
  23. error: string | null;
  24. eventView: EventView;
  25. isLoading: boolean;
  26. location: Location;
  27. onChangeShowTags: () => void;
  28. onEdit: () => void;
  29. organization: OrganizationSummary;
  30. showTags: boolean;
  31. tableData: TableData | null | undefined;
  32. title: string;
  33. queryDataset?: SavedQueryDatasets;
  34. supportsInvestigationRule?: boolean;
  35. };
  36. function handleDownloadAsCsv(title: string, {organization, eventView, tableData}: Props) {
  37. trackAnalytics('discover_v2.results.download_csv', {
  38. organization: organization.id, // org summary
  39. });
  40. downloadAsCsv(tableData, eventView.getColumns(), title);
  41. }
  42. function renderDownloadButton(canEdit: boolean, props: Props) {
  43. const {tableData} = props;
  44. return (
  45. <Feature
  46. features="organizations:discover-query"
  47. renderDisabled={() => renderBrowserExportButton(canEdit, props)}
  48. >
  49. {tableData?.data && tableData.data.length < 50
  50. ? renderBrowserExportButton(canEdit, props)
  51. : renderAsyncExportButton(canEdit, props)}
  52. </Feature>
  53. );
  54. }
  55. function renderBrowserExportButton(canEdit: boolean, props: Props) {
  56. const {isLoading, error} = props;
  57. const disabled = isLoading || error !== null || canEdit === false;
  58. const onClick = disabled ? undefined : () => handleDownloadAsCsv(props.title, props);
  59. return (
  60. <Button
  61. size="sm"
  62. disabled={disabled}
  63. onClick={onClick}
  64. data-test-id="grid-download-csv"
  65. icon={<IconDownload />}
  66. title={
  67. !disabled
  68. ? t(
  69. "There aren't that many results, start your export and it'll download immediately."
  70. )
  71. : undefined
  72. }
  73. >
  74. {t('Export All')}
  75. </Button>
  76. );
  77. }
  78. function renderAsyncExportButton(canEdit: boolean, props: Props) {
  79. const {isLoading, error, location, eventView} = props;
  80. const disabled = isLoading || error !== null || canEdit === false;
  81. return (
  82. <DataExport
  83. payload={{
  84. queryType: ExportQueryType.DISCOVER,
  85. queryInfo: eventView.getEventsAPIPayload(location),
  86. }}
  87. disabled={disabled}
  88. icon={<IconDownload />}
  89. >
  90. {t('Export All')}
  91. </DataExport>
  92. );
  93. }
  94. // Placate eslint proptype checking
  95. function renderEditButton(canEdit: boolean, props: Props) {
  96. const onClick = canEdit ? props.onEdit : undefined;
  97. return (
  98. <GuideAnchor target="columns_header_button">
  99. <Button
  100. size="sm"
  101. disabled={!canEdit}
  102. onClick={onClick}
  103. data-test-id="grid-edit-enable"
  104. icon={<IconSliders />}
  105. >
  106. {t('Columns')}
  107. </Button>
  108. </GuideAnchor>
  109. );
  110. }
  111. // Placate eslint proptype checking
  112. function renderSummaryButton({onChangeShowTags, showTags}: Props) {
  113. return (
  114. <Button size="sm" onClick={onChangeShowTags} icon={<IconTag />}>
  115. {showTags ? t('Hide Tags') : t('Show Tags')}
  116. </Button>
  117. );
  118. }
  119. type FeatureWrapperProps = Props & {
  120. children: (hasFeature: boolean, props: Props) => React.ReactNode;
  121. };
  122. function FeatureWrapper(props: FeatureWrapperProps) {
  123. const noEditMessage = t('Requires discover query feature.');
  124. const editFeatures = ['organizations:discover-query'];
  125. const renderDisabled = p => (
  126. <Hovercard
  127. body={
  128. <FeatureDisabled
  129. features={p.features}
  130. hideHelpToggle
  131. message={noEditMessage}
  132. featureName={noEditMessage}
  133. />
  134. }
  135. >
  136. {p.children(p)}
  137. </Hovercard>
  138. );
  139. return (
  140. <Feature
  141. hookName="feature-disabled:grid-editable-actions"
  142. renderDisabled={renderDisabled}
  143. features={editFeatures}
  144. >
  145. {({hasFeature}) => props.children(hasFeature, props)}
  146. </Feature>
  147. );
  148. }
  149. function TableActions(props: Props) {
  150. const {tableData, queryDataset, supportsInvestigationRule} = props;
  151. const location = useLocation();
  152. const organization = useOrganization();
  153. const cursor = location?.query?.cursor;
  154. const cursorOffset = parseCursor(cursor)?.offset ?? 0;
  155. const numSamples = tableData?.data?.length ?? null;
  156. const totalNumSamples = numSamples === null ? null : numSamples + cursorOffset;
  157. const isTransactions =
  158. hasDatasetSelector(organization) && queryDataset === SavedQueryDatasets.TRANSACTIONS;
  159. return (
  160. <Fragment>
  161. {supportsInvestigationRule &&
  162. (!hasDatasetSelector(organization) || isTransactions) && (
  163. <InvestigationRuleCreation
  164. {...props}
  165. buttonProps={{size: 'sm'}}
  166. numSamples={totalNumSamples}
  167. key="investigationRuleCreation"
  168. />
  169. )}
  170. <FeatureWrapper {...props} key="edit">
  171. {renderEditButton}
  172. </FeatureWrapper>
  173. <FeatureWrapper {...props} key="download">
  174. {renderDownloadButton}
  175. </FeatureWrapper>
  176. {renderSummaryButton(props)}
  177. </Fragment>
  178. );
  179. }
  180. export default TableActions;