metricsExtractionRulesTable.tsx 7.6 KB

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