metricsExtractionRulesTable.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import {Fragment, useCallback, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {openModal} from 'sentry/actionCreators/modal';
  5. import Tag from 'sentry/components/badge/tag';
  6. import {Button, LinkButton} from 'sentry/components/button';
  7. import {openConfirmModal} from 'sentry/components/confirm';
  8. import {modalCss} from 'sentry/components/featureFeedback/feedbackModal';
  9. import {PanelTable} from 'sentry/components/panels/panelTable';
  10. import SearchBar from 'sentry/components/searchBar';
  11. import {IconArrow} from 'sentry/icons/iconArrow';
  12. import {IconDelete} from 'sentry/icons/iconDelete';
  13. import {IconEdit} from 'sentry/icons/iconEdit';
  14. import {t} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import type {Project} from 'sentry/types/project';
  17. import {getReadableMetricType} from 'sentry/utils/metrics/formatters';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. import {MetricsExtractionRuleEditModal} from 'sentry/views/settings/projectMetrics/metricsExtractionRuleEditModal';
  20. import {
  21. type MetricsExtractionRule,
  22. useDeleteMetricsExtractionRules,
  23. useMetricsExtractionRules,
  24. } from 'sentry/views/settings/projectMetrics/utils/api';
  25. import {useSearchQueryParam} from 'sentry/views/settings/projectMetrics/utils/useSearchQueryParam';
  26. type Props = {
  27. project: Project;
  28. };
  29. export function MetricsExtractionRulesTable({project}: Props) {
  30. const organization = useOrganization();
  31. const [query, setQuery] = useSearchQueryParam('query');
  32. const {data: extractionRules, isLoading} = useMetricsExtractionRules(
  33. organization.slug,
  34. project.slug
  35. );
  36. const {mutate: deleteMetricsExtractionRules} = useDeleteMetricsExtractionRules(
  37. organization.slug,
  38. project.slug
  39. );
  40. const filteredExtractionRules = useMemo(() => {
  41. return (extractionRules || []).filter(rule =>
  42. rule.spanAttribute.toLowerCase().includes(query.toLowerCase())
  43. );
  44. }, [extractionRules, query]);
  45. const handleDelete = useCallback(
  46. (rule: MetricsExtractionRule) => {
  47. openConfirmModal({
  48. onConfirm: () =>
  49. deleteMetricsExtractionRules(
  50. {metricsExtractionRules: [rule]},
  51. {
  52. onSuccess: () => {
  53. addSuccessMessage(t('Metric extraction rule deleted'));
  54. },
  55. onError: () => {
  56. addErrorMessage(t('Failed to delete metric extraction rule'));
  57. },
  58. }
  59. ),
  60. message: t('Are you sure you want to delete this extraction rule?'),
  61. confirmText: t('Delete Extraction Rule'),
  62. });
  63. },
  64. [deleteMetricsExtractionRules]
  65. );
  66. const handleEdit = useCallback(
  67. (rule: MetricsExtractionRule) => {
  68. openModal(
  69. props => (
  70. <MetricsExtractionRuleEditModal
  71. project={project}
  72. metricExtractionRule={rule}
  73. {...props}
  74. />
  75. ),
  76. {modalCss}
  77. );
  78. },
  79. [project]
  80. );
  81. return (
  82. <Fragment>
  83. <SearchWrapper>
  84. <h6>{t('Metric Extraction Rules')}</h6>
  85. <FlexSpacer />
  86. <SearchBar
  87. placeholder={t('Search Extraction Rules')}
  88. onChange={setQuery}
  89. query={query}
  90. size="sm"
  91. />
  92. <LinkButton
  93. to={`/settings/projects/${project.slug}/metrics/extract-metric`}
  94. priority="primary"
  95. size="sm"
  96. >
  97. {t('Add Extraction Rule')}
  98. </LinkButton>
  99. </SearchWrapper>
  100. <RulesTable
  101. isLoading={isLoading}
  102. onDelete={handleDelete}
  103. onEdit={handleEdit}
  104. extractionRules={filteredExtractionRules}
  105. hasSearch={!!query}
  106. />
  107. </Fragment>
  108. );
  109. }
  110. interface RulesTableProps {
  111. extractionRules: MetricsExtractionRule[];
  112. hasSearch: boolean;
  113. isLoading: boolean;
  114. onDelete: (rule: MetricsExtractionRule) => void;
  115. onEdit: (rule: MetricsExtractionRule) => void;
  116. }
  117. function RulesTable({
  118. extractionRules,
  119. isLoading,
  120. onDelete,
  121. onEdit,
  122. hasSearch,
  123. }: RulesTableProps) {
  124. return (
  125. <ExtractionRulesPanelTable
  126. headers={[
  127. <Cell key="spanAttribute">
  128. <IconArrow size="xs" direction="down" />
  129. {t('Span attribute')}
  130. </Cell>,
  131. <Cell right key="type">
  132. {t('Type')}
  133. </Cell>,
  134. <Cell right key="unit">
  135. {t('Unit')}
  136. </Cell>,
  137. <Cell right key="filters">
  138. {t('Filters')}
  139. </Cell>,
  140. <Cell right key="tags">
  141. {t('Tags')}
  142. </Cell>,
  143. <Cell right key="actions">
  144. {t('Actions')}
  145. </Cell>,
  146. ]}
  147. emptyMessage={
  148. hasSearch
  149. ? t('No extraction rules match the query.')
  150. : t('You have not created any extraction rules yet.')
  151. }
  152. isEmpty={extractionRules.length === 0}
  153. isLoading={isLoading}
  154. >
  155. {extractionRules
  156. .toSorted((a, b) => a?.spanAttribute?.localeCompare(b?.spanAttribute))
  157. .map(rule => (
  158. <Fragment key={rule.spanAttribute + rule.type + rule.unit}>
  159. <Cell>{rule.spanAttribute}</Cell>
  160. <Cell right>
  161. <Tag>{getReadableMetricType(rule.type)}</Tag>
  162. </Cell>
  163. <Cell right>
  164. <Tag>{rule.unit}</Tag>
  165. </Cell>
  166. <Cell right>
  167. {rule.conditions.length ? (
  168. <Button priority="link" onClick={() => onEdit(rule)}>
  169. {rule.conditions.length}
  170. </Button>
  171. ) : (
  172. <NoValue>{t('(none)')}</NoValue>
  173. )}
  174. </Cell>
  175. <Cell right>
  176. {rule.tags.length ? (
  177. <Button priority="link" onClick={() => onEdit(rule)}>
  178. {rule.tags.length}
  179. </Button>
  180. ) : (
  181. <NoValue>{t('(none)')}</NoValue>
  182. )}
  183. </Cell>
  184. <Cell right>
  185. <Button
  186. aria-label={t('Delete rule')}
  187. size="xs"
  188. icon={<IconDelete />}
  189. borderless
  190. onClick={() => onDelete(rule)}
  191. />
  192. <Button
  193. aria-label={t('Edit rule')}
  194. size="xs"
  195. icon={<IconEdit />}
  196. borderless
  197. onClick={() => onEdit(rule)}
  198. />
  199. </Cell>
  200. </Fragment>
  201. ))}
  202. </ExtractionRulesPanelTable>
  203. );
  204. }
  205. const SearchWrapper = styled('div')`
  206. display: flex;
  207. align-items: flex-start;
  208. margin-top: ${space(4)};
  209. margin-bottom: ${space(1)};
  210. gap: ${space(1)};
  211. & > h6 {
  212. margin: 0;
  213. }
  214. `;
  215. const FlexSpacer = styled('div')`
  216. flex: 1;
  217. `;
  218. const ExtractionRulesPanelTable = styled(PanelTable)`
  219. grid-template-columns: 1fr repeat(5, min-content);
  220. `;
  221. const Cell = styled('div')<{right?: boolean}>`
  222. display: flex;
  223. align-items: center;
  224. align-self: stretch;
  225. gap: ${space(0.5)};
  226. justify-content: ${p => (p.right ? 'flex-end' : 'flex-start')};
  227. `;
  228. const NoValue = styled('span')`
  229. color: ${p => p.theme.subText};
  230. `;