metricsExtractionRulesTable.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import {Fragment, useCallback} from 'react';
  2. import styled from '@emotion/styled';
  3. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  4. import {Button} from 'sentry/components/button';
  5. import {openConfirmModal} from 'sentry/components/confirm';
  6. import {DateTime} from 'sentry/components/dateTime';
  7. import UserBadge from 'sentry/components/idBadge/userBadge';
  8. import {PanelTable} from 'sentry/components/panels/panelTable';
  9. import SearchBar from 'sentry/components/searchBar';
  10. import {Tooltip} from 'sentry/components/tooltip';
  11. import {IconWarning} from 'sentry/icons';
  12. import {IconArrow} from 'sentry/icons/iconArrow';
  13. import {IconDelete} from 'sentry/icons/iconDelete';
  14. import {IconEdit} from 'sentry/icons/iconEdit';
  15. import {t} from 'sentry/locale';
  16. import {space} from 'sentry/styles/space';
  17. import type {MetricsExtractionRule} from 'sentry/types/metrics';
  18. import type {Project} from 'sentry/types/project';
  19. import {trackAnalytics} from 'sentry/utils/analytics';
  20. import {useCardinalityLimitedMetricVolume} from 'sentry/utils/metrics/useCardinalityLimitedMetricVolume';
  21. import {useMembers} from 'sentry/utils/useMembers';
  22. import useOrganization from 'sentry/utils/useOrganization';
  23. import {openExtractionRuleCreateModal} from 'sentry/views/settings/projectMetrics/metricsExtractionRuleCreateModal';
  24. import {openExtractionRuleEditModal} from 'sentry/views/settings/projectMetrics/metricsExtractionRuleEditModal';
  25. import {
  26. useDeleteMetricsExtractionRules,
  27. useMetricsExtractionRules,
  28. } from 'sentry/views/settings/projectMetrics/utils/useMetricsExtractionRules';
  29. import {useSearchQueryParam} from 'sentry/views/settings/projectMetrics/utils/useSearchQueryParam';
  30. type Props = {
  31. project: Project;
  32. };
  33. export function MetricsExtractionRulesTable({project}: Props) {
  34. const organization = useOrganization();
  35. const [query, setQuery] = useSearchQueryParam('query');
  36. const {data: extractionRules, isPending: isLoadingExtractionRules} =
  37. useMetricsExtractionRules({
  38. orgId: organization.slug,
  39. projectId: project.id,
  40. query: {query},
  41. });
  42. const {mutate: deleteMetricsExtractionRules} = useDeleteMetricsExtractionRules(
  43. organization.slug,
  44. project.id
  45. );
  46. const {data: cardinality, isPending: isLoadingCardinality} =
  47. useCardinalityLimitedMetricVolume({
  48. projects: [project.id],
  49. });
  50. const handleDelete = useCallback(
  51. (rule: MetricsExtractionRule) => {
  52. openConfirmModal({
  53. onConfirm: () =>
  54. deleteMetricsExtractionRules(
  55. {metricsExtractionRules: [rule]},
  56. {
  57. onSuccess: () => {
  58. addSuccessMessage(t('Metric deleted'));
  59. trackAnalytics('metrics_extractions.delete', {organization});
  60. },
  61. onError: () => {
  62. addErrorMessage(t('Failed to delete metric'));
  63. },
  64. }
  65. ),
  66. message: t('Are you sure you want to delete this metric?'),
  67. confirmText: t('Delete Metric'),
  68. });
  69. },
  70. [deleteMetricsExtractionRules, organization]
  71. );
  72. const handleEdit = useCallback(
  73. (rule: MetricsExtractionRule) => {
  74. openExtractionRuleEditModal({
  75. organization,
  76. metricExtractionRule: rule,
  77. source: 'settings',
  78. });
  79. },
  80. [organization]
  81. );
  82. const handleCreate = useCallback(() => {
  83. openExtractionRuleCreateModal({
  84. organization,
  85. projectId: project.id,
  86. source: 'settings',
  87. });
  88. }, [organization, project.id]);
  89. return (
  90. <Fragment>
  91. <SearchWrapper>
  92. <h6>{t('Span Metrics')}</h6>
  93. <FlexSpacer />
  94. <SearchBar
  95. placeholder={t('Search Metrics')}
  96. onChange={setQuery}
  97. query={query}
  98. size="sm"
  99. />
  100. <Button onClick={handleCreate} priority="primary" size="sm">
  101. {t('Add Metric')}
  102. </Button>
  103. </SearchWrapper>
  104. <RulesTable
  105. isLoading={isLoadingExtractionRules || isLoadingCardinality}
  106. onDelete={handleDelete}
  107. onEdit={handleEdit}
  108. extractionRules={extractionRules || []}
  109. cardinality={cardinality || {}}
  110. hasSearch={!!query}
  111. />
  112. </Fragment>
  113. );
  114. }
  115. interface RulesTableProps {
  116. cardinality: Record<string, number>;
  117. extractionRules: MetricsExtractionRule[];
  118. hasSearch: boolean;
  119. isLoading: boolean;
  120. onDelete: (rule: MetricsExtractionRule) => void;
  121. onEdit: (rule: MetricsExtractionRule) => void;
  122. }
  123. function RulesTable({
  124. extractionRules,
  125. cardinality,
  126. isLoading,
  127. onDelete,
  128. onEdit,
  129. hasSearch,
  130. }: RulesTableProps) {
  131. const {members} = useMembers();
  132. const isCardinalityLimited = (rule: MetricsExtractionRule): boolean => {
  133. const mris = rule.conditions.flatMap(condition => condition.mris);
  134. return mris.some(conditionMri => cardinality[conditionMri] > 0);
  135. };
  136. return (
  137. <ExtractionRulesPanelTable
  138. headers={[
  139. <Cell key="spanAttribute">
  140. <IconArrow size="xs" direction="down" />
  141. {t('Span attribute')}
  142. </Cell>,
  143. <Cell key="createdBy">{t('Created by')}</Cell>,
  144. <Cell right key="createdBy">
  145. {t('Created at')}
  146. </Cell>,
  147. <Cell right key="actions">
  148. {t('Actions')}
  149. </Cell>,
  150. ]}
  151. emptyMessage={
  152. hasSearch
  153. ? t('No metrics match the query.')
  154. : t('You have not created any span metrics yet.')
  155. }
  156. isEmpty={extractionRules.length === 0}
  157. isLoading={isLoading}
  158. >
  159. {extractionRules
  160. .toSorted((a, b) => a?.spanAttribute?.localeCompare(b?.spanAttribute))
  161. .map(rule => {
  162. const createdByUser = members.find(
  163. member => member.id === String(rule.createdById)
  164. );
  165. return (
  166. <Fragment key={rule.spanAttribute + rule.unit}>
  167. <Cell>
  168. {isCardinalityLimited(rule) ? (
  169. <Tooltip
  170. title={t(
  171. 'Some of your defined queries are exeeding the cardinality limit. Remove tags or add filters to receive accurate data.'
  172. )}
  173. containerDisplayMode="inline-flex"
  174. >
  175. <IconWarning
  176. size="xs"
  177. color="yellow300"
  178. role="img"
  179. aria-label={t('Exceeding the cardinality limit warning')}
  180. />
  181. </Tooltip>
  182. ) : null}
  183. {rule.spanAttribute}
  184. </Cell>
  185. <Cell>
  186. <UserBadge
  187. displayName={createdByUser?.name ?? t('Unknown')}
  188. user={createdByUser}
  189. hideEmail
  190. avatarSize={24}
  191. />
  192. </Cell>
  193. <Cell>
  194. <DateTime date={rule.dateAdded} />
  195. </Cell>
  196. <Cell right>
  197. <Button
  198. aria-label={t('Edit metric')}
  199. size="xs"
  200. icon={<IconEdit />}
  201. borderless
  202. onClick={() => onEdit(rule)}
  203. />
  204. <Button
  205. aria-label={t('Delete metric')}
  206. size="xs"
  207. icon={<IconDelete />}
  208. borderless
  209. onClick={() => onDelete(rule)}
  210. />
  211. </Cell>
  212. </Fragment>
  213. );
  214. })}
  215. </ExtractionRulesPanelTable>
  216. );
  217. }
  218. const SearchWrapper = styled('div')`
  219. display: flex;
  220. align-items: flex-start;
  221. margin-top: ${space(4)};
  222. margin-bottom: ${space(1)};
  223. gap: ${space(1)};
  224. & > h6 {
  225. margin: 0;
  226. }
  227. `;
  228. const FlexSpacer = styled('div')`
  229. flex: 1;
  230. `;
  231. const ExtractionRulesPanelTable = styled(PanelTable)`
  232. grid-template-columns: 1fr repeat(3, max-content);
  233. `;
  234. const Cell = styled('div')<{right?: boolean}>`
  235. display: flex;
  236. align-items: center;
  237. align-self: stretch;
  238. gap: ${space(0.5)};
  239. justify-content: ${p => (p.right ? 'flex-end' : 'flex-start')};
  240. `;