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 adf3000b24 Add signal not-contains filter actions
adf3000b24 is described below

commit adf3000b2471b258850b24e5062fe2ec000c10df
Author: Logic <[email protected]>
AuthorDate: Tue Jun 9 23:12:00 2026 +0800

    Add signal not-contains filter actions
---
 .../ingestion/otlp/metrics/otlp-metrics-page.tsx   | 40 ++++++++++++
 web-next/app/ingestion/otlp/metrics/page.test.tsx  | 24 ++++++++
 web-next/app/log/manage/log-manage-page.tsx        | 47 +++++++++++++-
 web-next/app/log/manage/page.test.tsx              | 32 ++++++++++
 web-next/app/trace/manage/page.test.tsx            |  9 +++
 web-next/app/trace/manage/trace-manage-page.tsx    | 71 +++++++++++++++++++++-
 web-next/lib/i18n-runtime-messages.ts              | 14 +++++
 7 files changed, 233 insertions(+), 4 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 36e95fbb0e..7e462df32f 100644
--- a/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
+++ b/web-next/app/ingestion/otlp/metrics/otlp-metrics-page.tsx
@@ -131,6 +131,13 @@ function 
buildMetricAttributeContainsFilterExpression(name: string, value: strin
   return `${trimmedName} CONTAINS ${escapeMetricFilterValue(trimmedValue)}`;
 }
 
+function buildMetricAttributeNotContainsFilterExpression(name: string, value: 
string) {
+  const trimmedName = name.trim();
+  const trimmedValue = value.trim();
+  if (!trimmedName || !trimmedValue) return null;
+  return `${trimmedName} NOT CONTAINS 
${escapeMetricFilterValue(trimmedValue)}`;
+}
+
 function buildMetricAttributeExistsFilterExpression(name: string) {
   const trimmedName = name.trim();
   if (!trimmedName) return null;
@@ -847,6 +854,17 @@ export default function OtlpMetricsPage() {
     replaceMetricsRoute(nextDraft, undefined, series?.key || query.series, 
series ? buildMetricSeriesRouteContext(series) : {});
   }, [draft, query.series, replaceMetricsRoute]);
 
+  const applyMetricAttributeNotContainsFilter = useCallback((name: string, 
value: string, series?: OtlpMetricSeriesView | null) => {
+    const expression = buildMetricAttributeNotContainsFilterExpression(name, 
value);
+    if (!expression) return;
+    const nextDraft = {
+      ...draft,
+      filter: mergeMetricFilterExpression(draft.filter, expression)
+    };
+    setDraft(nextDraft);
+    replaceMetricsRoute(nextDraft, undefined, series?.key || query.series, 
series ? buildMetricSeriesRouteContext(series) : {});
+  }, [draft, query.series, replaceMetricsRoute]);
+
   const applyMetricAttributeExistsFilter = useCallback((name: string, series?: 
OtlpMetricSeriesView | null) => {
     const expression = buildMetricAttributeExistsFilterExpression(name);
     if (!expression) return;
@@ -2677,6 +2695,28 @@ export default function OtlpMetricsPage() {
                               </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: 'exclude',
                             header: 
t('otlp.metrics.attributes.column.exclude'),
diff --git a/web-next/app/ingestion/otlp/metrics/page.test.tsx 
b/web-next/app/ingestion/otlp/metrics/page.test.tsx
index a327961347..12c3bb7ec8 100644
--- a/web-next/app/ingestion/otlp/metrics/page.test.tsx
+++ b/web-next/app/ingestion/otlp/metrics/page.test.tsx
@@ -1038,21 +1038,26 @@ describe('otlp metrics page', () => {
     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.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('buildMetricAttributeExcludeFilterExpression');
     expect(source).toContain('buildMetricAttributeContainsFilterExpression');
+    
expect(source).toContain('buildMetricAttributeNotContainsFilterExpression');
     expect(source).toContain('buildMetricAttributeExistsFilterExpression');
     expect(source).toContain('buildMetricAttributeNotExistsFilterExpression');
     expect(source).toContain('applyMetricAttributeExcludeFilter');
     expect(source).toContain('applyMetricAttributeContainsFilter');
+    expect(source).toContain('applyMetricAttributeNotContainsFilter');
     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-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}');
@@ -3055,6 +3060,25 @@ 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;
+    expect(notContainsParams.get('filter')).toContain('service.name NOT 
CONTAINS checkout');
+    expect(notContainsParams.get('series')).toBe('checkout_latency-0');
+    expect(notContainsParams.get('entityId')).toBe('7');
+    expect(notContainsParams.get('serviceName')).toBe('checkout');
+    expect(notContainsParams.get('environment')).toBe('prod');
+    expect(notContainsParams.get('traceId')).toBe('trace-checkout');
+    expect(notContainsParams.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');
diff --git a/web-next/app/log/manage/log-manage-page.tsx 
b/web-next/app/log/manage/log-manage-page.tsx
index 3ff4f3af87..a683f3d229 100644
--- a/web-next/app/log/manage/log-manage-page.tsx
+++ b/web-next/app/log/manage/log-manage-page.tsx
@@ -291,6 +291,16 @@ function buildLogAttributeContainsExpression(row: 
LogAttributeRow, objectValueLa
   return { kind, expression: `${key} CONTAINS ${value}` };
 }
 
+function buildLogAttributeNotContainsExpression(row: LogAttributeRow, 
objectValueLabel: string) {
+  const kind = resolveLogAttributeFilterKind(row);
+  const key = row.name.trim();
+  const value = row.value.trim();
+  if (!kind || value === objectValueLabel || !isSafeLogAttributeFilterKey(key) 
|| !isSafeLogAttributeFilterValue(value)) {
+    return null;
+  }
+  return { kind, expression: `${key} NOT CONTAINS ${value}` };
+}
+
 function buildLogAttributeExistsExpression(row: LogAttributeRow) {
   const kind = resolveLogAttributeFilterKind(row);
   const key = row.name.trim();
@@ -2264,6 +2274,19 @@ function LogManageExplorer({
     applyQuery(nextQuery);
   }, [applyQuery, draft, setDraft, t]);
 
+  const applyLogAttributeNotContainsFilter = useCallback((row: 
LogAttributeRow) => {
+    const filter = buildLogAttributeNotContainsExpression(row, 
t('log.manage.attributes.value.object'));
+    if (!filter) return;
+    const nextQuery: LogQueryState = {
+      ...draft,
+      ...(filter.kind === 'resource'
+        ? { resourceFilter: 
mergeLogAttributeFilterExpression(draft.resourceFilter, filter.expression) }
+        : { attributeFilter: 
mergeLogAttributeFilterExpression(draft.attributeFilter, filter.expression) })
+    };
+    setDraft(nextQuery);
+    applyQuery(nextQuery);
+  }, [applyQuery, draft, setDraft, t]);
+
   const applyLogAttributeExistsFilter = useCallback((row: LogAttributeRow) => {
     const filter = buildLogAttributeExistsExpression(row);
     if (!filter) return;
@@ -2380,12 +2403,13 @@ function LogManageExplorer({
     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'));
+    const notContainsFilter = buildLogAttributeNotContainsExpression(row, 
t('log.manage.attributes.value.object'));
     const existsFilter = buildLogAttributeExistsExpression(row);
     const notExistsFilter = buildLogAttributeNotExistsExpression(row);
     const group = buildLogAttributeGroupBy(row);
     const fieldColumn = buildLogAttributeFieldColumn(row);
     const fieldColumnVisible = Boolean(fieldColumn && 
visibleLogFieldColumns.includes(fieldColumn));
-    if (!filter && !excludeFilter && !containsFilter && !existsFilter && 
!notExistsFilter && !group && !fieldColumn) return null;
+    if (!filter && !excludeFilter && !containsFilter && !notContainsFilter && 
!existsFilter && !notExistsFilter && !group && !fieldColumn) return null;
     return (
       <span className="inline-flex flex-wrap gap-1">
         {fieldColumn ? (
@@ -2503,6 +2527,25 @@ function LogManageExplorer({
             {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}
         {existsFilter ? (
           <HzButton
             data-log-manage-attribute-exists-action={existsFilter.kind}
@@ -2562,7 +2605,7 @@ function LogManageExplorer({
         ) : null}
       </span>
     );
-  }, [applyLogAttributeContainsFilter, applyLogAttributeExistsFilter, 
applyLogAttributeFieldColumn, applyLogAttributeFilter, 
applyLogAttributeNotExistsFilter, applyLogContextAttributeFilter, 
excludeLogAttributeFilter, groupLogAttribute, replaceLogAttributeFilter, t, 
visibleLogFieldColumns]);
+  }, [applyLogAttributeContainsFilter, applyLogAttributeExistsFilter, 
applyLogAttributeFieldColumn, applyLogAttributeFilter, 
applyLogAttributeNotContainsFilter, applyLogAttributeNotExistsFilter, 
applyLogContextAttributeFilter, excludeLogAttributeFilter, groupLogAttribute, 
replaceLogAttributeFilter, 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 b3449a46a5..41c4004a3b 100644
--- a/web-next/app/log/manage/page.test.tsx
+++ b/web-next/app/log/manage/page.test.tsx
@@ -656,6 +656,9 @@ describe('log manage page', () => {
     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('data-log-manage-attribute-exists-action={existsFilter.kind}');
     
expect(source).toContain('data-log-manage-attribute-not-exists-action={notExistsFilter.kind}');
     expect(source).toContain('buildLogAttributeNotExistsExpression');
@@ -3542,6 +3545,35 @@ describe('log manage page', () => {
 
       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();
+      });
+
+      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();
+      });
+
+      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');
     } finally {
       mockState.renderData.list.content = originalContent;
     }
diff --git a/web-next/app/trace/manage/page.test.tsx 
b/web-next/app/trace/manage/page.test.tsx
index 69c0fa1bae..85819e546f 100644
--- a/web-next/app/trace/manage/page.test.tsx
+++ b/web-next/app/trace/manage/page.test.tsx
@@ -1801,11 +1801,13 @@ describe('trace manage page', () => {
     expect(source).toContain('buildTraceResourceFilterExpression');
     expect(source).toContain('buildTraceResourceExcludeFilterExpression');
     expect(source).toContain('buildTraceResourceContainsFilterExpression');
+    expect(source).toContain('buildTraceResourceNotContainsFilterExpression');
     expect(source).toContain('buildTraceResourceExistsFilterExpression');
     expect(source).toContain('buildTraceResourceNotExistsFilterExpression');
     expect(source).toContain('buildTraceSpanAttributeFilterExpression');
     expect(source).toContain('buildTraceSpanAttributeExcludeFilterExpression');
     
expect(source).toContain('buildTraceSpanAttributeContainsFilterExpression');
+    
expect(source).toContain('buildTraceSpanAttributeNotContainsFilterExpression');
     expect(source).toContain('buildTraceSpanAttributeExistsFilterExpression');
     
expect(source).toContain('buildTraceSpanAttributeNotExistsFilterExpression');
     expect(source).toContain('buildTraceSpanAttributeGroupBy');
@@ -1813,12 +1815,14 @@ describe('trace manage page', () => {
     expect(source).toContain('applyTraceResourceFilter');
     expect(source).toContain('excludeTraceResourceFilter');
     expect(source).toContain('applyTraceResourceContainsFilter');
+    expect(source).toContain('applyTraceResourceNotContainsFilter');
     expect(source).toContain('applyTraceResourceExistsFilter');
     expect(source).toContain('applyTraceResourceNotExistsFilter');
     expect(source).toContain('replaceTraceResourceFilter');
     expect(source).toContain('applyTraceSpanAttributeFilter');
     expect(source).toContain('excludeTraceSpanAttributeFilter');
     expect(source).toContain('applyTraceSpanAttributeContainsFilter');
+    expect(source).toContain('applyTraceSpanAttributeNotContainsFilter');
     expect(source).toContain('applyTraceSpanAttributeExistsFilter');
     expect(source).toContain('applyTraceSpanAttributeNotExistsFilter');
     expect(source).toContain('replaceTraceSpanAttributeFilter');
@@ -1833,6 +1837,8 @@ describe('trace manage page', () => {
     
expect(source).toContain('data-trace-manage-drawer-span-attribute-filter-out-action-owner="hertzbeat-ui-button"');
     
expect(source).toContain('data-trace-manage-drawer-span-attribute-contains-action="true"');
     
expect(source).toContain('data-trace-manage-drawer-span-attribute-contains-action-owner="hertzbeat-ui-button"');
+    
expect(source).toContain('data-trace-manage-drawer-span-attribute-not-contains-action="true"');
+    
expect(source).toContain('data-trace-manage-drawer-span-attribute-not-contains-action-owner="hertzbeat-ui-button"');
     
expect(source).toContain('data-trace-manage-drawer-span-attribute-exists-action="true"');
     
expect(source).toContain('data-trace-manage-drawer-span-attribute-exists-action-owner="hertzbeat-ui-button"');
     
expect(source).toContain('data-trace-manage-drawer-span-attribute-not-exists-action="true"');
@@ -1856,6 +1862,9 @@ describe('trace manage page', () => {
     
expect(source).toContain('data-trace-manage-drawer-resource-contains-action="true"');
     
expect(source).toContain('data-trace-manage-drawer-resource-contains-action-owner="hertzbeat-ui-button"');
     
expect(source).toContain("t('trace.manage.drawer.attributes.contains-action')");
+    
expect(source).toContain('data-trace-manage-drawer-resource-not-contains-action="true"');
+    
expect(source).toContain('data-trace-manage-drawer-resource-not-contains-action-owner="hertzbeat-ui-button"');
+    
expect(source).toContain("t('trace.manage.drawer.attributes.not-contains-action')");
     
expect(source).toContain('data-trace-manage-drawer-resource-exists-action="true"');
     
expect(source).toContain('data-trace-manage-drawer-resource-exists-action-owner="hertzbeat-ui-button"');
     
expect(source).toContain('data-trace-manage-drawer-resource-not-exists-action="true"');
diff --git a/web-next/app/trace/manage/trace-manage-page.tsx 
b/web-next/app/trace/manage/trace-manage-page.tsx
index e05a6b6e14..10385658ad 100644
--- a/web-next/app/trace/manage/trace-manage-page.tsx
+++ b/web-next/app/trace/manage/trace-manage-page.tsx
@@ -524,6 +524,15 @@ function buildTraceResourceContainsFilterExpression(name: 
React.ReactNode, value
   return `${key} CONTAINS ${filterValue}`;
 }
 
+function buildTraceResourceNotContainsFilterExpression(name: React.ReactNode, 
value: React.ReactNode) {
+  const key = String(name ?? '').trim();
+  const filterValue = String(value ?? '').trim();
+  if (!isSafeTraceResourceFilterKey(key) || 
!isSafeTraceResourceFilterValue(filterValue)) {
+    return null;
+  }
+  return `${key} NOT CONTAINS ${filterValue}`;
+}
+
 function buildTraceResourceExistsFilterExpression(name: React.ReactNode) {
   const key = String(name ?? '').trim();
   if (!isSafeTraceResourceFilterKey(key)) {
@@ -552,6 +561,10 @@ function 
buildTraceSpanAttributeContainsFilterExpression(name: React.ReactNode,
   return buildTraceResourceContainsFilterExpression(name, value);
 }
 
+function buildTraceSpanAttributeNotContainsFilterExpression(name: 
React.ReactNode, value: React.ReactNode) {
+  return buildTraceResourceNotContainsFilterExpression(name, value);
+}
+
 function buildTraceSpanAttributeExistsFilterExpression(name: React.ReactNode) {
   return buildTraceResourceExistsFilterExpression(name);
 }
@@ -815,6 +828,7 @@ function TraceWaterfallDrawer({
   onApplySpanAttributeFilter,
   onExcludeSpanAttributeFilter,
   onApplySpanAttributeContainsFilter,
+  onApplySpanAttributeNotContainsFilter,
   onApplySpanAttributeExistsFilter,
   onApplySpanAttributeNotExistsFilter,
   onReplaceSpanAttributeFilter,
@@ -822,6 +836,7 @@ function TraceWaterfallDrawer({
   onApplyResourceFilter,
   onExcludeResourceFilter,
   onApplyResourceContainsFilter,
+  onApplyResourceNotContainsFilter,
   onApplyResourceExistsFilter,
   onApplyResourceNotExistsFilter,
   onReplaceResourceFilter,
@@ -839,6 +854,7 @@ function TraceWaterfallDrawer({
   onApplySpanAttributeFilter: (name: string, value: string) => void;
   onExcludeSpanAttributeFilter: (name: string, value: string) => void;
   onApplySpanAttributeContainsFilter: (name: string, value: string) => void;
+  onApplySpanAttributeNotContainsFilter: (name: string, value: string) => void;
   onApplySpanAttributeExistsFilter: (name: string) => void;
   onApplySpanAttributeNotExistsFilter: (name: string) => void;
   onReplaceSpanAttributeFilter: (name: string, value: string) => void;
@@ -846,6 +862,7 @@ function TraceWaterfallDrawer({
   onApplyResourceFilter: (name: string, value: string) => void;
   onExcludeResourceFilter: (name: string, value: string) => void;
   onApplyResourceContainsFilter: (name: string, value: string) => void;
+  onApplyResourceNotContainsFilter: (name: string, value: string) => void;
   onApplyResourceExistsFilter: (name: string) => void;
   onApplyResourceNotExistsFilter: (name: string) => void;
   onReplaceResourceFilter: (name: string, value: string) => void;
@@ -1283,7 +1300,7 @@ function TraceWaterfallDrawer({
                     title: row.title,
                     copy: row.copy,
                     meta: row.meta,
-                    action: buildTraceSpanAttributeFilterExpression(row.title, 
row.copy) || buildTraceSpanAttributeExcludeFilterExpression(row.title, 
row.copy) || buildTraceSpanAttributeContainsFilterExpression(row.title, 
row.copy) || buildTraceSpanAttributeExistsFilterExpression(row.title) || 
buildTraceSpanAttributeNotExistsFilterExpression(row.title) || 
buildTraceSpanAttributeGroupBy(row.title) ? (
+                    action: buildTraceSpanAttributeFilterExpression(row.title, 
row.copy) || buildTraceSpanAttributeExcludeFilterExpression(row.title, 
row.copy) || buildTraceSpanAttributeContainsFilterExpression(row.title, 
row.copy) || buildTraceSpanAttributeNotContainsFilterExpression(row.title, 
row.copy) || buildTraceSpanAttributeExistsFilterExpression(row.title) || 
buildTraceSpanAttributeNotExistsFilterExpression(row.title) || 
buildTraceSpanAttributeGroupBy(row.title) ? (
                       <HzActionGroup
                         
data-trace-manage-drawer-span-attribute-action-group="filter-group"
                         
data-trace-manage-drawer-span-attribute-action-group-owner="hertzbeat-ui-action-group"
@@ -1330,6 +1347,19 @@ function TraceWaterfallDrawer({
                               <HzButtonIcon icon={Search} 
data-trace-manage-drawer-span-attribute-contains-action-icon="contains" 
data-trace-manage-drawer-span-attribute-contains-action-icon-owner="hertzbeat-ui-button-icon"
 />
                               
{t('trace.manage.drawer.attributes.contains-action')}
                             </HzButton>
+                            <HzButton
+                              
data-trace-manage-drawer-span-attribute-not-contains-action="true"
+                              
data-trace-manage-drawer-span-attribute-not-contains-action-owner="hertzbeat-ui-button"
+                              
data-trace-manage-drawer-span-attribute-filter-name={row.title}
+                              
data-trace-manage-drawer-span-attribute-filter-value={row.copy}
+                              size="sm"
+                              intent="secondary"
+                              onClick={() => 
onApplySpanAttributeNotContainsFilter(row.title, row.copy)}
+                              
aria-label={t('trace.manage.drawer.attributes.not-contains-action.aria', { 
name: row.title, value: row.copy })}
+                            >
+                              <HzButtonIcon icon={Ban} 
data-trace-manage-drawer-span-attribute-not-contains-action-icon="not-contains" 
data-trace-manage-drawer-span-attribute-not-contains-action-icon-owner="hertzbeat-ui-button-icon"
 />
+                              
{t('trace.manage.drawer.attributes.not-contains-action')}
+                            </HzButton>
                             <HzButton
                               
data-trace-manage-drawer-span-attribute-exists-action="true"
                               
data-trace-manage-drawer-span-attribute-exists-action-owner="hertzbeat-ui-button"
@@ -1397,7 +1427,7 @@ function TraceWaterfallDrawer({
                     title: row.title,
                     copy: row.copy,
                     meta: row.meta,
-                    action: buildTraceResourceFilterExpression(row.title, 
row.copy) || buildTraceResourceExcludeFilterExpression(row.title, row.copy) || 
buildTraceResourceContainsFilterExpression(row.title, row.copy) || 
buildTraceResourceExistsFilterExpression(row.title) || 
buildTraceResourceNotExistsFilterExpression(row.title) || 
buildTraceResourceGroupBy(row.title) ? (
+                    action: buildTraceResourceFilterExpression(row.title, 
row.copy) || buildTraceResourceExcludeFilterExpression(row.title, row.copy) || 
buildTraceResourceContainsFilterExpression(row.title, row.copy) || 
buildTraceResourceNotContainsFilterExpression(row.title, row.copy) || 
buildTraceResourceExistsFilterExpression(row.title) || 
buildTraceResourceNotExistsFilterExpression(row.title) || 
buildTraceResourceGroupBy(row.title) ? (
                       <HzActionGroup
                         
data-trace-manage-drawer-resource-action-group="filter-group"
                         
data-trace-manage-drawer-resource-action-group-owner="hertzbeat-ui-action-group"
@@ -1444,6 +1474,19 @@ function TraceWaterfallDrawer({
                             <HzButtonIcon icon={Search} 
data-trace-manage-drawer-resource-contains-action-icon="contains" 
data-trace-manage-drawer-resource-contains-action-icon-owner="hertzbeat-ui-button-icon"
 />
                             
{t('trace.manage.drawer.attributes.contains-action')}
                           </HzButton>
+                          <HzButton
+                            
data-trace-manage-drawer-resource-not-contains-action="true"
+                            
data-trace-manage-drawer-resource-not-contains-action-owner="hertzbeat-ui-button"
+                            
data-trace-manage-drawer-resource-filter-name={row.title}
+                            
data-trace-manage-drawer-resource-filter-value={row.copy}
+                            size="sm"
+                            intent="secondary"
+                            onClick={() => 
onApplyResourceNotContainsFilter(row.title, row.copy)}
+                            
aria-label={t('trace.manage.drawer.attributes.not-contains-action.aria', { 
name: row.title, value: row.copy })}
+                          >
+                            <HzButtonIcon icon={Ban} 
data-trace-manage-drawer-resource-not-contains-action-icon="not-contains" 
data-trace-manage-drawer-resource-not-contains-action-icon-owner="hertzbeat-ui-button-icon"
 />
+                            
{t('trace.manage.drawer.attributes.not-contains-action')}
+                          </HzButton>
                           <HzButton
                             
data-trace-manage-drawer-resource-exists-action="true"
                             
data-trace-manage-drawer-resource-exists-action-owner="hertzbeat-ui-button"
@@ -2009,6 +2052,17 @@ function TraceExplorer({
     applyQuery(nextQuery);
   }, [applyQuery, draft, setDraft]);
 
+  const applyTraceResourceNotContainsFilter = useCallback((name: string, 
value: string) => {
+    const expression = buildTraceResourceNotContainsFilterExpression(name, 
value);
+    if (!expression) return;
+    const nextQuery: TraceQueryState = {
+      ...draft,
+      resourceFilter: mergeTraceResourceFilterExpression(draft.resourceFilter, 
expression)
+    };
+    setDraft(nextQuery);
+    applyQuery(nextQuery);
+  }, [applyQuery, draft, setDraft]);
+
   const applyTraceResourceExistsFilter = useCallback((name: string) => {
     const expression = buildTraceResourceExistsFilterExpression(name);
     if (!expression) return;
@@ -2075,6 +2129,17 @@ function TraceExplorer({
     applyQuery(nextQuery);
   }, [applyQuery, draft, setDraft]);
 
+  const applyTraceSpanAttributeNotContainsFilter = useCallback((name: string, 
value: string) => {
+    const expression = 
buildTraceSpanAttributeNotContainsFilterExpression(name, value);
+    if (!expression) return;
+    const nextQuery: TraceQueryState = {
+      ...draft,
+      attributeFilter: 
mergeTraceResourceFilterExpression(draft.attributeFilter, expression)
+    };
+    setDraft(nextQuery);
+    applyQuery(nextQuery);
+  }, [applyQuery, draft, setDraft]);
+
   const applyTraceSpanAttributeExistsFilter = useCallback((name: string) => {
     const expression = buildTraceSpanAttributeExistsFilterExpression(name);
     if (!expression) return;
@@ -3656,6 +3721,7 @@ function TraceExplorer({
           onApplySpanAttributeFilter={applyTraceSpanAttributeFilter}
           onExcludeSpanAttributeFilter={excludeTraceSpanAttributeFilter}
           
onApplySpanAttributeContainsFilter={applyTraceSpanAttributeContainsFilter}
+          
onApplySpanAttributeNotContainsFilter={applyTraceSpanAttributeNotContainsFilter}
           
onApplySpanAttributeExistsFilter={applyTraceSpanAttributeExistsFilter}
           
onApplySpanAttributeNotExistsFilter={applyTraceSpanAttributeNotExistsFilter}
           onReplaceSpanAttributeFilter={replaceTraceSpanAttributeFilter}
@@ -3663,6 +3729,7 @@ function TraceExplorer({
           onApplyResourceFilter={applyTraceResourceFilter}
           onExcludeResourceFilter={excludeTraceResourceFilter}
           onApplyResourceContainsFilter={applyTraceResourceContainsFilter}
+          
onApplyResourceNotContainsFilter={applyTraceResourceNotContainsFilter}
           onApplyResourceExistsFilter={applyTraceResourceExistsFilter}
           onApplyResourceNotExistsFilter={applyTraceResourceNotExistsFilter}
           onReplaceResourceFilter={replaceTraceResourceFilter}
diff --git a/web-next/lib/i18n-runtime-messages.ts 
b/web-next/lib/i18n-runtime-messages.ts
index 21abc762c5..cfa9be754b 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -2741,6 +2741,8 @@ export const SUPPLEMENTAL_MESSAGES: 
Partial<Record<LocaleCode, Messages>> = {
     'trace.manage.drawer.attributes.filter-action.aria': 'Filter {{name}} 
equals {{value}}',
     'trace.manage.drawer.attributes.contains-action': 'Contains',
     'trace.manage.drawer.attributes.contains-action.aria': 'Filter traces 
where {{name}} contains {{value}}',
+    'trace.manage.drawer.attributes.not-contains-action': 'Not contains',
+    'trace.manage.drawer.attributes.not-contains-action.aria': 'Filter traces 
where {{name}} does not contain {{value}}',
     'trace.manage.drawer.attributes.filter-out-action': 'Exclude',
     'trace.manage.drawer.attributes.filter-out-action.aria': 'Exclude traces 
where {{name}} equals {{value}}',
     'trace.manage.drawer.attributes.exists-action': 'Exists',
@@ -3211,6 +3213,7 @@ export const SUPPLEMENTAL_MESSAGES: 
Partial<Record<LocaleCode, Messages>> = {
     'otlp.metrics.attributes.column.value': 'Value',
     'otlp.metrics.attributes.column.filter': 'Filter',
     'otlp.metrics.attributes.column.contains': 'Contains',
+    'otlp.metrics.attributes.column.not-contains': 'Not contains',
     'otlp.metrics.attributes.column.exclude': 'Exclude',
     'otlp.metrics.attributes.column.exists': 'Exists',
     'otlp.metrics.attributes.column.not-exists': 'Not exists',
@@ -3220,6 +3223,8 @@ export const SUPPLEMENTAL_MESSAGES: 
Partial<Record<LocaleCode, Messages>> = {
     'otlp.metrics.attributes.filter-action.aria': 'Filter {{name}} equals 
{{value}}',
     'otlp.metrics.attributes.contains-action': 'Contains',
     'otlp.metrics.attributes.contains-action.aria': 'Filter metrics where 
{{name}} contains {{value}}',
+    'otlp.metrics.attributes.not-contains-action': 'Not contains',
+    'otlp.metrics.attributes.not-contains-action.aria': 'Filter metrics where 
{{name}} does not contain {{value}}',
     'otlp.metrics.attributes.filter-out-action': 'Exclude',
     'otlp.metrics.attributes.filter-out-action.aria': 'Exclude metrics where 
{{name}} equals {{value}}',
     'otlp.metrics.attributes.exists-action': 'Exists',
@@ -4003,6 +4008,8 @@ export const SUPPLEMENTAL_MESSAGES: 
Partial<Record<LocaleCode, Messages>> = {
     'log.manage.attributes.filter-action.aria': 'Filter {{name}} equals 
{{value}}',
     'log.manage.attributes.contains-action': 'Contains',
     'log.manage.attributes.contains-action.aria': 'Filter logs where {{name}} 
contains {{value}}',
+    'log.manage.attributes.not-contains-action': 'Not contains',
+    'log.manage.attributes.not-contains-action.aria': 'Filter logs where 
{{name}} does not contain {{value}}',
     'log.manage.attributes.context-filter-action': 'Filter context',
     'log.manage.attributes.context-filter-action.aria': 'Filter surrounding 
context where {{name}} equals {{value}}',
     'log.manage.attributes.filter-out-action': 'Exclude',
@@ -7231,6 +7238,8 @@ export const SUPPLEMENTAL_MESSAGES: 
Partial<Record<LocaleCode, Messages>> = {
     'trace.manage.drawer.attributes.filter-action.aria': '过滤 {{name}} 等于 
{{value}}',
     'trace.manage.drawer.attributes.contains-action': '包含',
     'trace.manage.drawer.attributes.contains-action.aria': '过滤 {{name}} 包含 
{{value}} 的链路',
+    'trace.manage.drawer.attributes.not-contains-action': '不包含',
+    'trace.manage.drawer.attributes.not-contains-action.aria': '过滤 {{name}} 
不包含 {{value}} 的链路',
     'trace.manage.drawer.attributes.filter-out-action': '排除',
     'trace.manage.drawer.attributes.filter-out-action.aria': '排除 {{name}} 等于 
{{value}} 的链路',
     'trace.manage.drawer.attributes.exists-action': '存在',
@@ -7701,6 +7710,7 @@ export const SUPPLEMENTAL_MESSAGES: 
Partial<Record<LocaleCode, Messages>> = {
     'otlp.metrics.attributes.column.value': '值',
     'otlp.metrics.attributes.column.filter': '过滤',
     'otlp.metrics.attributes.column.contains': '包含',
+    'otlp.metrics.attributes.column.not-contains': '不包含',
     'otlp.metrics.attributes.column.exclude': '排除',
     'otlp.metrics.attributes.column.exists': '存在',
     'otlp.metrics.attributes.column.not-exists': '不存在',
@@ -7710,6 +7720,8 @@ export const SUPPLEMENTAL_MESSAGES: 
Partial<Record<LocaleCode, Messages>> = {
     'otlp.metrics.attributes.filter-action.aria': '过滤 {{name}} 等于 {{value}}',
     'otlp.metrics.attributes.contains-action': '包含',
     'otlp.metrics.attributes.contains-action.aria': '过滤 {{name}} 包含 {{value}} 
的指标',
+    'otlp.metrics.attributes.not-contains-action': '不包含',
+    'otlp.metrics.attributes.not-contains-action.aria': '过滤 {{name}} 不包含 
{{value}} 的指标',
     'otlp.metrics.attributes.filter-out-action': '排除',
     'otlp.metrics.attributes.filter-out-action.aria': '排除 {{name}} 等于 
{{value}} 的指标',
     'otlp.metrics.attributes.exists-action': '存在',
@@ -8493,6 +8505,8 @@ export const SUPPLEMENTAL_MESSAGES: 
Partial<Record<LocaleCode, Messages>> = {
     'log.manage.attributes.filter-action.aria': '过滤 {{name}} 等于 {{value}}',
     'log.manage.attributes.contains-action': '包含',
     'log.manage.attributes.contains-action.aria': '过滤 {{name}} 包含 {{value}} 
的日志',
+    'log.manage.attributes.not-contains-action': '不包含',
+    'log.manage.attributes.not-contains-action.aria': '过滤 {{name}} 不包含 
{{value}} 的日志',
     'log.manage.attributes.context-filter-action': '筛选上下文',
     'log.manage.attributes.context-filter-action.aria': '筛选 {{name}} 等于 
{{value}} 的周边日志',
     'log.manage.attributes.filter-out-action': '排除',


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to