metricsExtractionRulesTable.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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 {DEFAULT_METRICS_CARDINALITY_LIMIT} from 'sentry/utils/metrics/constants';
  20. import {useMetricsCardinality} from 'sentry/utils/metrics/useMetricsCardinality';
  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, isLoading: isLoadingExtractionRules} =
  37. useMetricsExtractionRules(organization.slug, project.id, {query});
  38. const {mutate: deleteMetricsExtractionRules} = useDeleteMetricsExtractionRules(
  39. organization.slug,
  40. project.id
  41. );
  42. const {data: cardinality, isLoading: isLoadingCardinality} = useMetricsCardinality({
  43. projects: [project.id],
  44. });
  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((rule: MetricsExtractionRule) => {
  67. openExtractionRuleEditModal({
  68. metricExtractionRule: rule,
  69. });
  70. }, []);
  71. const handleCreate = useCallback(() => {
  72. openExtractionRuleCreateModal({projectId: project.id});
  73. }, [project]);
  74. return (
  75. <Fragment>
  76. <SearchWrapper>
  77. <h6>{t('Span Metrics')}</h6>
  78. <FlexSpacer />
  79. <SearchBar
  80. placeholder={t('Search Metrics')}
  81. onChange={setQuery}
  82. query={query}
  83. size="sm"
  84. />
  85. <Button onClick={handleCreate} priority="primary" size="sm">
  86. {t('Add Metric')}
  87. </Button>
  88. </SearchWrapper>
  89. <RulesTable
  90. isLoading={isLoadingExtractionRules || isLoadingCardinality}
  91. onDelete={handleDelete}
  92. onEdit={handleEdit}
  93. extractionRules={extractionRules || []}
  94. cardinality={cardinality || {}}
  95. hasSearch={!!query}
  96. />
  97. </Fragment>
  98. );
  99. }
  100. interface RulesTableProps {
  101. cardinality: Record<string, number>;
  102. extractionRules: MetricsExtractionRule[];
  103. hasSearch: boolean;
  104. isLoading: boolean;
  105. onDelete: (rule: MetricsExtractionRule) => void;
  106. onEdit: (rule: MetricsExtractionRule) => void;
  107. }
  108. function RulesTable({
  109. extractionRules,
  110. cardinality,
  111. isLoading,
  112. onDelete,
  113. onEdit,
  114. hasSearch,
  115. }: RulesTableProps) {
  116. const {members} = useMembers();
  117. const getMaxCardinality = (rule: MetricsExtractionRule) => {
  118. const mris = rule.conditions.flatMap(condition => condition.mris);
  119. return mris.reduce((acc, mri) => Math.max(acc, cardinality[mri] || 0), 0);
  120. };
  121. return (
  122. <ExtractionRulesPanelTable
  123. headers={[
  124. <Cell key="spanAttribute">
  125. <IconArrow size="xs" direction="down" />
  126. {t('Span attribute')}
  127. </Cell>,
  128. <Cell key="createdBy">{t('Created by')}</Cell>,
  129. <Cell right key="createdBy">
  130. {t('Created at')}
  131. </Cell>,
  132. <Cell right key="actions">
  133. {t('Actions')}
  134. </Cell>,
  135. ]}
  136. emptyMessage={
  137. hasSearch
  138. ? t('No metrics match the query.')
  139. : t('You have not created any span metrics yet.')
  140. }
  141. isEmpty={extractionRules.length === 0}
  142. isLoading={isLoading}
  143. >
  144. {extractionRules
  145. .toSorted((a, b) => a?.spanAttribute?.localeCompare(b?.spanAttribute))
  146. .map(rule => {
  147. const createdByUser = members.find(
  148. member => member.id === String(rule.createdById)
  149. );
  150. return (
  151. <Fragment key={rule.spanAttribute + rule.unit}>
  152. <Cell>
  153. {getMaxCardinality(rule) >= DEFAULT_METRICS_CARDINALITY_LIMIT ? (
  154. <Tooltip
  155. title={t(
  156. 'Some of your defined queries are exeeding the cardinality limit. Remove tags or add filters to receive accurate data.'
  157. )}
  158. >
  159. <IconWarning size="xs" color="yellow300" />
  160. </Tooltip>
  161. ) : null}
  162. {rule.spanAttribute}
  163. </Cell>
  164. <Cell>
  165. <UserBadge
  166. displayName={createdByUser?.name ?? t('Unknown')}
  167. user={createdByUser}
  168. hideEmail
  169. avatarSize={24}
  170. />
  171. </Cell>
  172. <Cell>
  173. <DateTime date={rule.dateAdded} />
  174. </Cell>
  175. <Cell right>
  176. <Button
  177. aria-label={t('Edit metric')}
  178. size="xs"
  179. icon={<IconEdit />}
  180. borderless
  181. onClick={() => onEdit(rule)}
  182. />
  183. <Button
  184. aria-label={t('Delete metric')}
  185. size="xs"
  186. icon={<IconDelete />}
  187. borderless
  188. onClick={() => onDelete(rule)}
  189. />
  190. </Cell>
  191. </Fragment>
  192. );
  193. })}
  194. </ExtractionRulesPanelTable>
  195. );
  196. }
  197. const SearchWrapper = styled('div')`
  198. display: flex;
  199. align-items: flex-start;
  200. margin-top: ${space(4)};
  201. margin-bottom: ${space(1)};
  202. gap: ${space(1)};
  203. & > h6 {
  204. margin: 0;
  205. }
  206. `;
  207. const FlexSpacer = styled('div')`
  208. flex: 1;
  209. `;
  210. const ExtractionRulesPanelTable = styled(PanelTable)`
  211. grid-template-columns: 1fr repeat(3, max-content);
  212. `;
  213. const Cell = styled('div')<{right?: boolean}>`
  214. display: flex;
  215. align-items: center;
  216. align-self: stretch;
  217. gap: ${space(0.5)};
  218. justify-content: ${p => (p.right ? 'flex-end' : 'flex-start')};
  219. `;