123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- import {Fragment, useCallback} from 'react';
- import styled from '@emotion/styled';
- import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
- import {Button} from 'sentry/components/button';
- import {openConfirmModal} from 'sentry/components/confirm';
- import {DateTime} from 'sentry/components/dateTime';
- import UserBadge from 'sentry/components/idBadge/userBadge';
- import {PanelTable} from 'sentry/components/panels/panelTable';
- import SearchBar from 'sentry/components/searchBar';
- import {Tooltip} from 'sentry/components/tooltip';
- import {IconWarning} from 'sentry/icons';
- import {IconArrow} from 'sentry/icons/iconArrow';
- import {IconDelete} from 'sentry/icons/iconDelete';
- import {IconEdit} from 'sentry/icons/iconEdit';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {MetricsExtractionRule} from 'sentry/types/metrics';
- import type {Project} from 'sentry/types/project';
- import {useCardinalityLimitedMetricVolume} from 'sentry/utils/metrics/useCardinalityLimitedMetricVolume';
- import {useMembers} from 'sentry/utils/useMembers';
- import useOrganization from 'sentry/utils/useOrganization';
- import {openExtractionRuleCreateModal} from 'sentry/views/settings/projectMetrics/metricsExtractionRuleCreateModal';
- import {openExtractionRuleEditModal} from 'sentry/views/settings/projectMetrics/metricsExtractionRuleEditModal';
- import {
- useDeleteMetricsExtractionRules,
- useMetricsExtractionRules,
- } from 'sentry/views/settings/projectMetrics/utils/useMetricsExtractionRules';
- import {useSearchQueryParam} from 'sentry/views/settings/projectMetrics/utils/useSearchQueryParam';
- type Props = {
- project: Project;
- };
- export function MetricsExtractionRulesTable({project}: Props) {
- const organization = useOrganization();
- const [query, setQuery] = useSearchQueryParam('query');
- const {data: extractionRules, isLoading: isLoadingExtractionRules} =
- useMetricsExtractionRules({
- orgId: organization.slug,
- projectId: project.id,
- query: {query},
- });
- const {mutate: deleteMetricsExtractionRules} = useDeleteMetricsExtractionRules(
- organization.slug,
- project.id
- );
- const {data: cardinality, isLoading: isLoadingCardinality} =
- useCardinalityLimitedMetricVolume({
- projects: [project.id],
- });
- const handleDelete = useCallback(
- (rule: MetricsExtractionRule) => {
- openConfirmModal({
- onConfirm: () =>
- deleteMetricsExtractionRules(
- {metricsExtractionRules: [rule]},
- {
- onSuccess: () => {
- addSuccessMessage(t('Metric extraction rule deleted'));
- },
- onError: () => {
- addErrorMessage(t('Failed to delete metric extraction rule'));
- },
- }
- ),
- message: t('Are you sure you want to delete this extraction rule?'),
- confirmText: t('Delete Extraction Rule'),
- });
- },
- [deleteMetricsExtractionRules]
- );
- const handleEdit = useCallback((rule: MetricsExtractionRule) => {
- openExtractionRuleEditModal({
- metricExtractionRule: rule,
- });
- }, []);
- const handleCreate = useCallback(() => {
- openExtractionRuleCreateModal({projectId: project.id});
- }, [project]);
- return (
- <Fragment>
- <SearchWrapper>
- <h6>{t('Span Metrics')}</h6>
- <FlexSpacer />
- <SearchBar
- placeholder={t('Search Metrics')}
- onChange={setQuery}
- query={query}
- size="sm"
- />
- <Button onClick={handleCreate} priority="primary" size="sm">
- {t('Add Metric')}
- </Button>
- </SearchWrapper>
- <RulesTable
- isLoading={isLoadingExtractionRules || isLoadingCardinality}
- onDelete={handleDelete}
- onEdit={handleEdit}
- extractionRules={extractionRules || []}
- cardinality={cardinality || {}}
- hasSearch={!!query}
- />
- </Fragment>
- );
- }
- interface RulesTableProps {
- cardinality: Record<string, number>;
- extractionRules: MetricsExtractionRule[];
- hasSearch: boolean;
- isLoading: boolean;
- onDelete: (rule: MetricsExtractionRule) => void;
- onEdit: (rule: MetricsExtractionRule) => void;
- }
- function RulesTable({
- extractionRules,
- cardinality,
- isLoading,
- onDelete,
- onEdit,
- hasSearch,
- }: RulesTableProps) {
- const {members} = useMembers();
- const isCardinalityLimited = (rule: MetricsExtractionRule): boolean => {
- const mris = rule.conditions.flatMap(condition => condition.mris);
- return mris.some(conditionMri => cardinality[conditionMri] > 0);
- };
- return (
- <ExtractionRulesPanelTable
- headers={[
- <Cell key="spanAttribute">
- <IconArrow size="xs" direction="down" />
- {t('Span attribute')}
- </Cell>,
- <Cell key="createdBy">{t('Created by')}</Cell>,
- <Cell right key="createdBy">
- {t('Created at')}
- </Cell>,
- <Cell right key="actions">
- {t('Actions')}
- </Cell>,
- ]}
- emptyMessage={
- hasSearch
- ? t('No metrics match the query.')
- : t('You have not created any span metrics yet.')
- }
- isEmpty={extractionRules.length === 0}
- isLoading={isLoading}
- >
- {extractionRules
- .toSorted((a, b) => a?.spanAttribute?.localeCompare(b?.spanAttribute))
- .map(rule => {
- const createdByUser = members.find(
- member => member.id === String(rule.createdById)
- );
- return (
- <Fragment key={rule.spanAttribute + rule.unit}>
- <Cell>
- {isCardinalityLimited(rule) ? (
- <Tooltip
- title={t(
- 'Some of your defined queries are exeeding the cardinality limit. Remove tags or add filters to receive accurate data.'
- )}
- containerDisplayMode="inline-flex"
- >
- <IconWarning
- size="xs"
- color="yellow300"
- role="img"
- aria-label={t('Exceeding the cardinality limit warning')}
- />
- </Tooltip>
- ) : null}
- {rule.spanAttribute}
- </Cell>
- <Cell>
- <UserBadge
- displayName={createdByUser?.name ?? t('Unknown')}
- user={createdByUser}
- hideEmail
- avatarSize={24}
- />
- </Cell>
- <Cell>
- <DateTime date={rule.dateAdded} />
- </Cell>
- <Cell right>
- <Button
- aria-label={t('Edit metric')}
- size="xs"
- icon={<IconEdit />}
- borderless
- onClick={() => onEdit(rule)}
- />
- <Button
- aria-label={t('Delete metric')}
- size="xs"
- icon={<IconDelete />}
- borderless
- onClick={() => onDelete(rule)}
- />
- </Cell>
- </Fragment>
- );
- })}
- </ExtractionRulesPanelTable>
- );
- }
- const SearchWrapper = styled('div')`
- display: flex;
- align-items: flex-start;
- margin-top: ${space(4)};
- margin-bottom: ${space(1)};
- gap: ${space(1)};
- & > h6 {
- margin: 0;
- }
- `;
- const FlexSpacer = styled('div')`
- flex: 1;
- `;
- const ExtractionRulesPanelTable = styled(PanelTable)`
- grid-template-columns: 1fr repeat(3, max-content);
- `;
- const Cell = styled('div')<{right?: boolean}>`
- display: flex;
- align-items: center;
- align-self: stretch;
- gap: ${space(0.5)};
- justify-content: ${p => (p.right ? 'flex-end' : 'flex-start')};
- `;
|