mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 09:40:58 +00:00
[bugfix&improve] Fix alert label persistence and improve custom label input (#3933)
This commit is contained in:
+36
-4
@@ -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);
|
||||
}
|
||||
|
||||
+44
-34
@@ -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"));
|
||||
}
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
+2
-2
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+1
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -910,6 +910,9 @@
|
||||
field: 'labels',
|
||||
type: 'label-selector'
|
||||
}"
|
||||
[extra]="{
|
||||
labelIsCustom: labelIsCustom
|
||||
}"
|
||||
[name]="'labels'"
|
||||
[(ngModel)]="define.labels"
|
||||
/>
|
||||
|
||||
@@ -577,6 +577,7 @@ export class AlertSettingComponent implements OnInit {
|
||||
userExpr!: string;
|
||||
severity!: string;
|
||||
alertMode!: string;
|
||||
labelIsCustom: boolean = true;
|
||||
logFields: any[] = [];
|
||||
|
||||
editAlertDefine(alertDefineId: number) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user