This is an automated email from the ASF dual-hosted git repository.
zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/2.0.0 by this push:
new 9d2aeddbb7 Compact metrics attribute operator actions
9d2aeddbb7 is described below
commit 9d2aeddbb75f1b7ac89aba24c4dcaf3e4cc88910
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 23:38:22 2026 +0800
Compact metrics attribute operator actions
---
.../ingestion/otlp/metrics/otlp-metrics-page.tsx | 315 +++++++--------------
web-next/app/ingestion/otlp/metrics/page.test.tsx | 185 ++++--------
web-next/lib/i18n-runtime-messages.ts | 26 ++
3 files changed, 187 insertions(+), 339 deletions(-)
diff --git a/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
b/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
index 6f6f7e8f4f..74aca8e8a2 100644
--- a/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
+++ b/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
@@ -62,6 +62,7 @@ type OtlpMetricsTranslate = ReturnType<typeof useI18n>['t'];
type MetricsSavedQueryView = SignalSavedQueryView;
type MetricsDashboardPanelDraftState = 'idle' | 'saving' | 'saved' | 'failed';
+type MetricAttributeOperator = 'filter' | 'contains' | 'not-contains' | 'in' |
'not-in' | 'exclude' | 'exists' | 'not-exists' | 'replace' | 'group';
const METRICS_SAVED_QUERY_VIEW_STORAGE_KEY =
'hertzbeat.otlp-metrics.saved-query-views';
const METRICS_SAVED_QUERY_VIEW_LIMIT = 5;
@@ -73,6 +74,43 @@ const DEFAULT_METRIC_INVENTORY_PAGE_SIZE = '10';
const DEFAULT_METRIC_INVENTORY_PAGE_INDEX = '0';
const METRIC_INVENTORY_PAGE_SIZE_OPTIONS = ['5', '10', '20', '50'] as const;
const METRICS_EXPORT_SCOPES: OtlpMetricsExportScope[] = ['all', 'selected'];
+const METRIC_ATTRIBUTE_OPERATORS: MetricAttributeOperator[] = ['filter',
'contains', 'not-contains', 'in', 'not-in', 'exclude', 'exists', 'not-exists',
'replace', 'group'];
+
+function buildMetricAttributeOperatorOptions(t: OtlpMetricsTranslate) {
+ return METRIC_ATTRIBUTE_OPERATORS.map(operator => ({
+ value: operator,
+ label: t(`otlp.metrics.attributes.operator.${operator}` as const)
+ }));
+}
+
+function metricAttributeOperatorDataAttributes(operator:
MetricAttributeOperator, name: string) {
+ const base = {
+ 'data-otlp-metrics-attribute-operator-option': operator,
+ 'data-otlp-metrics-attribute-operator-name': name
+ };
+ switch (operator) {
+ case 'filter':
+ return { ...base, 'data-otlp-metrics-attribute-filter-action': name,
'data-otlp-metrics-attribute-filter-action-owner':
'hertzbeat-ui-select-menu-option' };
+ case 'contains':
+ return { ...base, 'data-otlp-metrics-attribute-contains-action': name,
'data-otlp-metrics-attribute-contains-action-owner':
'hertzbeat-ui-select-menu-option' };
+ case 'not-contains':
+ return { ...base, 'data-otlp-metrics-attribute-not-contains-action':
name, 'data-otlp-metrics-attribute-not-contains-action-owner':
'hertzbeat-ui-select-menu-option' };
+ case 'in':
+ return { ...base, 'data-otlp-metrics-attribute-in-action': name,
'data-otlp-metrics-attribute-in-action-owner':
'hertzbeat-ui-select-menu-option' };
+ case 'not-in':
+ return { ...base, 'data-otlp-metrics-attribute-not-in-action': name,
'data-otlp-metrics-attribute-not-in-action-owner':
'hertzbeat-ui-select-menu-option' };
+ case 'exclude':
+ return { ...base, 'data-otlp-metrics-attribute-filter-out-action': name,
'data-otlp-metrics-attribute-filter-out-action-owner':
'hertzbeat-ui-select-menu-option' };
+ case 'exists':
+ return { ...base, 'data-otlp-metrics-attribute-exists-action': name,
'data-otlp-metrics-attribute-exists-action-owner':
'hertzbeat-ui-select-menu-option' };
+ case 'not-exists':
+ return { ...base, 'data-otlp-metrics-attribute-not-exists-action': name,
'data-otlp-metrics-attribute-not-exists-action-owner':
'hertzbeat-ui-select-menu-option' };
+ case 'replace':
+ return { ...base, 'data-otlp-metrics-attribute-replace-action': name,
'data-otlp-metrics-attribute-replace-action-owner':
'hertzbeat-ui-select-menu-option' };
+ case 'group':
+ return { ...base, 'data-otlp-metrics-attribute-group-action': name,
'data-otlp-metrics-attribute-group-action-owner':
'hertzbeat-ui-select-menu-option' };
+ }
+}
function resolveMetricInventoryPageSize(value?: string) {
return METRIC_INVENTORY_PAGE_SIZE_OPTIONS.find(option => option === value)
|| DEFAULT_METRIC_INVENTORY_PAGE_SIZE;
@@ -969,6 +1007,52 @@ export default function OtlpMetricsPage() {
replaceMetricsRoute(nextDraft, undefined, series?.key || query.series,
series ? buildMetricSeriesRouteContext(series) : {});
}, [draft, query.series, replaceMetricsRoute]);
+ const applyMetricAttributeOperator = useCallback((operator:
MetricAttributeOperator, name: string, value: string, series?:
OtlpMetricSeriesView | null) => {
+ switch (operator) {
+ case 'filter':
+ applyMetricAttributeFilter(name, value, series);
+ break;
+ case 'contains':
+ applyMetricAttributeContainsFilter(name, value, series);
+ break;
+ case 'not-contains':
+ applyMetricAttributeNotContainsFilter(name, value, series);
+ break;
+ case 'in':
+ applyMetricAttributeInFilter(name, value, series);
+ break;
+ case 'not-in':
+ applyMetricAttributeNotInFilter(name, value, series);
+ break;
+ case 'exclude':
+ applyMetricAttributeExcludeFilter(name, value, series);
+ break;
+ case 'exists':
+ applyMetricAttributeExistsFilter(name, series);
+ break;
+ case 'not-exists':
+ applyMetricAttributeNotExistsFilter(name, series);
+ break;
+ case 'replace':
+ applyMetricAttributeReplaceFilter(name, value, series);
+ break;
+ case 'group':
+ applyMetricAttributeGroupBy(name, series);
+ break;
+ }
+ }, [
+ applyMetricAttributeContainsFilter,
+ applyMetricAttributeExcludeFilter,
+ applyMetricAttributeExistsFilter,
+ applyMetricAttributeFilter,
+ applyMetricAttributeGroupBy,
+ applyMetricAttributeInFilter,
+ applyMetricAttributeNotContainsFilter,
+ applyMetricAttributeNotExistsFilter,
+ applyMetricAttributeNotInFilter,
+ applyMetricAttributeReplaceFilter
+ ]);
+
const applyMetricsTimeContext = useCallback((timeContext: TimeContext) => {
const sanitized = sanitizeTimeContext(timeContext);
setDraft(previous => ({
@@ -1039,6 +1123,7 @@ export default function OtlpMetricsPage() {
const selectedSeriesEvidenceRows =
buildMetricSeriesEvidenceRows(selectedMetricSeries, formatTime, t);
const selectedSeriesSampleRows =
buildMetricSeriesSampleRows(selectedMetricSeries, formatTime, t);
const selectedSeriesAttributeRows =
buildMetricSeriesAttributeRows(selectedMetricSeries, metricAttributeSearch);
+ const metricAttributeOperatorOptions =
buildMetricAttributeOperatorOptions(t);
const selectedSeriesContextDetailRows =
selectedSeriesContextRows.map(row => ({
key: row.label,
title: row.label,
@@ -2688,223 +2773,21 @@ export default function OtlpMetricsPage() {
render: row => <HzDataCellText variant="meta"
display="block" casing="plain">{row.value}</HzDataCellText>
},
{
- key: 'filter',
- header: t('otlp.metrics.attributes.column.filter'),
- render: row => (
- <HzButton
- type="button"
- size="xs"
- intent="ghost"
-
data-otlp-metrics-attribute-filter-action={row.name}
-
data-otlp-metrics-attribute-filter-action-owner="hertzbeat-ui-button"
-
aria-label={t('otlp.metrics.attributes.filter-action.aria', { name: row.name,
value: row.value })}
- onClick={() =>
applyMetricAttributeFilter(row.name, row.value, selectedMetricSeries)}
- >
- <HzButtonIcon
- icon={Filter}
-
data-otlp-metrics-attribute-filter-action-icon={row.name}
-
data-otlp-metrics-attribute-filter-action-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('otlp.metrics.attributes.filter-action')}
- </HzButton>
- )
- },
- {
- key: 'contains',
- header:
t('otlp.metrics.attributes.column.contains'),
+ key: 'operator',
+ header:
t('otlp.metrics.attributes.column.operator'),
render: row => (
- <HzButton
- type="button"
- size="xs"
- intent="ghost"
-
data-otlp-metrics-attribute-contains-action={row.name}
-
data-otlp-metrics-attribute-contains-action-owner="hertzbeat-ui-button"
-
aria-label={t('otlp.metrics.attributes.contains-action.aria', { name: row.name,
value: row.value })}
- onClick={() =>
applyMetricAttributeContainsFilter(row.name, row.value, selectedMetricSeries)}
- >
- <HzButtonIcon
- icon={Search}
-
data-otlp-metrics-attribute-contains-action-icon={row.name}
-
data-otlp-metrics-attribute-contains-action-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('otlp.metrics.attributes.contains-action')}
- </HzButton>
- )
- },
- {
- key: 'not-contains',
- header:
t('otlp.metrics.attributes.column.not-contains'),
- render: row => (
- <HzButton
- type="button"
- size="xs"
- intent="ghost"
-
data-otlp-metrics-attribute-not-contains-action={row.name}
-
data-otlp-metrics-attribute-not-contains-action-owner="hertzbeat-ui-button"
-
aria-label={t('otlp.metrics.attributes.not-contains-action.aria', { name:
row.name, value: row.value })}
- onClick={() =>
applyMetricAttributeNotContainsFilter(row.name, row.value,
selectedMetricSeries)}
- >
- <HzButtonIcon
- icon={Ban}
-
data-otlp-metrics-attribute-not-contains-action-icon={row.name}
-
data-otlp-metrics-attribute-not-contains-action-icon-owner="hertzbeat-ui-button-icon"
- />
-
{t('otlp.metrics.attributes.not-contains-action')}
- </HzButton>
- )
- },
- {
- key: 'in',
- header: t('otlp.metrics.attributes.column.in'),
- render: row => (
- <HzButton
- type="button"
- size="xs"
- intent="ghost"
-
data-otlp-metrics-attribute-in-action={row.name}
-
data-otlp-metrics-attribute-in-action-owner="hertzbeat-ui-button"
-
aria-label={t('otlp.metrics.attributes.in-action.aria', { name: row.name,
value: row.value })}
- onClick={() =>
applyMetricAttributeInFilter(row.name, row.value, selectedMetricSeries)}
- >
- <HzButtonIcon
- icon={ListChecks}
-
data-otlp-metrics-attribute-in-action-icon={row.name}
-
data-otlp-metrics-attribute-in-action-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('otlp.metrics.attributes.in-action')}
- </HzButton>
- )
- },
- {
- key: 'not-in',
- header: t('otlp.metrics.attributes.column.not-in'),
- render: row => (
- <HzButton
- type="button"
- size="xs"
- intent="ghost"
-
data-otlp-metrics-attribute-not-in-action={row.name}
-
data-otlp-metrics-attribute-not-in-action-owner="hertzbeat-ui-button"
-
aria-label={t('otlp.metrics.attributes.not-in-action.aria', { name: row.name,
value: row.value })}
- onClick={() =>
applyMetricAttributeNotInFilter(row.name, row.value, selectedMetricSeries)}
- >
- <HzButtonIcon
- icon={Ban}
-
data-otlp-metrics-attribute-not-in-action-icon={row.name}
-
data-otlp-metrics-attribute-not-in-action-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('otlp.metrics.attributes.not-in-action')}
- </HzButton>
- )
- },
- {
- key: 'exclude',
- header:
t('otlp.metrics.attributes.column.exclude'),
- render: row => (
- <HzButton
- type="button"
- size="xs"
- intent="ghost"
-
data-otlp-metrics-attribute-filter-out-action={row.name}
-
data-otlp-metrics-attribute-filter-out-action-owner="hertzbeat-ui-button"
-
aria-label={t('otlp.metrics.attributes.filter-out-action.aria', { name:
row.name, value: row.value })}
- onClick={() =>
applyMetricAttributeExcludeFilter(row.name, row.value, selectedMetricSeries)}
- >
- <HzButtonIcon
- icon={X}
-
data-otlp-metrics-attribute-filter-out-action-icon={row.name}
-
data-otlp-metrics-attribute-filter-out-action-icon-owner="hertzbeat-ui-button-icon"
- />
-
{t('otlp.metrics.attributes.filter-out-action')}
- </HzButton>
- )
- },
- {
- key: 'exists',
- header: t('otlp.metrics.attributes.column.exists'),
- render: row => (
- <HzButton
- type="button"
- size="xs"
- intent="ghost"
-
data-otlp-metrics-attribute-exists-action={row.name}
-
data-otlp-metrics-attribute-exists-action-owner="hertzbeat-ui-button"
-
aria-label={t('otlp.metrics.attributes.exists-action.aria', { name: row.name })}
- onClick={() =>
applyMetricAttributeExistsFilter(row.name, selectedMetricSeries)}
- >
- <HzButtonIcon
- icon={Check}
-
data-otlp-metrics-attribute-exists-action-icon={row.name}
-
data-otlp-metrics-attribute-exists-action-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('otlp.metrics.attributes.exists-action')}
- </HzButton>
- )
- },
- {
- key: 'not-exists',
- header:
t('otlp.metrics.attributes.column.not-exists'),
- render: row => (
- <HzButton
- type="button"
- size="xs"
- intent="ghost"
-
data-otlp-metrics-attribute-not-exists-action={row.name}
-
data-otlp-metrics-attribute-not-exists-action-owner="hertzbeat-ui-button"
-
aria-label={t('otlp.metrics.attributes.not-exists-action.aria', { name:
row.name })}
- onClick={() =>
applyMetricAttributeNotExistsFilter(row.name, selectedMetricSeries)}
- >
- <HzButtonIcon
- icon={Ban}
-
data-otlp-metrics-attribute-not-exists-action-icon={row.name}
-
data-otlp-metrics-attribute-not-exists-action-icon-owner="hertzbeat-ui-button-icon"
- />
-
{t('otlp.metrics.attributes.not-exists-action')}
- </HzButton>
- )
- },
- {
- key: 'replace',
- header:
t('otlp.metrics.attributes.column.replace'),
- render: row => (
- <HzButton
- type="button"
- size="xs"
- intent="ghost"
-
data-otlp-metrics-attribute-replace-action={row.name}
-
data-otlp-metrics-attribute-replace-action-owner="hertzbeat-ui-button"
-
aria-label={t('otlp.metrics.attributes.replace-action.aria', { name: row.name,
value: row.value })}
- onClick={() =>
applyMetricAttributeReplaceFilter(row.name, row.value, selectedMetricSeries)}
- >
- <HzButtonIcon
- icon={Replace}
-
data-otlp-metrics-attribute-replace-action-icon={row.name}
-
data-otlp-metrics-attribute-replace-action-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('otlp.metrics.attributes.replace-action')}
- </HzButton>
- )
- },
- {
- key: 'group',
- header: t('otlp.metrics.attributes.column.group'),
- render: row => (
- <HzButton
- type="button"
- size="xs"
- intent="ghost"
-
data-otlp-metrics-attribute-group-action={row.name}
-
data-otlp-metrics-attribute-group-action-owner="hertzbeat-ui-button"
-
aria-label={t('otlp.metrics.attributes.group-action.aria', { name: row.name })}
- onClick={() =>
applyMetricAttributeGroupBy(row.name, selectedMetricSeries)}
- >
- <HzButtonIcon
- icon={Workflow}
-
data-otlp-metrics-attribute-group-action-icon={row.name}
-
data-otlp-metrics-attribute-group-action-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('otlp.metrics.attributes.group-action')}
- </HzButton>
+ <HzSelect
+
data-otlp-metrics-attribute-operator-action={row.name}
+
data-otlp-metrics-attribute-operator-action-owner="hertzbeat-ui-select"
+ width="metrics-group-by"
+ size="sm"
+ value=""
+
placeholder={t('otlp.metrics.attributes.operator.placeholder')}
+
aria-label={t('otlp.metrics.attributes.operator-action.aria', { name: row.name,
value: row.value })}
+ options={metricAttributeOperatorOptions}
+ optionDataAttributes={option =>
metricAttributeOperatorDataAttributes(option.value as MetricAttributeOperator,
row.name)}
+ onChange={event =>
applyMetricAttributeOperator(event.target.value as MetricAttributeOperator,
row.name, row.value, selectedMetricSeries)}
+ />
)
}
]}
diff --git a/web-next/app/ingestion/otlp/metrics/page.test.tsx
b/web-next/app/ingestion/otlp/metrics/page.test.tsx
index 7364946b7e..a60c5a5c7b 100644
--- a/web-next/app/ingestion/otlp/metrics/page.test.tsx
+++ b/web-next/app/ingestion/otlp/metrics/page.test.tsx
@@ -1037,13 +1037,7 @@ describe('otlp metrics page', () => {
expect(source).toContain('data-otlp-metrics-attribute-table-owner="hertzbeat-ui-data-table"');
expect(source).toContain("header:
t('otlp.metrics.attributes.column.name')");
expect(source).toContain("header:
t('otlp.metrics.attributes.column.value')");
- expect(source).toContain("header:
t('otlp.metrics.attributes.column.contains')");
- expect(source).toContain("header:
t('otlp.metrics.attributes.column.not-contains')");
- expect(source).toContain("header: t('otlp.metrics.attributes.column.in')");
- expect(source).toContain("header:
t('otlp.metrics.attributes.column.not-in')");
- expect(source).toContain("header:
t('otlp.metrics.attributes.column.exclude')");
- expect(source).toContain("header:
t('otlp.metrics.attributes.column.exists')");
- expect(source).toContain("header:
t('otlp.metrics.attributes.column.not-exists')");
+ expect(source).toContain("header:
t('otlp.metrics.attributes.column.operator')");
expect(source).toContain('buildMetricAttributeExcludeFilterExpression');
expect(source).toContain('buildMetricAttributeContainsFilterExpression');
expect(source).toContain('buildMetricAttributeNotContainsFilterExpression');
@@ -1058,20 +1052,15 @@ describe('otlp metrics page', () => {
expect(source).toContain('applyMetricAttributeNotInFilter');
expect(source).toContain('applyMetricAttributeExistsFilter');
expect(source).toContain('applyMetricAttributeNotExistsFilter');
-
expect(source).toContain('data-otlp-metrics-attribute-filter-out-action={row.name}');
-
expect(source).toContain('data-otlp-metrics-attribute-filter-out-action-owner="hertzbeat-ui-button"');
-
expect(source).toContain('data-otlp-metrics-attribute-contains-action={row.name}');
-
expect(source).toContain('data-otlp-metrics-attribute-contains-action-owner="hertzbeat-ui-button"');
-
expect(source).toContain('data-otlp-metrics-attribute-not-contains-action={row.name}');
-
expect(source).toContain('data-otlp-metrics-attribute-not-contains-action-owner="hertzbeat-ui-button"');
-
expect(source).toContain('data-otlp-metrics-attribute-in-action={row.name}');
-
expect(source).toContain('data-otlp-metrics-attribute-in-action-owner="hertzbeat-ui-button"');
-
expect(source).toContain('data-otlp-metrics-attribute-not-in-action={row.name}');
-
expect(source).toContain('data-otlp-metrics-attribute-not-in-action-owner="hertzbeat-ui-button"');
-
expect(source).toContain('data-otlp-metrics-attribute-exists-action={row.name}');
-
expect(source).toContain('data-otlp-metrics-attribute-exists-action-owner="hertzbeat-ui-button"');
-
expect(source).toContain('data-otlp-metrics-attribute-not-exists-action={row.name}');
-
expect(source).toContain('data-otlp-metrics-attribute-not-exists-action-owner="hertzbeat-ui-button"');
+ expect(source).toContain('buildMetricAttributeOperatorOptions');
+ expect(source).toContain('metricAttributeOperatorDataAttributes');
+ expect(source).toContain('applyMetricAttributeOperator');
+
expect(source).toContain('data-otlp-metrics-attribute-operator-action={row.name}');
+
expect(source).toContain('data-otlp-metrics-attribute-operator-action-owner="hertzbeat-ui-select"');
+ expect(source).toContain('optionDataAttributes={option =>
metricAttributeOperatorDataAttributes(option.value as MetricAttributeOperator,
row.name)}');
+
expect(source).not.toContain('data-otlp-metrics-attribute-filter-out-action-owner="hertzbeat-ui-button"');
+
expect(source).not.toContain('data-otlp-metrics-attribute-contains-action-owner="hertzbeat-ui-button"');
+
expect(source).not.toContain('data-otlp-metrics-attribute-group-action-owner="hertzbeat-ui-button"');
expect(source).toContain('boundary="top"');
expect(source).not.toContain('className="mt-3 border-t border-[#252b35]
pt-3"');
expect(source).toContain('data-otlp-metrics-linked-record-summary="log-trace-alert-links"');
@@ -3031,19 +3020,38 @@ describe('otlp metrics page', () => {
expect(attributeTable?.textContent).toContain('checkout');
expect(attributeTable?.textContent).toContain('route');
- const serviceFilterAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-filter-action="service.name"]')
as HTMLButtonElement | null;
-
expect(serviceFilterAction?.getAttribute('data-otlp-metrics-attribute-filter-action-owner')).toBe('hertzbeat-ui-button');
-
expect(serviceFilterAction?.getAttribute('aria-label')).toContain('service.name');
-
- await act(async () => {
- serviceFilterAction?.dispatchEvent(new MouseEvent('click', { bubbles:
true }));
- await Promise.resolve();
- });
+ const serviceOperator =
interactionContainer.querySelector('[data-otlp-metrics-attribute-operator-action="service.name"]')
as HTMLElement | null;
+ expect(serviceOperator).not.toBeNull();
+
expect(serviceOperator?.getAttribute('data-otlp-metrics-attribute-operator-action-owner')).toBe('hertzbeat-ui-select');
+ expect(serviceOperator?.getAttribute('data-hz-ui')).toBe('select');
+ expect((serviceOperator?.querySelector('[data-hz-ui="select-trigger"]') as
HTMLButtonElement |
null)?.getAttribute('aria-label')).toContain('service.name');
+
+ const applyAttributeOperator = async (name: string, operator: string) => {
+ const operatorRoot =
interactionContainer.querySelector(`[data-otlp-metrics-attribute-operator-action="${name}"]`)
as HTMLElement | null;
+ expect(operatorRoot).not.toBeNull();
+ await act(async () => {
+ (operatorRoot?.querySelector('[data-hz-ui="select-trigger"]') as
HTMLButtonElement | null)?.click();
+ await Promise.resolve();
+ });
+ const option = operatorRoot?.querySelector(
+
`[data-otlp-metrics-attribute-operator-option="${operator}"][data-otlp-metrics-attribute-operator-name="${name}"]`
+ ) as HTMLButtonElement | null;
+ expect(option).not.toBeNull();
+ expect(option?.getAttribute(`data-otlp-metrics-attribute-${operator ===
'exclude' ? 'filter-out' :
operator}-action-owner`)).toBe('hertzbeat-ui-select-menu-option');
+ await act(async () => {
+ option?.click();
+ await Promise.resolve();
+ });
+ const href = String(mockState.replace.mock.calls.at(-1)?.[0]);
+ return {
+ href,
+ params: new URL(href, 'http://localhost').searchParams
+ };
+ };
- const filterHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
+ const { href: filterHref, params: filterParams } = await
applyAttributeOperator('service.name', 'filter');
expect(filterHref).toContain('/ingestion/otlp/metrics?');
expect(filterHref).toContain('filter=service.name%3D%22checkout%22');
- const filterParams = new URL(filterHref, 'http://localhost').searchParams;
expect(filterParams.get('series')).toBe('checkout_latency-0');
expect(filterParams.get('entityId')).toBe('7');
expect(filterParams.get('serviceName')).toBe('checkout');
@@ -3051,17 +3059,7 @@ describe('otlp metrics page', () => {
expect(filterParams.get('traceId')).toBe('trace-checkout');
expect(filterParams.get('spanId')).toBe('span-checkout');
- const serviceContainsAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-contains-action="service.name"]')
as HTMLButtonElement | null;
-
expect(serviceContainsAction?.getAttribute('data-otlp-metrics-attribute-contains-action-owner')).toBe('hertzbeat-ui-button');
-
expect(serviceContainsAction?.getAttribute('aria-label')).toContain('service.name');
-
- await act(async () => {
- serviceContainsAction?.dispatchEvent(new MouseEvent('click', { bubbles:
true }));
- await Promise.resolve();
- });
-
- const containsHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
- const containsParams = new URL(containsHref,
'http://localhost').searchParams;
+ const { params: containsParams } = await
applyAttributeOperator('service.name', 'contains');
expect(containsParams.get('filter')).toContain('service.name CONTAINS
checkout');
expect(containsParams.get('series')).toBe('checkout_latency-0');
expect(containsParams.get('entityId')).toBe('7');
@@ -3070,17 +3068,7 @@ describe('otlp metrics page', () => {
expect(containsParams.get('traceId')).toBe('trace-checkout');
expect(containsParams.get('spanId')).toBe('span-checkout');
- const serviceNotContainsAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-not-contains-action="service.name"]')
as HTMLButtonElement | null;
-
expect(serviceNotContainsAction?.getAttribute('data-otlp-metrics-attribute-not-contains-action-owner')).toBe('hertzbeat-ui-button');
-
expect(serviceNotContainsAction?.getAttribute('aria-label')).toContain('service.name');
-
- await act(async () => {
- serviceNotContainsAction?.dispatchEvent(new MouseEvent('click', {
bubbles: true }));
- await Promise.resolve();
- });
-
- const notContainsHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
- const notContainsParams = new URL(notContainsHref,
'http://localhost').searchParams;
+ const { params: notContainsParams } = await
applyAttributeOperator('service.name', 'not-contains');
expect(notContainsParams.get('filter')).toContain('service.name NOT
CONTAINS checkout');
expect(notContainsParams.get('series')).toBe('checkout_latency-0');
expect(notContainsParams.get('entityId')).toBe('7');
@@ -3089,17 +3077,7 @@ describe('otlp metrics page', () => {
expect(notContainsParams.get('traceId')).toBe('trace-checkout');
expect(notContainsParams.get('spanId')).toBe('span-checkout');
- const serviceInAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-in-action="service.name"]')
as HTMLButtonElement | null;
-
expect(serviceInAction?.getAttribute('data-otlp-metrics-attribute-in-action-owner')).toBe('hertzbeat-ui-button');
-
expect(serviceInAction?.getAttribute('aria-label')).toContain('service.name');
-
- await act(async () => {
- serviceInAction?.dispatchEvent(new MouseEvent('click', { bubbles: true
}));
- await Promise.resolve();
- });
-
- const inHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
- const inParams = new URL(inHref, 'http://localhost').searchParams;
+ const { params: inParams } = await applyAttributeOperator('service.name',
'in');
expect(inParams.get('filter')).toContain('service.name IN ("checkout")');
expect(inParams.get('series')).toBe('checkout_latency-0');
expect(inParams.get('entityId')).toBe('7');
@@ -3108,17 +3086,7 @@ describe('otlp metrics page', () => {
expect(inParams.get('traceId')).toBe('trace-checkout');
expect(inParams.get('spanId')).toBe('span-checkout');
- const serviceNotInAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-not-in-action="service.name"]')
as HTMLButtonElement | null;
-
expect(serviceNotInAction?.getAttribute('data-otlp-metrics-attribute-not-in-action-owner')).toBe('hertzbeat-ui-button');
-
expect(serviceNotInAction?.getAttribute('aria-label')).toContain('service.name');
-
- await act(async () => {
- serviceNotInAction?.dispatchEvent(new MouseEvent('click', { bubbles:
true }));
- await Promise.resolve();
- });
-
- const notInHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
- const notInParams = new URL(notInHref, 'http://localhost').searchParams;
+ const { params: notInParams } = await
applyAttributeOperator('service.name', 'not-in');
expect(notInParams.get('filter')).toContain('service.name NOT IN
("checkout")');
expect(notInParams.get('series')).toBe('checkout_latency-0');
expect(notInParams.get('entityId')).toBe('7');
@@ -3127,17 +3095,7 @@ describe('otlp metrics page', () => {
expect(notInParams.get('traceId')).toBe('trace-checkout');
expect(notInParams.get('spanId')).toBe('span-checkout');
- const serviceExcludeAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-filter-out-action="service.name"]')
as HTMLButtonElement | null;
-
expect(serviceExcludeAction?.getAttribute('data-otlp-metrics-attribute-filter-out-action-owner')).toBe('hertzbeat-ui-button');
-
expect(serviceExcludeAction?.getAttribute('aria-label')).toContain('service.name');
-
- await act(async () => {
- serviceExcludeAction?.dispatchEvent(new MouseEvent('click', { bubbles:
true }));
- await Promise.resolve();
- });
-
- const excludeHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
- const excludeParams = new URL(excludeHref,
'http://localhost').searchParams;
+ const { params: excludeParams } = await
applyAttributeOperator('service.name', 'exclude');
expect(excludeParams.get('filter')).toContain('service.name!="checkout"');
expect(excludeParams.get('series')).toBe('checkout_latency-0');
expect(excludeParams.get('entityId')).toBe('7');
@@ -3146,17 +3104,7 @@ describe('otlp metrics page', () => {
expect(excludeParams.get('traceId')).toBe('trace-checkout');
expect(excludeParams.get('spanId')).toBe('span-checkout');
- const serviceExistsAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-exists-action="service.name"]')
as HTMLButtonElement | null;
-
expect(serviceExistsAction?.getAttribute('data-otlp-metrics-attribute-exists-action-owner')).toBe('hertzbeat-ui-button');
-
expect(serviceExistsAction?.getAttribute('aria-label')).toContain('service.name');
-
- await act(async () => {
- serviceExistsAction?.dispatchEvent(new MouseEvent('click', { bubbles:
true }));
- await Promise.resolve();
- });
-
- const existsHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
- const existsParams = new URL(existsHref, 'http://localhost').searchParams;
+ const { params: existsParams } = await
applyAttributeOperator('service.name', 'exists');
expect(existsParams.get('filter')).toContain('service.name EXISTS');
expect(existsParams.get('series')).toBe('checkout_latency-0');
expect(existsParams.get('entityId')).toBe('7');
@@ -3165,17 +3113,7 @@ describe('otlp metrics page', () => {
expect(existsParams.get('traceId')).toBe('trace-checkout');
expect(existsParams.get('spanId')).toBe('span-checkout');
- const serviceNotExistsAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-not-exists-action="service.name"]')
as HTMLButtonElement | null;
-
expect(serviceNotExistsAction?.getAttribute('data-otlp-metrics-attribute-not-exists-action-owner')).toBe('hertzbeat-ui-button');
-
expect(serviceNotExistsAction?.getAttribute('aria-label')).toContain('service.name');
-
- await act(async () => {
- serviceNotExistsAction?.dispatchEvent(new MouseEvent('click', { bubbles:
true }));
- await Promise.resolve();
- });
-
- const notExistsHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
- const notExistsParams = new URL(notExistsHref,
'http://localhost').searchParams;
+ const { params: notExistsParams } = await
applyAttributeOperator('service.name', 'not-exists');
expect(notExistsParams.get('filter')).toContain('service.name NOT EXISTS');
expect(notExistsParams.get('series')).toBe('checkout_latency-0');
expect(notExistsParams.get('entityId')).toBe('7');
@@ -3184,19 +3122,9 @@ describe('otlp metrics page', () => {
expect(notExistsParams.get('traceId')).toBe('trace-checkout');
expect(notExistsParams.get('spanId')).toBe('span-checkout');
- const routeGroupAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-group-action="route"]')
as HTMLButtonElement | null;
-
expect(routeGroupAction?.getAttribute('data-otlp-metrics-attribute-group-action-owner')).toBe('hertzbeat-ui-button');
- expect(routeGroupAction?.getAttribute('aria-label')).toContain('route');
-
- await act(async () => {
- routeGroupAction?.dispatchEvent(new MouseEvent('click', { bubbles: true
}));
- await Promise.resolve();
- });
-
- const groupHref = String(mockState.replace.mock.calls.at(-1)?.[0]);
+ const { href: groupHref, params: groupParams } = await
applyAttributeOperator('route', 'group');
expect(groupHref).toContain('filter=service.name%3D%22checkout%22');
expect(groupHref).toContain('groupBy=route');
- const groupParams = new URL(groupHref, 'http://localhost').searchParams;
expect(groupParams.get('series')).toBe('checkout_latency-0');
expect(groupParams.get('traceId')).toBe('trace-checkout');
expect(groupParams.get('spanId')).toBe('span-checkout');
@@ -3282,13 +3210,24 @@ describe('otlp metrics page', () => {
await Promise.resolve();
});
- const replaceAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-replace-action="service.name"]')
as HTMLButtonElement | null;
+ const replaceAction =
interactionContainer.querySelector('[data-otlp-metrics-attribute-operator-action="service.name"]')
as HTMLElement | null;
expect(replaceAction).not.toBeNull();
-
expect(replaceAction?.getAttribute('data-otlp-metrics-attribute-replace-action-owner')).toBe('hertzbeat-ui-button');
-
expect(replaceAction?.getAttribute('aria-label')).toContain('service.name');
+
expect(replaceAction?.getAttribute('data-otlp-metrics-attribute-operator-action-owner')).toBe('hertzbeat-ui-select');
+ expect(replaceAction?.getAttribute('data-hz-ui')).toBe('select');
+ expect((replaceAction?.querySelector('[data-hz-ui="select-trigger"]') as
HTMLButtonElement |
null)?.getAttribute('aria-label')).toContain('service.name');
+
+ await act(async () => {
+ (replaceAction?.querySelector('[data-hz-ui="select-trigger"]') as
HTMLButtonElement | null)?.click();
+ await Promise.resolve();
+ });
+ const replaceOption = replaceAction?.querySelector(
+
'[data-otlp-metrics-attribute-operator-option="replace"][data-otlp-metrics-attribute-operator-name="service.name"]'
+ ) as HTMLButtonElement | null;
+ expect(replaceOption).not.toBeNull();
+
expect(replaceOption?.getAttribute('data-otlp-metrics-attribute-replace-action-owner')).toBe('hertzbeat-ui-select-menu-option');
await act(async () => {
- replaceAction?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+ replaceOption?.click();
await Promise.resolve();
});
diff --git a/web-next/lib/i18n-runtime-messages.ts
b/web-next/lib/i18n-runtime-messages.ts
index b83f21414a..d617911679 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -3215,6 +3215,7 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'otlp.metrics.attributes.search.placeholder': 'Search attributes',
'otlp.metrics.attributes.column.name': 'Attribute',
'otlp.metrics.attributes.column.value': 'Value',
+ 'otlp.metrics.attributes.column.operator': 'Operator',
'otlp.metrics.attributes.column.filter': 'Filter',
'otlp.metrics.attributes.column.contains': 'Contains',
'otlp.metrics.attributes.column.not-contains': 'Not contains',
@@ -3245,6 +3246,18 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'otlp.metrics.attributes.replace-action.aria': 'Replace filters with
{{name}} equals {{value}}',
'otlp.metrics.attributes.group-action': 'Group',
'otlp.metrics.attributes.group-action.aria': 'Group by {{name}}',
+ 'otlp.metrics.attributes.operator.placeholder': 'Choose',
+ 'otlp.metrics.attributes.operator-action.aria': 'Choose a metrics
attribute operator for {{name}} {{value}}',
+ 'otlp.metrics.attributes.operator.filter': 'Filter',
+ 'otlp.metrics.attributes.operator.contains': 'Contains',
+ 'otlp.metrics.attributes.operator.not-contains': 'Not contains',
+ 'otlp.metrics.attributes.operator.in': 'In',
+ 'otlp.metrics.attributes.operator.not-in': 'Not in',
+ 'otlp.metrics.attributes.operator.exclude': 'Exclude',
+ 'otlp.metrics.attributes.operator.exists': 'Exists',
+ 'otlp.metrics.attributes.operator.not-exists': 'Not exists',
+ 'otlp.metrics.attributes.operator.replace': 'Replace',
+ 'otlp.metrics.attributes.operator.group': 'Group',
'otlp.metrics.attributes.empty': 'No attributes',
'otlp.metrics.series.entity-id': 'entityId {{entityId}}',
'otlp.metrics.series.entity-missing': 'Waiting for entity attribution',
@@ -7726,6 +7739,7 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'otlp.metrics.attributes.search.placeholder': '搜索属性',
'otlp.metrics.attributes.column.name': '属性',
'otlp.metrics.attributes.column.value': '值',
+ 'otlp.metrics.attributes.column.operator': '操作',
'otlp.metrics.attributes.column.filter': '过滤',
'otlp.metrics.attributes.column.contains': '包含',
'otlp.metrics.attributes.column.not-contains': '不包含',
@@ -7756,6 +7770,18 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'otlp.metrics.attributes.replace-action.aria': '替换为 {{name}} 等于 {{value}}
的过滤条件',
'otlp.metrics.attributes.group-action': '分组',
'otlp.metrics.attributes.group-action.aria': '按 {{name}} 分组',
+ 'otlp.metrics.attributes.operator.placeholder': '选择',
+ 'otlp.metrics.attributes.operator-action.aria': '为 {{name}} {{value}}
选择指标属性操作',
+ 'otlp.metrics.attributes.operator.filter': '过滤',
+ 'otlp.metrics.attributes.operator.contains': '包含',
+ 'otlp.metrics.attributes.operator.not-contains': '不包含',
+ 'otlp.metrics.attributes.operator.in': '属于',
+ 'otlp.metrics.attributes.operator.not-in': '不属于',
+ 'otlp.metrics.attributes.operator.exclude': '排除',
+ 'otlp.metrics.attributes.operator.exists': '存在',
+ 'otlp.metrics.attributes.operator.not-exists': '不存在',
+ 'otlp.metrics.attributes.operator.replace': '替换',
+ 'otlp.metrics.attributes.operator.group': '分组',
'otlp.metrics.attributes.empty': '暂无属性',
'otlp.metrics.series.entity-id': 'entityId {{entityId}}',
'otlp.metrics.series.entity-missing': '等待实体归因',
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]