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 db3adbf8b6 Compact log attribute operator actions
db3adbf8b6 is described below
commit db3adbf8b6dc76d7ed98a25537ac75bedcf28031
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 23:47:29 2026 +0800
Compact log attribute operator actions
---
web-next/app/log/manage/log-manage-page.tsx | 336 +++++++++++-----------------
web-next/app/log/manage/page.test.tsx | 265 ++++++----------------
web-next/lib/i18n-runtime-messages.ts | 26 +++
3 files changed, 221 insertions(+), 406 deletions(-)
diff --git a/web-next/app/log/manage/log-manage-page.tsx
b/web-next/app/log/manage/log-manage-page.tsx
index 2ee01d09ef..f366fd07b2 100644
--- a/web-next/app/log/manage/log-manage-page.tsx
+++ b/web-next/app/log/manage/log-manage-page.tsx
@@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useMemo, useRef,
useState } from 'react'
import Link from 'next/link';
import {
BarChart3,
- Ban,
BellPlus,
BellRing,
Check,
@@ -142,6 +141,7 @@ type LogSavedQueryView = SignalSavedQueryView;
type LogExportRowLimit = 'current' | '10000' | '30000' | '50000';
type LogDashboardPanelDraftState = 'idle' | 'saving' | 'saved' | 'failed';
+type LogAttributeOperator = 'context' | 'filter' | 'replace' | 'exclude' |
'contains' | 'not-contains' | 'in' | 'not-in' | 'exists' | 'not-exists' |
'group';
type LogDetailContextPayload = {
targetTimeUnixNano: number;
@@ -240,6 +240,19 @@ const LOG_SAVED_QUERY_VIEW_PERSISTENCE_OWNER:
Record<SignalSavedQueryViewPersist
'local-fallback': 'browser-local-storage'
};
const LOG_EXPORT_ROW_LIMITS: LogExportRowLimit[] = ['current', '10000',
'30000', '50000'];
+const LOG_ATTRIBUTE_OPERATOR_LABEL_KEYS: Record<LogAttributeOperator, string>
= {
+ context: 'log.manage.attributes.operator.context',
+ filter: 'log.manage.attributes.operator.filter',
+ replace: 'log.manage.attributes.operator.replace',
+ exclude: 'log.manage.attributes.operator.exclude',
+ contains: 'log.manage.attributes.operator.contains',
+ 'not-contains': 'log.manage.attributes.operator.not-contains',
+ in: 'log.manage.attributes.operator.in',
+ 'not-in': 'log.manage.attributes.operator.not-in',
+ exists: 'log.manage.attributes.operator.exists',
+ 'not-exists': 'log.manage.attributes.operator.not-exists',
+ group: 'log.manage.attributes.operator.group'
+};
const LOG_EXPORT_FETCH_PAGE_SIZE = 1000;
const LOG_CONTEXT_WINDOW_MS = 5 * 60 * 1000;
const LOG_CONTEXT_LIST_PAGE_SIZE = '20';
@@ -1212,6 +1225,46 @@ type RelatedTracePreviewState = {
type LogManageTranslate = ReturnType<typeof useI18n>['t'];
+function buildLogAttributeOperatorOptions(t: LogManageTranslate, operators:
LogAttributeOperator[]) {
+ return operators.map(operator => ({
+ value: operator,
+ label: t(LOG_ATTRIBUTE_OPERATOR_LABEL_KEYS[operator] as
Parameters<LogManageTranslate>[0])
+ }));
+}
+
+function logAttributeOperatorDataAttributes(operator: LogAttributeOperator,
kind: 'resource' | 'attribute', row: LogAttributeRow) {
+ const base = {
+ 'data-log-manage-attribute-operator-option': operator,
+ 'data-log-manage-attribute-operator-kind': kind,
+ 'data-log-manage-attribute-filter-name': row.name,
+ 'data-log-manage-attribute-filter-value': row.value
+ };
+ switch (operator) {
+ case 'context':
+ return { ...base, 'data-log-stream-detail-context-filter-action': kind,
'data-log-stream-detail-context-filter-owner':
'hertzbeat-ui-select-menu-option' };
+ case 'filter':
+ return { ...base, 'data-log-manage-attribute-filter-action': kind,
'data-log-manage-attribute-filter-owner': 'hertzbeat-ui-select-menu-option' };
+ case 'replace':
+ return { ...base, 'data-log-manage-attribute-filter-replace-action':
kind, 'data-log-manage-attribute-filter-replace-owner':
'hertzbeat-ui-select-menu-option' };
+ case 'exclude':
+ return { ...base, 'data-log-manage-attribute-filter-out-action': kind,
'data-log-manage-attribute-filter-out-owner': 'hertzbeat-ui-select-menu-option'
};
+ case 'contains':
+ return { ...base, 'data-log-manage-attribute-contains-action': kind,
'data-log-manage-attribute-contains-owner': 'hertzbeat-ui-select-menu-option' };
+ case 'not-contains':
+ return { ...base, 'data-log-manage-attribute-not-contains-action': kind,
'data-log-manage-attribute-not-contains-owner':
'hertzbeat-ui-select-menu-option' };
+ case 'in':
+ return { ...base, 'data-log-manage-attribute-in-action': kind,
'data-log-manage-attribute-in-owner': 'hertzbeat-ui-select-menu-option' };
+ case 'not-in':
+ return { ...base, 'data-log-manage-attribute-not-in-action': kind,
'data-log-manage-attribute-not-in-owner': 'hertzbeat-ui-select-menu-option' };
+ case 'exists':
+ return { ...base, 'data-log-manage-attribute-exists-action': kind,
'data-log-manage-attribute-exists-owner': 'hertzbeat-ui-select-menu-option' };
+ case 'not-exists':
+ return { ...base, 'data-log-manage-attribute-not-exists-action': kind,
'data-log-manage-attribute-not-exists-owner': 'hertzbeat-ui-select-menu-option'
};
+ case 'group':
+ return { ...base, 'data-log-manage-attribute-group-action': kind,
'data-log-manage-attribute-group-owner': 'hertzbeat-ui-select-menu-option' };
+ }
+}
+
function isLogSavedQueryView(value: unknown): value is LogSavedQueryView {
if (!value || typeof value !== 'object') return false;
const candidate = value as Partial<LogSavedQueryView>;
@@ -2387,6 +2440,56 @@ function LogManageExplorer({
applyQuery(nextQuery);
}, [applyQuery, draft, setDraft]);
+ const applyLogAttributeOperator = useCallback((operator:
LogAttributeOperator, row: LogAttributeRow) => {
+ switch (operator) {
+ case 'context':
+ applyLogContextAttributeFilter(row);
+ break;
+ case 'filter':
+ applyLogAttributeFilter(row);
+ break;
+ case 'replace':
+ replaceLogAttributeFilter(row);
+ break;
+ case 'exclude':
+ excludeLogAttributeFilter(row);
+ break;
+ case 'contains':
+ applyLogAttributeContainsFilter(row);
+ break;
+ case 'not-contains':
+ applyLogAttributeNotContainsFilter(row);
+ break;
+ case 'in':
+ applyLogAttributeInFilter(row);
+ break;
+ case 'not-in':
+ applyLogAttributeNotInFilter(row);
+ break;
+ case 'exists':
+ applyLogAttributeExistsFilter(row);
+ break;
+ case 'not-exists':
+ applyLogAttributeNotExistsFilter(row);
+ break;
+ case 'group':
+ groupLogAttribute(row);
+ break;
+ }
+ }, [
+ applyLogAttributeContainsFilter,
+ applyLogAttributeExistsFilter,
+ applyLogAttributeFilter,
+ applyLogAttributeInFilter,
+ applyLogAttributeNotContainsFilter,
+ applyLogAttributeNotExistsFilter,
+ applyLogAttributeNotInFilter,
+ applyLogContextAttributeFilter,
+ excludeLogAttributeFilter,
+ groupLogAttribute,
+ replaceLogAttributeFilter
+ ]);
+
const applyLogAttributeFieldColumn = useCallback((row: LogAttributeRow) => {
const fieldColumn = buildLogAttributeFieldColumn(row);
if (!fieldColumn) return;
@@ -2450,6 +2553,7 @@ function LogManageExplorer({
}, [activeGroupBy, applyQuery, applyRouteContext, draft, routeContext,
setDraft]);
const renderLogAttributeFilterAction = useCallback((row: LogAttributeRow) =>
{
+ const kind = resolveLogAttributeFilterKind(row);
const filter = buildLogAttributeFilterExpression(row,
t('log.manage.attributes.value.object'));
const excludeFilter = buildLogAttributeExcludeExpression(row,
t('log.manage.attributes.value.object'));
const containsFilter = buildLogAttributeContainsExpression(row,
t('log.manage.attributes.value.object'));
@@ -2461,7 +2565,18 @@ function LogManageExplorer({
const group = buildLogAttributeGroupBy(row);
const fieldColumn = buildLogAttributeFieldColumn(row);
const fieldColumnVisible = Boolean(fieldColumn &&
visibleLogFieldColumns.includes(fieldColumn));
- if (!filter && !excludeFilter && !containsFilter && !notContainsFilter &&
!inFilter && !notInFilter && !existsFilter && !notExistsFilter && !group &&
!fieldColumn) return null;
+ const availableOperators: LogAttributeOperator[] = [
+ ...(filter ? ['context', 'filter', 'replace'] as const : []),
+ ...(excludeFilter ? ['exclude'] as const : []),
+ ...(containsFilter ? ['contains'] as const : []),
+ ...(notContainsFilter ? ['not-contains'] as const : []),
+ ...(inFilter ? ['in'] as const : []),
+ ...(notInFilter ? ['not-in'] as const : []),
+ ...(existsFilter ? ['exists'] as const : []),
+ ...(notExistsFilter ? ['not-exists'] as const : []),
+ ...(group ? ['group'] as const : [])
+ ];
+ if (!availableOperators.length && !fieldColumn) return null;
return (
<span className="inline-flex flex-wrap gap-1">
{fieldColumn ? (
@@ -2486,216 +2601,25 @@ function LogManageExplorer({
{fieldColumnVisible ?
t('log.manage.attributes.remove-column-action') :
t('log.manage.attributes.add-column-action')}
</HzButton>
) : null}
- {filter ? (
- <>
- <HzButton
- data-log-stream-detail-context-filter-action={filter.kind}
- data-log-stream-detail-context-filter-owner="hertzbeat-ui-button"
- data-log-manage-attribute-filter-name={row.name}
- data-log-manage-attribute-filter-value={row.value}
- size="sm"
- intent="secondary"
- onClick={() => applyLogContextAttributeFilter(row)}
-
aria-label={t('log.manage.attributes.context-filter-action.aria', { name:
row.name, value: row.value })}
- >
- <HzButtonIcon
- icon={ScrollText}
- data-log-stream-detail-context-filter-icon="context-filter"
-
data-log-stream-detail-context-filter-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('log.manage.attributes.context-filter-action')}
- </HzButton>
- <HzButton
- data-log-manage-attribute-filter-action={filter.kind}
- data-log-manage-attribute-filter-owner="hertzbeat-ui-button"
- data-log-manage-attribute-filter-name={row.name}
- data-log-manage-attribute-filter-value={row.value}
- size="sm"
- intent="secondary"
- onClick={() => applyLogAttributeFilter(row)}
- aria-label={t('log.manage.attributes.filter-action.aria', {
name: row.name, value: row.value })}
- >
- <HzButtonIcon
- icon={Filter}
- data-log-manage-attribute-filter-action-icon="filter"
-
data-log-manage-attribute-filter-action-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('log.manage.attributes.filter-action')}
- </HzButton>
- <HzButton
- data-log-manage-attribute-filter-replace-action={filter.kind}
-
data-log-manage-attribute-filter-replace-owner="hertzbeat-ui-button"
- data-log-manage-attribute-filter-name={row.name}
- data-log-manage-attribute-filter-value={row.value}
- size="sm"
- intent="secondary"
- onClick={() => replaceLogAttributeFilter(row)}
- aria-label={t('log.manage.attributes.replace-action.aria', {
name: row.name, value: row.value })}
- >
- <HzButtonIcon
- icon={Replace}
- data-log-manage-attribute-filter-replace-icon="replace"
-
data-log-manage-attribute-filter-replace-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('log.manage.attributes.replace-action')}
- </HzButton>
- </>
- ) : null}
- {excludeFilter ? (
- <HzButton
- data-log-manage-attribute-filter-out-action={excludeFilter.kind}
- data-log-manage-attribute-filter-out-owner="hertzbeat-ui-button"
- data-log-manage-attribute-filter-name={row.name}
- data-log-manage-attribute-filter-value={row.value}
- size="sm"
- intent="secondary"
- onClick={() => excludeLogAttributeFilter(row)}
- aria-label={t('log.manage.attributes.filter-out-action.aria', {
name: row.name, value: row.value })}
- >
- <HzButtonIcon
- icon={X}
- data-log-manage-attribute-filter-out-icon="exclude"
-
data-log-manage-attribute-filter-out-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('log.manage.attributes.filter-out-action')}
- </HzButton>
- ) : null}
- {containsFilter ? (
- <HzButton
- data-log-manage-attribute-contains-action={containsFilter.kind}
- data-log-manage-attribute-contains-owner="hertzbeat-ui-button"
+ {kind && availableOperators.length ? (
+ <HzSelect
+ data-log-manage-attribute-operator-action={kind}
+ data-log-manage-attribute-operator-owner="hertzbeat-ui-select"
data-log-manage-attribute-filter-name={row.name}
data-log-manage-attribute-filter-value={row.value}
size="sm"
- intent="secondary"
- onClick={() => applyLogAttributeContainsFilter(row)}
- aria-label={t('log.manage.attributes.contains-action.aria', {
name: row.name, value: row.value })}
- >
- <HzButtonIcon
- icon={Search}
- data-log-manage-attribute-contains-icon="contains"
-
data-log-manage-attribute-contains-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('log.manage.attributes.contains-action')}
- </HzButton>
- ) : null}
- {notContainsFilter ? (
- <HzButton
-
data-log-manage-attribute-not-contains-action={notContainsFilter.kind}
- data-log-manage-attribute-not-contains-owner="hertzbeat-ui-button"
- data-log-manage-attribute-filter-name={row.name}
- data-log-manage-attribute-filter-value={row.value}
- size="sm"
- intent="secondary"
- onClick={() => applyLogAttributeNotContainsFilter(row)}
- aria-label={t('log.manage.attributes.not-contains-action.aria', {
name: row.name, value: row.value })}
- >
- <HzButtonIcon
- icon={Ban}
- data-log-manage-attribute-not-contains-icon="not-contains"
-
data-log-manage-attribute-not-contains-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('log.manage.attributes.not-contains-action')}
- </HzButton>
- ) : null}
- {inFilter ? (
- <HzButton
- data-log-manage-attribute-in-action={inFilter.kind}
- data-log-manage-attribute-in-owner="hertzbeat-ui-button"
- data-log-manage-attribute-filter-name={row.name}
- data-log-manage-attribute-filter-value={row.value}
- size="sm"
- intent="secondary"
- onClick={() => applyLogAttributeInFilter(row)}
- aria-label={t('log.manage.attributes.in-action.aria', { name:
row.name, value: row.value })}
- >
- <HzButtonIcon
- icon={ListChecks}
- data-log-manage-attribute-in-icon="in"
-
data-log-manage-attribute-in-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('log.manage.attributes.in-action')}
- </HzButton>
- ) : null}
- {notInFilter ? (
- <HzButton
- data-log-manage-attribute-not-in-action={notInFilter.kind}
- data-log-manage-attribute-not-in-owner="hertzbeat-ui-button"
- data-log-manage-attribute-filter-name={row.name}
- data-log-manage-attribute-filter-value={row.value}
- size="sm"
- intent="secondary"
- onClick={() => applyLogAttributeNotInFilter(row)}
- aria-label={t('log.manage.attributes.not-in-action.aria', { name:
row.name, value: row.value })}
- >
- <HzButtonIcon
- icon={Ban}
- data-log-manage-attribute-not-in-icon="not-in"
-
data-log-manage-attribute-not-in-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('log.manage.attributes.not-in-action')}
- </HzButton>
- ) : null}
- {existsFilter ? (
- <HzButton
- data-log-manage-attribute-exists-action={existsFilter.kind}
- data-log-manage-attribute-exists-owner="hertzbeat-ui-button"
- data-log-manage-attribute-filter-name={row.name}
- data-log-manage-attribute-filter-value={row.value}
- size="sm"
- intent="secondary"
- onClick={() => applyLogAttributeExistsFilter(row)}
- aria-label={t('log.manage.attributes.exists-action.aria', { name:
row.name })}
- >
- <HzButtonIcon
- icon={Check}
- data-log-manage-attribute-exists-icon="exists"
-
data-log-manage-attribute-exists-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('log.manage.attributes.exists-action')}
- </HzButton>
- ) : null}
- {notExistsFilter ? (
- <HzButton
- data-log-manage-attribute-not-exists-action={notExistsFilter.kind}
- data-log-manage-attribute-not-exists-owner="hertzbeat-ui-button"
- data-log-manage-attribute-filter-name={row.name}
- data-log-manage-attribute-filter-value={row.value}
- size="sm"
- intent="secondary"
- onClick={() => applyLogAttributeNotExistsFilter(row)}
- aria-label={t('log.manage.attributes.not-exists-action.aria', {
name: row.name })}
- >
- <HzButtonIcon
- icon={Ban}
- data-log-manage-attribute-not-exists-icon="not-exists"
-
data-log-manage-attribute-not-exists-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('log.manage.attributes.not-exists-action')}
- </HzButton>
- ) : null}
- {group ? (
- <HzButton
- data-log-manage-attribute-group-action={group.kind}
- data-log-manage-attribute-group-owner="hertzbeat-ui-button"
- data-log-manage-attribute-filter-name={row.name}
- data-log-manage-attribute-filter-value={row.value}
- size="sm"
- intent="secondary"
- onClick={() => groupLogAttribute(row)}
- aria-label={t('log.manage.attributes.group-action.aria', { name:
row.name })}
- >
- <HzButtonIcon
- icon={BarChart3}
- data-log-manage-attribute-group-icon="group"
-
data-log-manage-attribute-group-icon-owner="hertzbeat-ui-button-icon"
- />
- {t('log.manage.attributes.group-action')}
- </HzButton>
+ width="log-severity"
+ value=""
+ placeholder={t('log.manage.attributes.operator.placeholder')}
+ aria-label={t('log.manage.attributes.operator-action.aria', {
name: row.name, value: row.value })}
+ options={buildLogAttributeOperatorOptions(t, availableOperators)}
+ optionDataAttributes={option =>
logAttributeOperatorDataAttributes(option.value as LogAttributeOperator, kind,
row)}
+ onChange={event => applyLogAttributeOperator(event.target.value as
LogAttributeOperator, row)}
+ />
) : null}
</span>
);
- }, [applyLogAttributeContainsFilter, applyLogAttributeExistsFilter,
applyLogAttributeFieldColumn, applyLogAttributeFilter,
applyLogAttributeInFilter, applyLogAttributeNotContainsFilter,
applyLogAttributeNotExistsFilter, applyLogAttributeNotInFilter,
applyLogContextAttributeFilter, excludeLogAttributeFilter, groupLogAttribute,
replaceLogAttributeFilter, t, visibleLogFieldColumns]);
+ }, [applyLogAttributeFieldColumn, applyLogAttributeOperator, t,
visibleLogFieldColumns]);
const openLogDetails = (entry: LogEntry | null, source: 'history' |
'stream', selectionState: 'attached' | 'detached' = 'attached') => {
if (!entry) return;
diff --git a/web-next/app/log/manage/page.test.tsx
b/web-next/app/log/manage/page.test.tsx
index 3fcac0abca..c7f07d40d0 100644
--- a/web-next/app/log/manage/page.test.tsx
+++ b/web-next/app/log/manage/page.test.tsx
@@ -581,6 +581,31 @@ function renderInteractiveLogManagePage(initialRouteState
= buildLogManageRouteS
interactionRoot?.render(<LogManagePage initialRouteState={initialRouteState}
/>);
}
+async function applyLogAttributeOperator(kind: 'resource' | 'attribute', name:
string, operator: string) {
+ const operatorRoot = interactionContainer?.querySelector(
+
`[data-log-manage-attribute-operator-action="${kind}"][data-log-manage-attribute-filter-name="${name}"]`
+ ) as HTMLElement | null;
+ expect(operatorRoot).toBeTruthy();
+
expect(operatorRoot?.getAttribute('data-log-manage-attribute-operator-owner')).toBe('hertzbeat-ui-select');
+ expect(operatorRoot?.getAttribute('data-hz-ui')).toBe('select');
+
+ await act(async () => {
+ (operatorRoot?.querySelector('[data-hz-ui="select-trigger"]') as
HTMLButtonElement | null)?.click();
+ await Promise.resolve();
+ });
+
+ const option = operatorRoot?.querySelector(
+
`[data-log-manage-attribute-operator-option="${operator}"][data-log-manage-attribute-operator-kind="${kind}"][data-log-manage-attribute-filter-name="${name}"]`
+ ) as HTMLButtonElement | null;
+ expect(option).toBeTruthy();
+
+ await act(async () => {
+ option?.click();
+ await Promise.resolve();
+ });
+ return String(mockState.replace.mock.calls.at(-1)?.[0]);
+}
+
async function flushDashboardEditPromises() {
for (let index = 0; index < 8; index += 1) {
await Promise.resolve();
@@ -655,20 +680,22 @@ describe('log manage page', () => {
expect(source).toContain('applyLogAttributeExistsFilter');
expect(source).toContain('buildLogAttributeContainsExpression');
expect(source).toContain('applyLogAttributeContainsFilter');
-
expect(source).toContain('data-log-manage-attribute-contains-action={containsFilter.kind}');
expect(source).toContain('buildLogAttributeNotContainsExpression');
expect(source).toContain('applyLogAttributeNotContainsFilter');
-
expect(source).toContain('data-log-manage-attribute-not-contains-action={notContainsFilter.kind}');
expect(source).toContain('buildLogAttributeInExpression');
expect(source).toContain('applyLogAttributeInFilter');
-
expect(source).toContain('data-log-manage-attribute-in-action={inFilter.kind}');
expect(source).toContain('buildLogAttributeNotInExpression');
expect(source).toContain('applyLogAttributeNotInFilter');
-
expect(source).toContain('data-log-manage-attribute-not-in-action={notInFilter.kind}');
-
expect(source).toContain('data-log-manage-attribute-exists-action={existsFilter.kind}');
-
expect(source).toContain('data-log-manage-attribute-not-exists-action={notExistsFilter.kind}');
expect(source).toContain('buildLogAttributeNotExistsExpression');
-
expect(source).toContain('data-log-manage-attribute-exists-owner="hertzbeat-ui-button"');
+ expect(source).toContain('buildLogAttributeOperatorOptions');
+ expect(source).toContain('logAttributeOperatorDataAttributes');
+ expect(source).toContain('applyLogAttributeOperator');
+
expect(source).toContain('data-log-manage-attribute-operator-action={kind}');
+
expect(source).toContain('data-log-manage-attribute-operator-owner="hertzbeat-ui-select"');
+ expect(source).toContain('optionDataAttributes={option =>
logAttributeOperatorDataAttributes(option.value as LogAttributeOperator, kind,
row)}');
+
expect(source).not.toContain('data-log-manage-attribute-exists-owner="hertzbeat-ui-button"');
+
expect(source).not.toContain('data-log-manage-attribute-contains-owner="hertzbeat-ui-button"');
+
expect(source).not.toContain('data-log-manage-attribute-group-owner="hertzbeat-ui-button"');
expect(source).toContain('data-log-manage-stream-selected-detail-owner="hertzbeat-ui-detail-rows"');
expect(source).toContain('data-log-manage-stream-detail-action-stack="shared-control-stack"');
expect(source).toContain('data-log-manage-stream-detail-action-stack-owner="hertzbeat-ui-control-stack"');
@@ -3259,16 +3286,10 @@ describe('log manage page', () => {
await Promise.resolve();
});
- const contextFilterAction = interactionContainer.querySelector(
-
'[data-log-stream-detail-context-filter-action="attribute"][data-log-manage-attribute-filter-name="region"]'
- ) as HTMLButtonElement | null;
- expect(contextFilterAction).toBeTruthy();
-
expect(contextFilterAction?.getAttribute('data-log-stream-detail-context-filter-owner')).toBe('hertzbeat-ui-button');
mockState.replace.mockClear();
+ await applyLogAttributeOperator('attribute', 'region', 'context');
await act(async () => {
- contextFilterAction?.click();
- await Promise.resolve();
await Promise.resolve();
});
@@ -3377,31 +3398,25 @@ describe('log manage page', () => {
await Promise.resolve();
});
- const resourceAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-filter-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
- ) as HTMLButtonElement | null;
- const attributeAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-filter-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
- ) as HTMLButtonElement | null;
- expect(resourceAction).toBeTruthy();
- expect(attributeAction).toBeTruthy();
-
expect(resourceAction?.getAttribute('data-log-manage-attribute-filter-owner')).toBe('hertzbeat-ui-button');
-
expect(attributeAction?.getAttribute('data-log-manage-attribute-filter-owner')).toBe('hertzbeat-ui-button');
+ const resourceOperator = interactionContainer.querySelector(
+
'[data-log-manage-attribute-operator-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
+ ) as HTMLElement | null;
+ const attributeOperator = interactionContainer.querySelector(
+
'[data-log-manage-attribute-operator-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
+ ) as HTMLElement | null;
+ expect(resourceOperator).toBeTruthy();
+ expect(attributeOperator).toBeTruthy();
+
expect(resourceOperator?.getAttribute('data-log-manage-attribute-operator-owner')).toBe('hertzbeat-ui-select');
+
expect(attributeOperator?.getAttribute('data-log-manage-attribute-operator-owner')).toBe('hertzbeat-ui-select');
mockState.replace.mockClear();
- await act(async () => {
- resourceAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('resource', 'service.version', 'filter');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(String(mockState.replace.mock.calls[0]?.[0])).toContain('resourceFilter=service.version%3D1.2.3');
mockState.replace.mockClear();
- await act(async () => {
- attributeAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('attribute', 'http.route', 'filter');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(String(mockState.replace.mock.calls[0]?.[0])).toContain('attributeFilter=http.route%3A%2Fcheckout%2F%3Aid');
@@ -3436,205 +3451,87 @@ describe('log manage page', () => {
await Promise.resolve();
});
- const excludeResourceAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-filter-out-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
- ) as HTMLButtonElement | null;
- const excludeAttributeAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-filter-out-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
- ) as HTMLButtonElement | null;
- expect(excludeResourceAction).toBeTruthy();
- expect(excludeAttributeAction).toBeTruthy();
-
expect(excludeResourceAction?.getAttribute('data-log-manage-attribute-filter-out-owner')).toBe('hertzbeat-ui-button');
-
expect(excludeAttributeAction?.getAttribute('data-log-manage-attribute-filter-out-owner')).toBe('hertzbeat-ui-button');
mockState.replace.mockClear();
- await act(async () => {
- excludeResourceAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('resource', 'service.version',
'exclude');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(String(mockState.replace.mock.calls[0]?.[0])).toContain('resourceFilter=service.version%21%3D1.2.3');
mockState.replace.mockClear();
- await act(async () => {
- excludeAttributeAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('attribute', 'http.route', 'exclude');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(String(mockState.replace.mock.calls[0]?.[0])).toContain('attributeFilter=http.route%21%3D%2Fcheckout%2F%3Aid');
- const existsResourceAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-exists-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
- ) as HTMLButtonElement | null;
- const existsAttributeAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-exists-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
- ) as HTMLButtonElement | null;
- expect(existsResourceAction).toBeTruthy();
- expect(existsAttributeAction).toBeTruthy();
-
expect(existsResourceAction?.getAttribute('data-log-manage-attribute-exists-owner')).toBe('hertzbeat-ui-button');
-
expect(existsAttributeAction?.getAttribute('data-log-manage-attribute-exists-owner')).toBe('hertzbeat-ui-button');
-
mockState.replace.mockClear();
- await act(async () => {
- existsResourceAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('resource', 'service.version', 'exists');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('resourceFilter')).toContain('service.version
EXISTS');
mockState.replace.mockClear();
- await act(async () => {
- existsAttributeAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('attribute', 'http.route', 'exists');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('attributeFilter')).toContain('http.route
EXISTS');
- const notExistsResourceAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-not-exists-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
- ) as HTMLButtonElement | null;
- const notExistsAttributeAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-not-exists-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
- ) as HTMLButtonElement | null;
- expect(notExistsResourceAction).toBeTruthy();
- expect(notExistsAttributeAction).toBeTruthy();
-
expect(notExistsResourceAction?.getAttribute('data-log-manage-attribute-not-exists-owner')).toBe('hertzbeat-ui-button');
-
expect(notExistsAttributeAction?.getAttribute('data-log-manage-attribute-not-exists-owner')).toBe('hertzbeat-ui-button');
-
mockState.replace.mockClear();
- await act(async () => {
- notExistsResourceAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('resource', 'service.version',
'not-exists');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('resourceFilter')).toContain('service.version
NOT EXISTS');
mockState.replace.mockClear();
- await act(async () => {
- notExistsAttributeAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('attribute', 'http.route', 'not-exists');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('attributeFilter')).toContain('http.route
NOT EXISTS');
- const containsResourceAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-contains-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
- ) as HTMLButtonElement | null;
- const containsAttributeAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-contains-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
- ) as HTMLButtonElement | null;
- expect(containsResourceAction).toBeTruthy();
- expect(containsAttributeAction).toBeTruthy();
-
expect(containsResourceAction?.getAttribute('data-log-manage-attribute-contains-owner')).toBe('hertzbeat-ui-button');
-
expect(containsAttributeAction?.getAttribute('data-log-manage-attribute-contains-owner')).toBe('hertzbeat-ui-button');
-
mockState.replace.mockClear();
- await act(async () => {
- containsResourceAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('resource', 'service.version',
'contains');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('resourceFilter')).toContain('service.version
CONTAINS 1.2.3');
mockState.replace.mockClear();
- await act(async () => {
- containsAttributeAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('attribute', 'http.route', 'contains');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('attributeFilter')).toContain('http.route
CONTAINS /checkout/:id');
- const notContainsResourceAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-not-contains-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
- ) as HTMLButtonElement | null;
- const notContainsAttributeAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-not-contains-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
- ) as HTMLButtonElement | null;
- expect(notContainsResourceAction).toBeTruthy();
- expect(notContainsAttributeAction).toBeTruthy();
-
expect(notContainsResourceAction?.getAttribute('data-log-manage-attribute-not-contains-owner')).toBe('hertzbeat-ui-button');
-
expect(notContainsAttributeAction?.getAttribute('data-log-manage-attribute-not-contains-owner')).toBe('hertzbeat-ui-button');
-
mockState.replace.mockClear();
- await act(async () => {
- notContainsResourceAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('resource', 'service.version',
'not-contains');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('resourceFilter')).toContain('service.version
NOT CONTAINS 1.2.3');
mockState.replace.mockClear();
- await act(async () => {
- notContainsAttributeAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('attribute', 'http.route',
'not-contains');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('attributeFilter')).toContain('http.route
NOT CONTAINS /checkout/:id');
- const inResourceAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-in-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
- ) as HTMLButtonElement | null;
- const inAttributeAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-in-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
- ) as HTMLButtonElement | null;
- expect(inResourceAction).toBeTruthy();
- expect(inAttributeAction).toBeTruthy();
-
expect(inResourceAction?.getAttribute('data-log-manage-attribute-in-owner')).toBe('hertzbeat-ui-button');
-
expect(inAttributeAction?.getAttribute('data-log-manage-attribute-in-owner')).toBe('hertzbeat-ui-button');
-
mockState.replace.mockClear();
- await act(async () => {
- inResourceAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('resource', 'service.version', 'in');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('resourceFilter')).toContain('service.version
IN ("1.2.3")');
mockState.replace.mockClear();
- await act(async () => {
- inAttributeAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('attribute', 'http.route', 'in');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('attributeFilter')).toContain('http.route
IN ("/checkout/:id")');
- const notInResourceAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-not-in-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
- ) as HTMLButtonElement | null;
- const notInAttributeAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-not-in-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
- ) as HTMLButtonElement | null;
- expect(notInResourceAction).toBeTruthy();
- expect(notInAttributeAction).toBeTruthy();
-
expect(notInResourceAction?.getAttribute('data-log-manage-attribute-not-in-owner')).toBe('hertzbeat-ui-button');
-
expect(notInAttributeAction?.getAttribute('data-log-manage-attribute-not-in-owner')).toBe('hertzbeat-ui-button');
-
mockState.replace.mockClear();
- await act(async () => {
- notInResourceAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('resource', 'service.version', 'not-in');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('resourceFilter')).toContain('service.version
NOT IN ("1.2.3")');
mockState.replace.mockClear();
- await act(async () => {
- notInAttributeAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('attribute', 'http.route', 'not-in');
expect(mockState.replace).toHaveBeenCalledTimes(1);
expect(new URL(String(mockState.replace.mock.calls[0]?.[0]),
'http://localhost').searchParams.get('attributeFilter')).toContain('http.route
NOT IN ("/checkout/:id")');
@@ -3671,32 +3568,16 @@ describe('log manage page', () => {
await Promise.resolve();
});
- const replaceResourceAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-filter-replace-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
- ) as HTMLButtonElement | null;
- const replaceAttributeAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-filter-replace-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
- ) as HTMLButtonElement | null;
- expect(replaceResourceAction).toBeTruthy();
- expect(replaceAttributeAction).toBeTruthy();
-
expect(replaceResourceAction?.getAttribute('data-log-manage-attribute-filter-replace-owner')).toBe('hertzbeat-ui-button');
-
expect(replaceAttributeAction?.getAttribute('data-log-manage-attribute-filter-replace-owner')).toBe('hertzbeat-ui-button');
mockState.replace.mockClear();
- await act(async () => {
- replaceResourceAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('resource', 'service.version',
'replace');
const resourceRoute = String(mockState.replace.mock.calls[0]?.[0]);
expect(resourceRoute).toContain('resourceFilter=service.version%3D1.2.3');
expect(resourceRoute).not.toContain('resourceFilter=service.version%3D1.0.0');
mockState.replace.mockClear();
- await act(async () => {
- replaceAttributeAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('attribute', 'http.route', 'replace');
const attributeRoute = String(mockState.replace.mock.calls[0]?.[0]);
expect(attributeRoute).toContain('attributeFilter=http.route%3A%2Fcheckout%2F%3Aid');
@@ -3732,32 +3613,16 @@ describe('log manage page', () => {
await Promise.resolve();
});
- const groupResourceAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-group-action="resource"][data-log-manage-attribute-filter-name="service.version"]'
- ) as HTMLButtonElement | null;
- const groupAttributeAction = interactionContainer.querySelector(
-
'[data-log-manage-attribute-group-action="attribute"][data-log-manage-attribute-filter-name="http.route"]'
- ) as HTMLButtonElement | null;
- expect(groupResourceAction).toBeTruthy();
- expect(groupAttributeAction).toBeTruthy();
-
expect(groupResourceAction?.getAttribute('data-log-manage-attribute-group-owner')).toBe('hertzbeat-ui-button');
-
expect(groupAttributeAction?.getAttribute('data-log-manage-attribute-group-owner')).toBe('hertzbeat-ui-button');
mockState.replace.mockClear();
- await act(async () => {
- groupResourceAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('resource', 'service.version', 'group');
const resourceRoute = String(mockState.replace.mock.calls[0]?.[0]);
expect(resourceRoute).toContain('groupBy=resource%3Aservice.version');
expect(resourceRoute).not.toContain('groupBy=resource%3Aservice.namespace');
mockState.replace.mockClear();
- await act(async () => {
- groupAttributeAction?.click();
- await Promise.resolve();
- });
+ await applyLogAttributeOperator('attribute', 'http.route', 'group');
const attributeRoute = String(mockState.replace.mock.calls[0]?.[0]);
expect(attributeRoute).toContain('groupBy=attribute%3Ahttp.route');
diff --git a/web-next/lib/i18n-runtime-messages.ts
b/web-next/lib/i18n-runtime-messages.ts
index d617911679..3ac5d8cd11 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -4049,6 +4049,19 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'log.manage.attributes.replace-action.aria': 'Replace filters with
{{name}} equals {{value}}',
'log.manage.attributes.group-action': 'Group',
'log.manage.attributes.group-action.aria': 'Group logs by {{name}}',
+ 'log.manage.attributes.operator.placeholder': 'Choose',
+ 'log.manage.attributes.operator-action.aria': 'Choose a log attribute
operator for {{name}} {{value}}',
+ 'log.manage.attributes.operator.context': 'Filter context',
+ 'log.manage.attributes.operator.filter': 'Filter',
+ 'log.manage.attributes.operator.replace': 'Replace',
+ 'log.manage.attributes.operator.exclude': 'Exclude',
+ 'log.manage.attributes.operator.contains': 'Contains',
+ 'log.manage.attributes.operator.not-contains': 'Not contains',
+ 'log.manage.attributes.operator.in': 'In',
+ 'log.manage.attributes.operator.not-in': 'Not in',
+ 'log.manage.attributes.operator.exists': 'Exists',
+ 'log.manage.attributes.operator.not-exists': 'Not exists',
+ 'log.manage.attributes.operator.group': 'Group',
'log.manage.attributes.add-column-action': 'Add column',
'log.manage.attributes.add-column-action.aria': 'Add {{name}} as a log
table column',
'log.manage.attributes.remove-column-action': 'Remove column',
@@ -8573,6 +8586,19 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'log.manage.attributes.replace-action.aria': '替换为 {{name}} 等于 {{value}}
的过滤条件',
'log.manage.attributes.group-action': '分组',
'log.manage.attributes.group-action.aria': '按 {{name}} 分组日志',
+ 'log.manage.attributes.operator.placeholder': '选择',
+ 'log.manage.attributes.operator-action.aria': '为 {{name}} {{value}}
选择日志属性操作',
+ 'log.manage.attributes.operator.context': '筛选上下文',
+ 'log.manage.attributes.operator.filter': '过滤',
+ 'log.manage.attributes.operator.replace': '替换',
+ 'log.manage.attributes.operator.exclude': '排除',
+ 'log.manage.attributes.operator.contains': '包含',
+ 'log.manage.attributes.operator.not-contains': '不包含',
+ 'log.manage.attributes.operator.in': '属于',
+ 'log.manage.attributes.operator.not-in': '不属于',
+ 'log.manage.attributes.operator.exists': '存在',
+ 'log.manage.attributes.operator.not-exists': '不存在',
+ 'log.manage.attributes.operator.group': '分组',
'log.manage.attributes.add-column-action': '添加列',
'log.manage.attributes.add-column-action.aria': '将 {{name}} 添加为日志表格列',
'log.manage.attributes.remove-column-action': '移除列',
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]