This is an automated email from the ASF dual-hosted git repository.

cyyang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git


The following commit(s) were added to refs/heads/master by this push:
     new a19a399acd [bugfix&improve] Fix alert label persistence and improve 
custom label input (#3933)
a19a399acd is described below

commit a19a399acd193c9575cf14ae7e74a3d072d86966
Author: P_Peaceful <[email protected]>
AuthorDate: Tue Jan 6 12:01:53 2026 +0800

    [bugfix&improve] Fix alert label persistence and improve custom label input 
(#3933)
---
 .../alert/service/impl/AlertDefineServiceImpl.java | 40 +++++++++--
 .../alert/service/AlertDefineServiceTest.java      | 78 ++++++++++++----------
 .../org/apache/hertzbeat/base}/dao/LabelDao.java   |  6 +-
 .../hertzbeat/base}/service/LabelService.java      |  2 +-
 .../manager/controller/LabelController.java        |  4 +-
 .../service/impl/AbstractImExportServiceImpl.java  |  2 +-
 .../manager/service/impl/LabelServiceImpl.java     |  4 +-
 .../manager/service/impl/MonitorServiceImpl.java   |  4 +-
 .../manager/service/LabelServiceTest.java          |  3 +-
 .../manager/service/MonitorServiceTest.java        |  1 +
 .../apache/hertzbeat/startup/dao/LabelDaoTest.java |  2 +-
 .../alert-setting/alert-setting.component.html     |  3 +
 .../alert/alert-setting/alert-setting.component.ts |  1 +
 .../label-selector/label-selector.component.html   | 16 ++++-
 .../label-selector/label-selector.component.ts     | 75 ++++++++++++++++++++-
 15 files changed, 187 insertions(+), 54 deletions(-)

diff --git 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java
 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java
index 16b2376fe8..22340def60 100644
--- 
a/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java
+++ 
b/hertzbeat-alerter/src/main/java/org/apache/hertzbeat/alert/service/impl/AlertDefineServiceImpl.java
@@ -29,11 +29,14 @@ import org.apache.hertzbeat.alert.dao.AlertDefineDao;
 import org.apache.hertzbeat.alert.service.AlertDefineImExportService;
 import org.apache.hertzbeat.alert.service.AlertDefineService;
 import org.apache.hertzbeat.alert.service.DataSourceService;
+import org.apache.hertzbeat.base.dao.LabelDao;
+import org.apache.hertzbeat.base.service.LabelService;
 import org.apache.hertzbeat.common.cache.CacheFactory;
 import org.apache.hertzbeat.common.constants.CommonConstants;
 import org.apache.hertzbeat.common.constants.ExportFileConstants;
 import org.apache.hertzbeat.common.constants.SignConstants;
 import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
+import org.apache.hertzbeat.common.entity.manager.Label;
 import org.apache.hertzbeat.common.util.FileUtil;
 import org.apache.hertzbeat.common.util.JexlExpressionRunner;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -48,6 +51,7 @@ import 
org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
 import org.springframework.web.multipart.MultipartFile;
 
+import javax.annotation.Resource;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
@@ -58,6 +62,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Alarm definition management interface implementation
@@ -69,10 +74,16 @@ public class AlertDefineServiceImpl implements 
AlertDefineService {
 
     @Autowired
     private AlertDefineDao alertDefineDao;
-    
+
     @Autowired
     private PeriodicAlertRuleScheduler periodicAlertRuleScheduler;
 
+    @Resource
+    private LabelService labelService;
+
+    @Resource
+    private LabelDao labelDao;
+
     private final DataSourceService dataSourceService;
 
     private final Map<String, AlertDefineImExportService> 
alertDefineImExportServiceMap = new HashMap<>();
@@ -93,7 +104,7 @@ public class AlertDefineServiceImpl implements 
AlertDefineService {
                     JexlExpressionRunner.compile(alertDefine.getExpr());
                 } catch (Exception e) {
                     throw new IllegalArgumentException("alert expr error: " + 
e.getMessage());
-                }   
+                }
             }
         }
         // the name of the alarm rule is unique
@@ -107,6 +118,7 @@ public class AlertDefineServiceImpl implements 
AlertDefineService {
 
     @Override
     public void addAlertDefine(AlertDefine alertDefine) throws 
RuntimeException {
+        saveNewCustomLabel(alertDefine);
         alertDefine = alertDefineDao.saveAndFlush(alertDefine);
         periodicAlertRuleScheduler.updateSchedule(alertDefine);
         CacheFactory.clearAlertDefineCache();
@@ -114,11 +126,31 @@ public class AlertDefineServiceImpl implements 
AlertDefineService {
 
     @Override
     public void modifyAlertDefine(AlertDefine alertDefine) throws 
RuntimeException {
+        saveNewCustomLabel(alertDefine);
         alertDefineDao.saveAndFlush(alertDefine);
         periodicAlertRuleScheduler.updateSchedule(alertDefine);
         CacheFactory.clearAlertDefineCache();
     }
 
+    private void saveNewCustomLabel(AlertDefine alertDefine) {
+        Map<String, String> labels = alertDefine.getLabels();
+        if (labels == null) {
+            labels = new HashMap<>(8);
+            alertDefine.setLabels(labels);
+        }
+        Map<String, String> customLabels = labels.entrySet().stream()
+            .filter(entry -> !isSystemBuiltInLabel(entry.getKey()))
+            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+        List<Label> addLabels = 
labelService.determineNewLabels(customLabels.entrySet());
+        if (!addLabels.isEmpty()) {
+            labelDao.saveAll(addLabels);
+        }
+    }
+
+    private boolean isSystemBuiltInLabel(String labelKey) {
+        return CommonConstants.ALERT_MODE_LABEL.equals(labelKey) || 
CommonConstants.LABEL_ALERT_SEVERITY.contains(labelKey);
+    }
+
     @Override
     public void deleteAlertDefine(long alertId) throws RuntimeException {
         alertDefineDao.deleteById(alertId);
@@ -256,7 +288,7 @@ public class AlertDefineServiceImpl implements 
AlertDefineService {
         if (!StringUtils.hasText(type)) {
             throw new IllegalArgumentException("Alert definition type cannot 
be null or empty");
         }
-        
+
         switch (type) {
             case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_REALTIME:
             case CommonConstants.METRIC_ALERT_THRESHOLD_TYPE_PERIODIC:
@@ -267,7 +299,7 @@ public class AlertDefineServiceImpl implements 
AlertDefineService {
             default:
                 throw new IllegalArgumentException("Unsupported alert 
definition type: " + type);
         }
-        
+
         // Query enabled alert definitions by type
         return alertDefineDao.findAlertDefinesByTypeAndEnableTrue(type);
     }
diff --git 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java
 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java
index 4f40c17ded..de29d2ccd5 100644
--- 
a/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java
+++ 
b/hertzbeat-alerter/src/test/java/org/apache/hertzbeat/alert/service/AlertDefineServiceTest.java
@@ -21,6 +21,8 @@ import com.google.common.collect.Lists;
 import 
org.apache.hertzbeat.alert.calculate.periodic.PeriodicAlertRuleScheduler;
 import org.apache.hertzbeat.alert.dao.AlertDefineDao;
 import org.apache.hertzbeat.alert.service.impl.AlertDefineServiceImpl;
+import org.apache.hertzbeat.base.dao.LabelDao;
+import org.apache.hertzbeat.base.service.LabelService;
 import org.apache.hertzbeat.common.cache.CacheFactory;
 import org.apache.hertzbeat.common.entity.alerter.AlertDefine;
 import org.junit.jupiter.api.BeforeEach;
@@ -69,7 +71,13 @@ class AlertDefineServiceTest {
 
     @Mock
     private AlertDefineDao alertDefineDao;
-    
+
+    @Mock
+    private LabelService labelService;
+
+    @Mock
+    private LabelDao labelDao;
+
     @Mock
     private PeriodicAlertRuleScheduler periodicAlertRuleScheduler;
 
@@ -86,6 +94,8 @@ class AlertDefineServiceTest {
     void setUp() {
         ReflectionTestUtils.setField(this.alertDefineService, 
"alertDefineDao", alertDefineDao);
         ReflectionTestUtils.setField(this.alertDefineService, 
"periodicAlertRuleScheduler", periodicAlertRuleScheduler);
+        ReflectionTestUtils.setField(this.alertDefineService, "labelService", 
labelService);
+        ReflectionTestUtils.setField(this.alertDefineService, "labelDao", 
labelDao);
 
         this.alertDefine = AlertDefine.builder()
                 .id(1L)
@@ -187,40 +197,40 @@ class AlertDefineServiceTest {
                 
AlertDefine.builder().id(1L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build(),
                 
AlertDefine.builder().id(2L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
         );
-        
+
         try (MockedStatic<CacheFactory> cacheFactoryMock = 
Mockito.mockStatic(CacheFactory.class)) {
             // Mock cache hit
             
cacheFactoryMock.when(CacheFactory::getMetricsAlertDefineCache).thenReturn(cachedAlertDefines);
-            
+
             List<AlertDefine> result = 
alertDefineService.getMetricsRealTimeAlertDefines();
-            
+
             assertNotNull(result);
             assertEquals(2, result.size());
             assertEquals(1L, result.get(0).getId());
             assertEquals(2L, result.get(1).getId());
-            
+
             // Verify no database query was called
             verify(alertDefineDao, 
times(0)).findAlertDefinesByTypeAndEnableTrue(any());
             cacheFactoryMock.verify(() -> 
CacheFactory.setMetricsAlertDefineCache(any()), times(0));
         }
-        
+
         // Test cache miss scenario
         List<AlertDefine> dbAlertDefines = Lists.newArrayList(
                 
AlertDefine.builder().id(3L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
         );
-        
+
         try (MockedStatic<CacheFactory> cacheFactoryMock = 
Mockito.mockStatic(CacheFactory.class)) {
             // Mock cache miss
             
cacheFactoryMock.when(CacheFactory::getMetricsAlertDefineCache).thenReturn(null);
             
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME))
                     .thenReturn(dbAlertDefines);
-            
+
             List<AlertDefine> result = 
alertDefineService.getMetricsRealTimeAlertDefines();
-            
+
             assertNotNull(result);
             assertEquals(1, result.size());
             assertEquals(3L, result.get(0).getId());
-            
+
             // Verify database query and cache setting were called
             verify(alertDefineDao, 
times(1)).findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME);
             cacheFactoryMock.verify(() -> 
CacheFactory.setMetricsAlertDefineCache(dbAlertDefines), times(1));
@@ -234,40 +244,40 @@ class AlertDefineServiceTest {
                 
AlertDefine.builder().id(4L).type(LOG_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build(),
                 
AlertDefine.builder().id(5L).type(LOG_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
         );
-        
+
         try (MockedStatic<CacheFactory> cacheFactoryMock = 
Mockito.mockStatic(CacheFactory.class)) {
             // Mock cache hit
             
cacheFactoryMock.when(CacheFactory::getLogAlertDefineCache).thenReturn(cachedAlertDefines);
-            
+
             List<AlertDefine> result = 
alertDefineService.getLogRealTimeAlertDefines();
-            
+
             assertNotNull(result);
             assertEquals(2, result.size());
             assertEquals(4L, result.get(0).getId());
             assertEquals(5L, result.get(1).getId());
-            
+
             // Verify no database query was called
             verify(alertDefineDao, 
times(0)).findAlertDefinesByTypeAndEnableTrue(any());
             cacheFactoryMock.verify(() -> 
CacheFactory.setLogAlertDefineCache(any()), times(0));
         }
-        
+
         // Test cache miss scenario
         List<AlertDefine> dbAlertDefines = Lists.newArrayList(
                 
AlertDefine.builder().id(6L).type(LOG_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
         );
-        
+
         try (MockedStatic<CacheFactory> cacheFactoryMock = 
Mockito.mockStatic(CacheFactory.class)) {
             // Mock cache miss
             
cacheFactoryMock.when(CacheFactory::getLogAlertDefineCache).thenReturn(null);
             
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME))
                     .thenReturn(dbAlertDefines);
-            
+
             List<AlertDefine> result = 
alertDefineService.getLogRealTimeAlertDefines();
-            
+
             assertNotNull(result);
             assertEquals(1, result.size());
             assertEquals(6L, result.get(0).getId());
-            
+
             // Verify database query and cache setting were called
             verify(alertDefineDao, 
times(1)).findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME);
             cacheFactoryMock.verify(() -> 
CacheFactory.setLogAlertDefineCache(dbAlertDefines), times(1));
@@ -280,58 +290,58 @@ class AlertDefineServiceTest {
                 
AlertDefine.builder().id(7L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build(),
                 
AlertDefine.builder().id(8L).type(METRIC_ALERT_THRESHOLD_TYPE_REALTIME).enable(true).build()
         );
-        
+
         // Test valid metric realtime alert type
         
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME))
                 .thenReturn(mockAlertDefines);
-        
+
         List<AlertDefine> result = 
alertDefineService.getAlertDefinesByType(METRIC_ALERT_THRESHOLD_TYPE_REALTIME);
-        
+
         assertNotNull(result);
         assertEquals(2, result.size());
         assertEquals(7L, result.get(0).getId());
         assertEquals(8L, result.get(1).getId());
         verify(alertDefineDao, 
times(1)).findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_REALTIME);
-        
+
         // Test valid metric periodic alert type
         reset(alertDefineDao);
         
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC))
                 .thenReturn(Lists.newArrayList());
-        
+
         result = 
alertDefineService.getAlertDefinesByType(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC);
-        
+
         assertNotNull(result);
         assertEquals(0, result.size());
         verify(alertDefineDao, 
times(1)).findAlertDefinesByTypeAndEnableTrue(METRIC_ALERT_THRESHOLD_TYPE_PERIODIC);
-        
+
         // Test valid log realtime alert type
         reset(alertDefineDao);
         
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME))
                 .thenReturn(mockAlertDefines);
-        
+
         result = 
alertDefineService.getAlertDefinesByType(LOG_ALERT_THRESHOLD_TYPE_REALTIME);
-        
+
         assertNotNull(result);
         assertEquals(2, result.size());
         verify(alertDefineDao, 
times(1)).findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_REALTIME);
-        
+
         // Test valid log periodic alert type
         reset(alertDefineDao);
         
when(alertDefineDao.findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC))
                 .thenReturn(Lists.newArrayList());
-        
+
         result = 
alertDefineService.getAlertDefinesByType(LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
-        
+
         assertNotNull(result);
         assertEquals(0, result.size());
         verify(alertDefineDao, 
times(1)).findAlertDefinesByTypeAndEnableTrue(LOG_ALERT_THRESHOLD_TYPE_PERIODIC);
-        
+
         // Test empty string type
         assertThrows(IllegalArgumentException.class, () -> 
alertDefineService.getAlertDefinesByType(""));
-        
+
         // Test null type
         assertThrows(IllegalArgumentException.class, () -> 
alertDefineService.getAlertDefinesByType(null));
-        
+
         // Test invalid type
         assertThrows(IllegalArgumentException.class, () -> 
alertDefineService.getAlertDefinesByType("invalid_type"));
     }
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/LabelDao.java
 b/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/dao/LabelDao.java
similarity index 96%
rename from 
hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/LabelDao.java
rename to 
hertzbeat-base/src/main/java/org/apache/hertzbeat/base/dao/LabelDao.java
index d1498e58e8..372e75acb3 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/LabelDao.java
+++ b/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/dao/LabelDao.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.dao;
+package org.apache.hertzbeat.base.dao;
 
 import java.util.Optional;
 import java.util.Set;
@@ -33,7 +33,7 @@ public interface LabelDao extends JpaRepository<Label, Long>, 
JpaSpecificationEx
      * @param ids id list
      */
     void deleteLabelsByIdIn(Set<Long> ids);
-    
+
     /**
      * find Label by name and value
      * @param name Label name
@@ -41,5 +41,5 @@ public interface LabelDao extends JpaRepository<Label, Long>, 
JpaSpecificationEx
      * @return Label
      */
     Optional<Label> findLabelByNameAndTagValue(String name, String value);
-    
+
 }
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/LabelService.java
 
b/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/service/LabelService.java
similarity index 97%
rename from 
hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/LabelService.java
rename to 
hertzbeat-base/src/main/java/org/apache/hertzbeat/base/service/LabelService.java
index 1b24dc279b..5de8b7e540 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/LabelService.java
+++ 
b/hertzbeat-base/src/main/java/org/apache/hertzbeat/base/service/LabelService.java
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package org.apache.hertzbeat.manager.service;
+package org.apache.hertzbeat.base.service;
 
 import java.util.HashSet;
 import java.util.List;
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/LabelController.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/LabelController.java
index 6588cd44ee..eba5c55a66 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/LabelController.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/LabelController.java
@@ -26,7 +26,7 @@ import java.util.HashSet;
 import java.util.List;
 import org.apache.hertzbeat.common.entity.dto.Message;
 import org.apache.hertzbeat.common.entity.manager.Label;
-import org.apache.hertzbeat.manager.service.LabelService;
+import org.apache.hertzbeat.base.service.LabelService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Page;
 import org.springframework.http.ResponseEntity;
@@ -46,7 +46,7 @@ import org.springframework.web.bind.annotation.RestController;
 @RestController
 @RequestMapping(path = "/api/label", produces = {APPLICATION_JSON_VALUE})
 public class LabelController {
-    
+
     @Autowired
     private LabelService labelService;
 
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
index 89277f36c6..7bf85944de 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AbstractImExportServiceImpl.java
@@ -31,7 +31,7 @@ import org.apache.hertzbeat.manager.config.ManagerSseManager;
 import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
 import org.apache.hertzbeat.manager.service.ImExportService;
 import org.apache.hertzbeat.manager.service.MonitorService;
-import org.apache.hertzbeat.manager.service.LabelService;
+import org.apache.hertzbeat.base.service.LabelService;
 import org.springframework.beans.BeanUtils;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.util.CollectionUtils;
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/LabelServiceImpl.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/LabelServiceImpl.java
index 127bcc892b..a8c63637ae 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/LabelServiceImpl.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/LabelServiceImpl.java
@@ -31,8 +31,8 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hertzbeat.common.entity.manager.Label;
-import org.apache.hertzbeat.manager.dao.LabelDao;
-import org.apache.hertzbeat.manager.service.LabelService;
+import org.apache.hertzbeat.base.dao.LabelDao;
+import org.apache.hertzbeat.base.service.LabelService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageRequest;
diff --git 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
index a1d903e4b4..4c97f91432 100644
--- 
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
+++ 
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MonitorServiceImpl.java
@@ -50,7 +50,7 @@ import org.apache.hertzbeat.grafana.service.DashboardService;
 import org.apache.hertzbeat.manager.component.validator.ParamValidatorManager;
 import org.apache.hertzbeat.manager.dao.CollectorDao;
 import org.apache.hertzbeat.manager.dao.CollectorMonitorBindDao;
-import org.apache.hertzbeat.manager.dao.LabelDao;
+import org.apache.hertzbeat.base.dao.LabelDao;
 import org.apache.hertzbeat.manager.dao.MonitorBindDao;
 import org.apache.hertzbeat.manager.dao.MonitorDao;
 import org.apache.hertzbeat.manager.dao.ParamDao;
@@ -59,7 +59,7 @@ import org.apache.hertzbeat.manager.pojo.dto.MetricsInfo;
 import org.apache.hertzbeat.manager.pojo.dto.MonitorDto;
 import org.apache.hertzbeat.manager.scheduler.CollectJobScheduling;
 import org.apache.hertzbeat.manager.service.AppService;
-import org.apache.hertzbeat.manager.service.LabelService;
+import org.apache.hertzbeat.base.service.LabelService;
 import org.apache.hertzbeat.manager.service.MetricsFavoriteService;
 import org.apache.hertzbeat.manager.service.MonitorService;
 import org.apache.hertzbeat.manager.service.helper.MonitorImExportHelper;
diff --git 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/LabelServiceTest.java
 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/LabelServiceTest.java
index a99d564f74..29e2d2924d 100644
--- 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/LabelServiceTest.java
+++ 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/LabelServiceTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.hertzbeat.manager.service;
 
+import org.apache.hertzbeat.base.service.LabelService;
 import org.junit.jupiter.api.Assertions;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -34,7 +35,7 @@ import java.util.Map;
 import java.util.Optional;
 
 import org.apache.hertzbeat.common.entity.manager.Label;
-import org.apache.hertzbeat.manager.dao.LabelDao;
+import org.apache.hertzbeat.base.dao.LabelDao;
 import org.apache.hertzbeat.manager.service.impl.LabelServiceImpl;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
diff --git 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/MonitorServiceTest.java
 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/MonitorServiceTest.java
index a48293a3c0..1a20aaad34 100644
--- 
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/MonitorServiceTest.java
+++ 
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/MonitorServiceTest.java
@@ -35,6 +35,7 @@ import jakarta.persistence.criteria.CriteriaBuilder;
 import jakarta.persistence.criteria.CriteriaQuery;
 import jakarta.persistence.criteria.Root;
 import org.apache.hertzbeat.alert.dao.AlertDefineBindDao;
+import org.apache.hertzbeat.base.service.LabelService;
 import org.apache.hertzbeat.common.constants.CommonConstants;
 import org.apache.hertzbeat.common.entity.job.Job;
 import org.apache.hertzbeat.common.entity.job.Metrics;
diff --git 
a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/dao/LabelDaoTest.java
 
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/dao/LabelDaoTest.java
index 9c57ad3f38..b8619010ca 100644
--- 
a/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/dao/LabelDaoTest.java
+++ 
b/hertzbeat-startup/src/test/java/org/apache/hertzbeat/startup/dao/LabelDaoTest.java
@@ -27,7 +27,7 @@ import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.hertzbeat.common.entity.manager.Label;
-import org.apache.hertzbeat.manager.dao.LabelDao;
+import org.apache.hertzbeat.base.dao.LabelDao;
 import org.apache.hertzbeat.startup.AbstractSpringIntegrationTest;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
diff --git 
a/web-app/src/app/routes/alert/alert-setting/alert-setting.component.html 
b/web-app/src/app/routes/alert/alert-setting/alert-setting.component.html
index 224e315a56..751dc4bf61 100644
--- a/web-app/src/app/routes/alert/alert-setting/alert-setting.component.html
+++ b/web-app/src/app/routes/alert/alert-setting/alert-setting.component.html
@@ -910,6 +910,9 @@
               field: 'labels',
               type: 'label-selector'
             }"
+            [extra]="{
+              labelIsCustom: labelIsCustom
+            }"
             [name]="'labels'"
             [(ngModel)]="define.labels"
           />
diff --git 
a/web-app/src/app/routes/alert/alert-setting/alert-setting.component.ts 
b/web-app/src/app/routes/alert/alert-setting/alert-setting.component.ts
index 8e51fc531e..bb6725af8c 100644
--- a/web-app/src/app/routes/alert/alert-setting/alert-setting.component.ts
+++ b/web-app/src/app/routes/alert/alert-setting/alert-setting.component.ts
@@ -577,6 +577,7 @@ export class AlertSettingComponent implements OnInit {
   userExpr!: string;
   severity!: string;
   alertMode!: string;
+  labelIsCustom: boolean = true;
   logFields: any[] = [];
 
   editAlertDefine(alertDefineId: number) {
diff --git 
a/web-app/src/app/shared/components/label-selector/label-selector.component.html
 
b/web-app/src/app/shared/components/label-selector/label-selector.component.html
index 0e34b7161b..c0fcc9a284 100644
--- 
a/web-app/src/app/shared/components/label-selector/label-selector.component.html
+++ 
b/web-app/src/app/shared/components/label-selector/label-selector.component.html
@@ -32,7 +32,13 @@
     >
       <nz-option *ngFor="let keyOption of labelKeys" [nzLabel]="keyOption" 
[nzValue]="keyOption"></nz-option>
       <nz-option
-        *ngIf="labelIsCustom && customInputKey && 
!hasMatchingOption(customInputKey, labelKeys)"
+        *ngIf="
+          labelIsCustom &&
+          activeSearchIndex === i &&
+          activeSearchType === 'key' &&
+          customInputKey &&
+          !hasMatchingOption(customInputKey, labelKeys)
+        "
         [nzLabel]="customInputKey"
         [nzValue]="customInputKey"
       ></nz-option>
@@ -53,7 +59,13 @@
     >
       <nz-option *ngFor="let valueOption of getLabelValues(value[i].key)" 
[nzLabel]="valueOption" [nzValue]="valueOption"></nz-option>
       <nz-option
-        *ngIf="labelIsCustom && customInputValue && 
!hasMatchingOption(customInputValue, getLabelValues(value[i].key))"
+        *ngIf="
+          labelIsCustom &&
+          activeSearchIndex === i &&
+          activeSearchType === 'value' &&
+          customInputValue &&
+          !hasMatchingOption(customInputValue, getLabelValues(value[i].key))
+        "
         [nzLabel]="customInputValue"
         [nzValue]="customInputValue"
       ></nz-option>
diff --git 
a/web-app/src/app/shared/components/label-selector/label-selector.component.ts 
b/web-app/src/app/shared/components/label-selector/label-selector.component.ts
index 031be73dfd..1d4275bcab 100644
--- 
a/web-app/src/app/shared/components/label-selector/label-selector.component.ts
+++ 
b/web-app/src/app/shared/components/label-selector/label-selector.component.ts
@@ -46,32 +46,105 @@ export class LabelSelectorComponent implements 
ControlValueAccessor, OnInit {
   value: Array<{ key: string; value: string }> = [];
   customInputKey: string = '';
   customInputValue: string = '';
+  // Track the active search input to prevent cross-row pollution of custom 
options
+  activeSearchIndex: number | null = null;
+  activeSearchType: 'key' | 'value' | null = null;
 
   _onChange = (_: any) => {};
   _onTouched = () => {};
 
   writeValue(value: any): void {
-    this.value = value && value.length > 0 ? value : [{ key: '', value: '' }];
+    // Validate and initialize: use valid array if provided, otherwise 
initialize with empty key-value pair
+    if (!value || !Array.isArray(value) || value.length === 0) {
+      this.value = [{ key: '', value: '' }];
+      return;
+    }
+    this.value = value;
+    // Build labelKeys and labelMap for autocomplete from the provided values
+    this.value.forEach(item => {
+      // Skip invalid items
+      if (!item || typeof item !== 'object' || !item.key) {
+        return;
+      }
+
+      const key = String(item.key).trim();
+      const val = item.value ? String(item.value).trim() : '';
+      // Skip empty keys
+      if (!key) {
+        return;
+      }
+      // Add new key to labelKeys
+      if (!this.labelKeys.includes(key)) {
+        this.labelKeys.push(key);
+      }
+      // Initialize value array for this key
+      if (!this.labelMap[key]) {
+        this.labelMap[key] = [];
+      }
+      // Add value to labelMap (with deduplication)
+      if (val && !this.labelMap[key].includes(val)) {
+        this.labelMap[key].push(val);
+      }
+    });
   }
 
+  /**
+   * Handle key change: clear the corresponding value and add new key to 
labelKeys for reuse
+   */
   onKeyChange(index: number, value: string) {
+    // Clear the value when key changes
     this.value[index].value = '';
+
+    // Add new key to labelKeys so other rows can see this custom key
+    if (value && !this.labelKeys.includes(value)) {
+      this.labelKeys.push(value);
+    }
+
+    // Initialize value array for this key
+    if (value && !this.labelMap[value]) {
+      this.labelMap[value] = [];
+    }
+
     this._onChange(this.value);
     this._onTouched();
   }
 
+  /**
+   * Handle value change: add new value to the corresponding key's labelMap 
for reuse
+   */
   onValueChange(index: number, value: string) {
+    const key = this.value[index].key;
+
+    // Add new value to labelMap so other rows can see this custom value
+    if (key && value && this.labelMap[key] && 
!this.labelMap[key].includes(value)) {
+      this.labelMap[key].push(value);
+    }
+
     this._onChange(this.value);
     this._onTouched();
   }
 
+  /**
+   * Handle search event: track the active input to ensure custom options only 
show in the corresponding field
+   */
   onSearch(value: string, index: number, type: 'key' | 'value'): void {
+    // Track the currently active search input
+    this.activeSearchIndex = index;
+    this.activeSearchType = type;
+
     if (value) {
       if (type === 'key') {
         this.customInputKey = value;
       } else {
         this.customInputValue = value;
       }
+    } else {
+      // Clear custom input value when search is empty
+      if (type === 'key') {
+        this.customInputKey = '';
+      } else {
+        this.customInputValue = '';
+      }
     }
   }
 


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

Reply via email to