2025-04-01 10:38:02 +09:00

194 lines
6.4 KiB
TypeScript

import { css } from '@emotion/css';
import { useEffect, useMemo, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data';
import { AccessoryButton } from '@grafana/plugin-ui';
import { Alert, HorizontalGroup, InputActionMeta, Select, useStyles2 } from '@grafana/ui';
import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen';
import { TempoDatasource } from '../datasource';
import { OPTIONS_LIMIT } from '../language_provider';
import { TempoQuery } from '../types';
import InlineSearchField from './InlineSearchField';
import { withTemplateVariableOptions } from './SearchField';
import { replaceAt } from './utils';
interface Props {
datasource: TempoDatasource;
onChange: (value: TempoQuery) => void;
query: Partial<TempoQuery> & TempoQuery;
isTagsLoading: boolean;
addVariablesToOptions?: boolean;
}
export const GroupByField = (props: Props) => {
const { datasource, onChange, query, isTagsLoading, addVariablesToOptions } = props;
const styles = useStyles2(getStyles);
const generateId = () => uuidv4().slice(0, 8);
const [tagQuery, setTagQuery] = useState<string>('');
useEffect(() => {
if (!query.groupBy || query.groupBy.length === 0) {
onChange({
...query,
groupBy: [
{
id: generateId(),
scope: TraceqlSearchScope.Span,
},
],
});
}
}, [onChange, query]);
const tagOptions = useMemo(
() => (f: TraceqlFilter) => {
const tags = datasource!.languageProvider.getMetricsSummaryTags(f.scope);
if (tagQuery.length === 0) {
return tags.slice(0, OPTIONS_LIMIT);
}
const queryLowerCase = tagQuery.toLowerCase();
return tags.filter((tag) => tag.toLowerCase().includes(queryLowerCase)).slice(0, OPTIONS_LIMIT);
},
[datasource, tagQuery]
);
const addFilter = () => {
updateFilter({
id: generateId(),
scope: TraceqlSearchScope.Span,
});
};
const removeFilter = (filter: TraceqlFilter) => {
onChange({ ...query, groupBy: query.groupBy?.filter((f) => f.id !== filter.id) });
};
const updateFilter = (filter: TraceqlFilter) => {
const copy = { ...query };
copy.groupBy ||= [];
const indexOfFilter = copy.groupBy.findIndex((f) => f.id === filter.id);
if (indexOfFilter >= 0) {
copy.groupBy = replaceAt(copy.groupBy, indexOfFilter, filter);
} else {
copy.groupBy.push(filter);
}
onChange(copy);
};
const scopeOptions = Object.values(TraceqlSearchScope)
.filter((s) => {
// only add scope if it has tags
return datasource.languageProvider.getTags(s).length > 0;
})
.map((t) => ({ label: t, value: t }));
return (
<InlineSearchField
label="Aggregate by"
tooltip={`Note: We recommend using Explore Traces instead. Select one or more tags to see the metrics summary.`}
>
<>
{query.groupBy?.map((f, i) => {
const tags = tagOptions(f)
?.concat(f.tag !== undefined && f.tag !== '' && !tagOptions(f)?.includes(f.tag) ? [f.tag] : [])
.map((t) => ({
label: t,
value: t,
}));
return (
<div key={f.id}>
<HorizontalGroup spacing={'none'} width={'auto'}>
<Select
aria-label={`Select scope for filter ${i + 1}`}
onChange={(v) => {
updateFilter({ ...f, scope: v?.value, tag: '' });
}}
options={scopeOptions}
placeholder="Select scope"
value={f.scope}
/>
<Select
aria-label={`Select tag for filter ${i + 1}`}
isClearable
allowCustomValue
isLoading={isTagsLoading}
key={f.tag}
onChange={(v) => {
updateFilter({ ...f, tag: v?.value });
}}
options={addVariablesToOptions ? withTemplateVariableOptions(tags) : tags}
onInputChange={(value: string, { action }: InputActionMeta) => {
if (action === 'input-change') {
setTagQuery(value);
}
}}
onCloseMenu={() => setTagQuery('')}
placeholder="Select tag"
value={f.tag || ''}
/>
{(f.tag || (query.groupBy?.length ?? 0) > 1) && (
<AccessoryButton
aria-label={`Remove tag for filter ${i + 1}`}
icon="times"
onClick={() => removeFilter(f)}
tooltip="Remove tag"
title={`Remove tag for filter ${i + 1}`}
variant="secondary"
/>
)}
{f.tag && i === (query.groupBy?.length ?? 0) - 1 && (
<span className={styles.addTag}>
<AccessoryButton
aria-label="Add tag"
icon="plus"
onClick={() => addFilter()}
tooltip="Add tag"
variant="secondary"
/>
</span>
)}
</HorizontalGroup>
</div>
);
})}
{query.groupBy && query.groupBy.length > 0 && query.groupBy[0].tag && (
<Alert title="" severity="warning" className={styles.notice}>
The aggregate by feature is deprecated. We recommend using Explore Traces instead. If you want to write your
own TraceQL queries to replicate this API, please check
<a
href={
'https://grafana.com/docs/tempo/latest/api_docs/metrics-summary/#deprecation-in-favor-of-traceql-metrics'
}
className={styles.noticeLink}
target="_blank"
rel="noreferrer noopener"
>
this page
</a>
.
</Alert>
)}
</>
</InlineSearchField>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
addTag: css({
marginLeft: theme.spacing(1),
}),
notice: css({
width: '500px',
marginTop: theme.spacing(0.75),
}),
noticeLink: css({
color: theme.colors.text.link,
textDecoration: 'underline',
marginLeft: '5px',
}),
});