This is an automated email from the ASF dual-hosted git repository.
zhengqiwei 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 6905bcc986 [feat] Support monitoring center indicator favorites
feature (#3735)
6905bcc986 is described below
commit 6905bcc98612a39a3baf1060e52d7115c84e00b4
Author: Duansg <[email protected]>
AuthorDate: Fri Sep 19 23:53:26 2025 +0800
[feat] Support monitoring center indicator favorites feature (#3735)
Co-authored-by: Calvin <[email protected]>
Co-authored-by: Tom <[email protected]>
---
.../common/entity/manager/MetricsFavorite.java | 70 +++++
.../common/entity/manager/MetricsFavoriteTest.java | 323 +++++++++++++++++++++
.../controller/MetricsFavoriteController.java | 105 +++++++
.../hertzbeat/manager/dao/MetricsFavoriteDao.java | 75 +++++
.../hertzbeat/manager/pojo/dto/MetricsInfo.java | 41 +++
.../hertzbeat/manager/pojo/dto/MonitorDto.java | 2 +-
.../manager/service/MetricsFavoriteService.java | 60 ++++
.../service/impl/MetricsFavoriteServiceImpl.java | 91 ++++++
.../manager/service/impl/MonitorServiceImpl.java | 30 +-
.../manager/support/GlobalExceptionHandler.java | 2 +-
.../controller/MetricsFavoriteControllerTest.java | 252 ++++++++++++++++
.../manager/dao/MetricsFavoriteDaoTest.java | 218 ++++++++++++++
.../impl/MetricsFavoriteServiceImplTest.java | 192 ++++++++++++
.../monitor-data-table.component.html | 12 +-
.../monitor-data-table.component.ts | 16 +-
.../monitor-detail/monitor-detail.component.html | 81 ++++++
.../monitor-detail/monitor-detail.component.less | 73 ++++-
.../monitor-detail/monitor-detail.component.ts | 276 +++++++++++++++++-
web-app/src/app/routes/monitor/monitor.module.ts | 2 +
web-app/src/app/service/monitor.service.ts | 13 +
web-app/src/assets/i18n/en-US.json | 10 +
web-app/src/assets/i18n/zh-CN.json | 10 +
web-app/src/assets/i18n/zh-TW.json | 10 +
23 files changed, 1946 insertions(+), 18 deletions(-)
diff --git
a/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/manager/MetricsFavorite.java
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/manager/MetricsFavorite.java
new file mode 100644
index 0000000000..e1e1f99bd3
--- /dev/null
+++
b/hertzbeat-common/src/main/java/org/apache/hertzbeat/common/entity/manager/MetricsFavorite.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.entity.manager;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import java.time.LocalDateTime;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.CreatedDate;
+
+/**
+ * Metrics Favorite Entity
+ */
+@Entity
+@Table(name = "hzb_metrics_favorite",
+ uniqueConstraints = @UniqueConstraint(columnNames = {"creator",
"monitor_id", "metrics_name"}))
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class MetricsFavorite {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @NotBlank(message = "Creator cannot be null or blank")
+ @Size(max = 255, message = "Creator length cannot exceed 255 characters")
+ @Column(name = "creator", nullable = false)
+ private String creator;
+
+ @NotNull(message = "Monitor ID cannot be null")
+ @Column(name = "monitor_id", nullable = false)
+ private Long monitorId;
+
+ @NotBlank(message = "Metrics name cannot be null or blank")
+ @Size(max = 255, message = "Metrics name length cannot exceed 255
characters")
+ @Column(name = "metrics_name", nullable = false)
+ private String metricsName;
+
+ @CreatedDate
+ @Column(name = "create_time", updatable = false)
+ private LocalDateTime createTime;
+}
\ No newline at end of file
diff --git
a/hertzbeat-common/src/test/java/org/apache/hertzbeat/common/entity/manager/MetricsFavoriteTest.java
b/hertzbeat-common/src/test/java/org/apache/hertzbeat/common/entity/manager/MetricsFavoriteTest.java
new file mode 100644
index 0000000000..2830015792
--- /dev/null
+++
b/hertzbeat-common/src/test/java/org/apache/hertzbeat/common/entity/manager/MetricsFavoriteTest.java
@@ -0,0 +1,323 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.common.entity.manager;
+
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test case for {@link MetricsFavorite}
+ */
+class MetricsFavoriteTest {
+
+ private final ValidatorFactory factory =
Validation.buildDefaultValidatorFactory();
+ private final Validator validator = factory.getValidator();
+
+ @Test
+ void testBuilder() {
+ String creator = "testUser";
+ Long monitorId = 1L;
+ String metricsName = "cpu";
+ LocalDateTime createTime = LocalDateTime.now();
+
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .id(1L)
+ .creator(creator)
+ .monitorId(monitorId)
+ .metricsName(metricsName)
+ .createTime(createTime)
+ .build();
+
+ assertNotNull(favorite);
+ assertEquals(1L, favorite.getId());
+ assertEquals(creator, favorite.getCreator());
+ assertEquals(monitorId, favorite.getMonitorId());
+ assertEquals(metricsName, favorite.getMetricsName());
+ assertEquals(createTime, favorite.getCreateTime());
+ }
+
+ @Test
+ void testBuilderWithoutOptionalFields() {
+ String creator = "testUser";
+ Long monitorId = 1L;
+ String metricsName = "cpu";
+
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .creator(creator)
+ .monitorId(monitorId)
+ .metricsName(metricsName)
+ .build();
+
+ assertNotNull(favorite);
+ assertNull(favorite.getId());
+ assertEquals(creator, favorite.getCreator());
+ assertEquals(monitorId, favorite.getMonitorId());
+ assertEquals(metricsName, favorite.getMetricsName());
+ assertNull(favorite.getCreateTime());
+ }
+
+ @Test
+ void testDefaultConstructor() {
+ MetricsFavorite favorite = new MetricsFavorite();
+
+ assertNotNull(favorite);
+ assertNull(favorite.getId());
+ assertNull(favorite.getCreator());
+ assertNull(favorite.getMonitorId());
+ assertNull(favorite.getMetricsName());
+ assertNull(favorite.getCreateTime());
+ }
+
+ @Test
+ void testSettersAndGetters() {
+ MetricsFavorite favorite = new MetricsFavorite();
+ String creator = "testUser";
+ Long monitorId = 1L;
+ String metricsName = "cpu";
+ LocalDateTime createTime = LocalDateTime.now();
+
+ favorite.setId(1L);
+ favorite.setCreator(creator);
+ favorite.setMonitorId(monitorId);
+ favorite.setMetricsName(metricsName);
+ favorite.setCreateTime(createTime);
+
+ assertEquals(1L, favorite.getId());
+ assertEquals(creator, favorite.getCreator());
+ assertEquals(monitorId, favorite.getMonitorId());
+ assertEquals(metricsName, favorite.getMetricsName());
+ assertEquals(createTime, favorite.getCreateTime());
+ }
+
+ @Test
+ void testValidation_ValidEntity() {
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .creator("testUser")
+ .monitorId(1L)
+ .metricsName("cpu")
+ .createTime(LocalDateTime.now())
+ .build();
+
+ Set<ConstraintViolation<MetricsFavorite>> violations =
validator.validate(favorite);
+
+ assertTrue(violations.isEmpty());
+ }
+
+ @Test
+ void testValidation_NullCreator() {
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .creator(null)
+ .monitorId(1L)
+ .metricsName("cpu")
+ .createTime(LocalDateTime.now())
+ .build();
+
+ Set<ConstraintViolation<MetricsFavorite>> violations =
validator.validate(favorite);
+
+ assertFalse(violations.isEmpty());
+ assertTrue(violations.stream().anyMatch(v ->
v.getPropertyPath().toString().equals("creator")));
+ }
+
+ @Test
+ void testValidation_BlankCreator() {
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .creator(" ")
+ .monitorId(1L)
+ .metricsName("cpu")
+ .createTime(LocalDateTime.now())
+ .build();
+
+ Set<ConstraintViolation<MetricsFavorite>> violations =
validator.validate(favorite);
+
+ assertFalse(violations.isEmpty());
+ assertTrue(violations.stream().anyMatch(v ->
v.getPropertyPath().toString().equals("creator")));
+ }
+
+ @Test
+ void testValidation_CreatorTooLong() {
+ String longCreator = "a".repeat(256);
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .creator(longCreator)
+ .monitorId(1L)
+ .metricsName("cpu")
+ .createTime(LocalDateTime.now())
+ .build();
+
+ Set<ConstraintViolation<MetricsFavorite>> violations =
validator.validate(favorite);
+
+ assertFalse(violations.isEmpty());
+ assertTrue(violations.stream().anyMatch(v ->
v.getPropertyPath().toString().equals("creator")));
+ }
+
+ @Test
+ void testValidation_NullMonitorId() {
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .creator("testUser")
+ .monitorId(null)
+ .metricsName("cpu")
+ .createTime(LocalDateTime.now())
+ .build();
+
+ Set<ConstraintViolation<MetricsFavorite>> violations =
validator.validate(favorite);
+
+ assertFalse(violations.isEmpty());
+ assertTrue(violations.stream().anyMatch(v ->
v.getPropertyPath().toString().equals("monitorId")));
+ }
+
+ @Test
+ void testValidation_NullMetricsName() {
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .creator("testUser")
+ .monitorId(1L)
+ .metricsName(null)
+ .createTime(LocalDateTime.now())
+ .build();
+
+ Set<ConstraintViolation<MetricsFavorite>> violations =
validator.validate(favorite);
+
+ assertFalse(violations.isEmpty());
+ assertTrue(violations.stream().anyMatch(v ->
v.getPropertyPath().toString().equals("metricsName")));
+ }
+
+ @Test
+ void testValidation_BlankMetricsName() {
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .creator("testUser")
+ .monitorId(1L)
+ .metricsName(" ")
+ .createTime(LocalDateTime.now())
+ .build();
+
+ Set<ConstraintViolation<MetricsFavorite>> violations =
validator.validate(favorite);
+
+ assertFalse(violations.isEmpty());
+ assertTrue(violations.stream().anyMatch(v ->
v.getPropertyPath().toString().equals("metricsName")));
+ }
+
+ @Test
+ void testValidation_MetricsNameTooLong() {
+ String longMetricsName = "a".repeat(256);
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .creator("testUser")
+ .monitorId(1L)
+ .metricsName(longMetricsName)
+ .createTime(LocalDateTime.now())
+ .build();
+
+ Set<ConstraintViolation<MetricsFavorite>> violations =
validator.validate(favorite);
+
+ assertFalse(violations.isEmpty());
+ assertTrue(violations.stream().anyMatch(v ->
v.getPropertyPath().toString().equals("metricsName")));
+ }
+
+ @Test
+ void testEqualsAndHashCode() {
+ LocalDateTime now = LocalDateTime.now();
+ MetricsFavorite favorite1 = MetricsFavorite.builder()
+ .id(1L)
+ .creator("testUser")
+ .monitorId(1L)
+ .metricsName("cpu")
+ .createTime(now)
+ .build();
+
+ MetricsFavorite favorite2 = MetricsFavorite.builder()
+ .id(1L)
+ .creator("testUser")
+ .monitorId(1L)
+ .metricsName("cpu")
+ .createTime(now)
+ .build();
+
+ MetricsFavorite favorite3 = MetricsFavorite.builder()
+ .id(2L)
+ .creator("testUser")
+ .monitorId(1L)
+ .metricsName("cpu")
+ .createTime(now)
+ .build();
+
+ assertEquals(favorite1, favorite2);
+ assertEquals(favorite1.hashCode(), favorite2.hashCode());
+ assertNotEquals(favorite1, favorite3);
+ assertNotEquals(favorite1.hashCode(), favorite3.hashCode());
+ }
+
+ @Test
+ void testToString() {
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .id(1L)
+ .creator("testUser")
+ .monitorId(1L)
+ .metricsName("cpu")
+ .createTime(LocalDateTime.now())
+ .build();
+
+ String toString = favorite.toString();
+
+ assertNotNull(toString);
+ assertTrue(toString.contains("MetricsFavorite"));
+ assertTrue(toString.contains("testUser"));
+ assertTrue(toString.contains("cpu"));
+ }
+
+ @Test
+ void testCreatorMaxLength() {
+ String maxLengthCreator = "a".repeat(255);
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .creator(maxLengthCreator)
+ .monitorId(1L)
+ .metricsName("cpu")
+ .createTime(LocalDateTime.now())
+ .build();
+
+ Set<ConstraintViolation<MetricsFavorite>> violations =
validator.validate(favorite);
+
+ assertTrue(violations.isEmpty());
+ assertEquals(255, favorite.getCreator().length());
+ }
+
+ @Test
+ void testMetricsNameMaxLength() {
+ String maxLengthMetricsName = "a".repeat(255);
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .creator("testUser")
+ .monitorId(1L)
+ .metricsName(maxLengthMetricsName)
+ .createTime(LocalDateTime.now())
+ .build();
+
+ Set<ConstraintViolation<MetricsFavorite>> violations =
validator.validate(favorite);
+
+ assertTrue(violations.isEmpty());
+ assertEquals(255, favorite.getMetricsName().length());
+ }
+}
\ No newline at end of file
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MetricsFavoriteController.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MetricsFavoriteController.java
new file mode 100644
index 0000000000..be3950fa26
--- /dev/null
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/controller/MetricsFavoriteController.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.controller;
+
+import com.usthe.sureness.subject.SubjectSum;
+import com.usthe.sureness.util.SurenessContextHolder;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.entity.dto.Message;
+import org.apache.hertzbeat.manager.service.MetricsFavoriteService;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Set;
+
+import static
org.apache.hertzbeat.common.constants.CommonConstants.LOGIN_FAILED_CODE;
+
+/**
+ * Metrics Favorite Controller
+ */
+@Tag(name = "Metrics Favorite API")
+@RestController
+@RequestMapping(path = "/api/metrics/favorite")
+@RequiredArgsConstructor
+@Slf4j
+public class MetricsFavoriteController {
+
+ private final MetricsFavoriteService metricsFavoriteService;
+
+ @PostMapping("/{monitorId}/{metricsName}")
+ @Operation(summary = "Add metrics to favorites", description = "Add
specific metrics to user's favorites")
+ public ResponseEntity<Message<Void>> addMetricsFavorite(
+ @Parameter(description = "Monitor ID", example = "6565463543")
@PathVariable Long monitorId,
+ @Parameter(description = "Metrics name", example = "cpu")
@PathVariable String metricsName) {
+ String user = getCurrentUser();
+ if (user == null) {
+ return ResponseEntity.ok(Message.fail(LOGIN_FAILED_CODE, "User not
authenticated"));
+ }
+ metricsFavoriteService.addMetricsFavorite(user, monitorId,
metricsName);
+ return ResponseEntity.ok(Message.success("Metrics added to favorites
successfully"));
+ }
+
+ @DeleteMapping("/{monitorId}/{metricsName}")
+ @Operation(summary = "Remove metrics from favorites", description =
"Remove specific metrics from user's favorites")
+ public ResponseEntity<Message<Void>> removeMetricsFavorite(
+ @Parameter(description = "Monitor ID", example = "6565463543")
@PathVariable Long monitorId,
+ @Parameter(description = "Metrics name", example = "cpu")
@PathVariable String metricsName) {
+
+ String user = getCurrentUser();
+ if (user == null) {
+ return ResponseEntity.ok(Message.fail(LOGIN_FAILED_CODE, "User not
authenticated"));
+ }
+ metricsFavoriteService.removeMetricsFavorite(user, monitorId,
metricsName);
+ return ResponseEntity.ok(Message.success("Metrics removed from
favorites successfully"));
+ }
+
+ @GetMapping("/{monitorId}")
+ @Operation(summary = "Get user's all favorited metrics", description =
"Get all favorited metrics for current user")
+ public ResponseEntity<Message<Set<String>>>
getUserFavoritedMetrics(@Parameter(description = "Monitor ID", example =
"6565463543") @PathVariable Long monitorId) {
+ String user = getCurrentUser();
+ if (user == null) {
+ return ResponseEntity.ok(Message.fail(LOGIN_FAILED_CODE, "User not
authenticated"));
+ }
+ Set<String> favoritedMetrics =
metricsFavoriteService.getUserFavoritedMetrics(user, monitorId);
+ return ResponseEntity.ok(Message.success(favoritedMetrics));
+ }
+
+ /**
+ * Get current user ID for favorite status
+ *
+ * @return user id
+ */
+ private String getCurrentUser() {
+ try {
+ SubjectSum subjectSum = SurenessContextHolder.getBindSubject();
+ return String.valueOf(subjectSum.getPrincipal());
+ } catch (Exception e) {
+ log.error("No user found, favorites will be disabled");
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/MetricsFavoriteDao.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/MetricsFavoriteDao.java
new file mode 100644
index 0000000000..a183133685
--- /dev/null
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/dao/MetricsFavoriteDao.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.dao;
+
+import org.apache.hertzbeat.common.entity.manager.MetricsFavorite;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * MetricsFavorite dao
+ */
+public interface MetricsFavoriteDao extends JpaRepository<MetricsFavorite,
Long> {
+
+ /**
+ * Find metrics favorite by creator and monitor id and metrics name
+ *
+ * @param creator user id
+ * @param monitorId monitor id
+ * @param metricsName metrics name
+ * @return optional metrics favorite
+ */
+ Optional<MetricsFavorite> findByCreatorAndMonitorIdAndMetricsName(String
creator, Long monitorId, String metricsName);
+
+ /**
+ * Find all metrics favorites by user id and monitor id
+ *
+ * @param creator user id
+ * @param monitorId monitor id
+ * @return list of metrics favorites
+ */
+ List<MetricsFavorite> findByCreatorAndMonitorId(String creator, Long
monitorId);
+
+ /**
+ * Delete metrics favorite by user id and monitor id and metrics name
+ *
+ * @param creator user id
+ * @param monitorId monitor id
+ * @param metricsName metrics name
+ */
+ @Modifying
+ @Query("DELETE FROM MetricsFavorite mf WHERE mf.creator = :creator AND
mf.monitorId = :monitorId AND mf.metricsName = :metricsName")
+ void deleteByUserIdAndMonitorIdAndMetricsName(@Param("creator") String
creator,
+ @Param("monitorId") Long
monitorId,
+ @Param("metricsName")
String metricsName);
+
+ /**
+ * Delete metrics favorites by monitor ids
+ *
+ * @param monitorIds monitor ids
+ */
+ @Modifying
+ @Query("DELETE FROM MetricsFavorite mf WHERE mf.monitorId IN :monitorIds")
+ void deleteFavoritesByMonitorIdIn(@Param("monitorIds") Set<Long>
monitorIds);
+}
\ No newline at end of file
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MetricsInfo.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MetricsInfo.java
new file mode 100644
index 0000000000..217ab541e1
--- /dev/null
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MetricsInfo.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.pojo.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Metrics Information with favorite status
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(description = "Metrics information with favorite status")
+public class MetricsInfo {
+
+ @Schema(description = "Metrics name", example = "cpu")
+ private String name;
+
+ @Schema(description = "Whether the metrics is favorited by current user")
+ private Boolean favorited;
+}
\ No newline at end of file
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDto.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDto.java
index be292fb393..6832c00c69 100644
---
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDto.java
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/pojo/dto/MonitorDto.java
@@ -47,7 +47,7 @@ public class MonitorDto {
private List<Param> params;
@Schema(description = "Monitor Metrics", accessMode = READ_ONLY)
- private List<String> metrics;
+ private List<MetricsInfo> metrics;
@Schema(description = "pinned collector, default null if system dispatch",
accessMode = READ_WRITE)
private String collector;
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/MetricsFavoriteService.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/MetricsFavoriteService.java
new file mode 100644
index 0000000000..8971cd3125
--- /dev/null
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/MetricsFavoriteService.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service;
+
+import java.util.Set;
+
+/**
+ * Metrics Favorite Service
+ */
+public interface MetricsFavoriteService {
+
+ /**
+ * Add metrics to favorites
+ *
+ * @param creator user id
+ * @param monitorId monitor id
+ * @param metricsName metrics name
+ */
+ void addMetricsFavorite(String creator, Long monitorId, String
metricsName);
+
+ /**
+ * Remove metrics from favorites
+ *
+ * @param userId user id
+ * @param monitorId monitor id
+ * @param metricsName metrics name
+ */
+ void removeMetricsFavorite(String userId, Long monitorId, String
metricsName);
+
+ /**
+ * Get user's favorited metrics names for a specific monitor
+ *
+ * @param userId user id
+ * @param monitorId monitor id
+ * @return set of favorited metrics names
+ */
+ Set<String> getUserFavoritedMetrics(String userId, Long monitorId);
+
+ /**
+ * Remove metrics from monitor ids
+ *
+ * @param monitorIds monitor ids
+ */
+ void deleteFavoritesByMonitorIdIn(Set<Long> monitorIds);
+}
\ No newline at end of file
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MetricsFavoriteServiceImpl.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MetricsFavoriteServiceImpl.java
new file mode 100644
index 0000000000..1129dfb7d7
--- /dev/null
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/MetricsFavoriteServiceImpl.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hertzbeat.common.entity.manager.MetricsFavorite;
+import org.apache.hertzbeat.manager.dao.MetricsFavoriteDao;
+import org.apache.hertzbeat.manager.service.MetricsFavoriteService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Metrics Favorite Service Implementation
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional(rollbackFor = Exception.class)
+public class MetricsFavoriteServiceImpl implements MetricsFavoriteService {
+
+ private final MetricsFavoriteDao metricsFavoriteDao;
+
+ @Override
+ public void addMetricsFavorite(String creator, Long monitorId, String
metricsName) {
+ Optional<MetricsFavorite> existing = metricsFavoriteDao
+ .findByCreatorAndMonitorIdAndMetricsName(creator, monitorId,
metricsName);
+ if (existing.isPresent()) {
+ throw new RuntimeException("Metrics favorite already exists: " +
metricsName);
+ }
+ MetricsFavorite favorite = MetricsFavorite.builder()
+ .creator(creator)
+ .monitorId(monitorId)
+ .metricsName(metricsName)
+ .createTime(LocalDateTime.now())
+ .build();
+ metricsFavoriteDao.save(favorite);
+ }
+
+ @Override
+ public void removeMetricsFavorite(String userId, Long monitorId, String
metricsName) {
+ metricsFavoriteDao.deleteByUserIdAndMonitorIdAndMetricsName(userId,
monitorId, metricsName);
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public Set<String> getUserFavoritedMetrics(String userId, Long monitorId) {
+ if (null == userId || null == monitorId) {
+ return Set.of();
+ }
+ List<MetricsFavorite> favorites =
metricsFavoriteDao.findByCreatorAndMonitorId(userId, monitorId);
+ if (null == favorites || favorites.isEmpty()) {
+ return Set.of();
+ }
+ return favorites.stream()
+ .map(MetricsFavorite::getMetricsName)
+ .filter(StringUtils::isNotBlank)
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public void deleteFavoritesByMonitorIdIn(Set<Long> monitorIds) {
+ if (null == monitorIds || monitorIds.isEmpty()) {
+ return;
+ }
+ metricsFavoriteDao.deleteFavoritesByMonitorIdIn(monitorIds);
+ }
+
+}
\ No newline at end of file
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 ab18501937..d76b4537ec 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
@@ -19,6 +19,8 @@ package org.apache.hertzbeat.manager.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.collect.Sets;
+import com.usthe.sureness.subject.SubjectSum;
+import com.usthe.sureness.util.SurenessContextHolder;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Predicate;
import jakarta.servlet.http.HttpServletResponse;
@@ -59,11 +61,13 @@ import org.apache.hertzbeat.manager.dao.MonitorBindDao;
import org.apache.hertzbeat.manager.dao.MonitorDao;
import org.apache.hertzbeat.manager.dao.ParamDao;
import org.apache.hertzbeat.manager.pojo.dto.AppCount;
+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.ImExportService;
import org.apache.hertzbeat.manager.service.LabelService;
+import org.apache.hertzbeat.manager.service.MetricsFavoriteService;
import org.apache.hertzbeat.manager.service.MonitorService;
import org.apache.hertzbeat.manager.support.exception.MonitorDatabaseException;
import org.apache.hertzbeat.manager.support.exception.MonitorDetectException;
@@ -138,6 +142,8 @@ public class MonitorServiceImpl implements MonitorService {
private LabelDao labelDao;
@Autowired
private LabelService labelService;
+ @Autowired
+ private MetricsFavoriteService metricsFavoriteService;
public MonitorServiceImpl(List<ImExportService> imExportServiceList) {
imExportServiceList.forEach(it -> imExportServiceMap.put(it.type(),
it));
@@ -575,6 +581,7 @@ public class MonitorServiceImpl implements MonitorService {
Set<Long> monitorIds =
monitors.stream().map(Monitor::getId).collect(Collectors.toSet());
alertDefineBindDao.deleteAlertDefineMonitorBindsByMonitorIdIn(monitorIds);
monitorBindDao.deleteMonitorBindByBizIdIn(monitorIds);
+ metricsFavoriteService.deleteFavoritesByMonitorIdIn(monitorIds);
for (Monitor monitor : monitors) {
monitorBindDao.deleteByMonitorId(monitor.getId());
collectorMonitorBindDao.deleteCollectorMonitorBindsByMonitorId(monitor.getId());
@@ -589,24 +596,37 @@ public class MonitorServiceImpl implements MonitorService
{
public MonitorDto getMonitorDto(long id) throws RuntimeException {
Optional<Monitor> monitorOptional = monitorDao.findById(id);
if (monitorOptional.isPresent()) {
+ // Get current user ID for favorite status
+ String currentUserId = null;
+ try {
+ SubjectSum subjectSum = SurenessContextHolder.getBindSubject();
+ currentUserId = String.valueOf(subjectSum.getPrincipal());
+ } catch (Exception e) {
+ log.debug("No user context found, favorites will be disabled");
+ }
+ Set<String> favoritedMetrics =
metricsFavoriteService.getUserFavoritedMetrics(currentUserId, id);
+
Monitor monitor = monitorOptional.get();
MonitorDto monitorDto = new MonitorDto();
List<Param> params = paramDao.findParamsByMonitorId(id);
monitorDto.setParams(params);
+ List<MetricsInfo> metricsInfos;
if
(DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(monitor.getApp()) ||
monitor.getType() == CommonConstants.MONITOR_TYPE_PUSH_AUTO_CREATE) {
List<CollectRep.MetricsData> metricsDataList =
warehouseService.queryMonitorMetricsData(id);
- List<String> metrics =
metricsDataList.stream().map(CollectRep.MetricsData::getMetrics).collect(Collectors.toList());
- monitorDto.setMetrics(metrics);
+ metricsInfos = metricsDataList.stream()
+ .map(t ->
MetricsInfo.builder().name(t.getMetrics()).favorited(favoritedMetrics.contains(t.getMetrics())).build())
+ .collect(Collectors.toList());
monitorDto.setGrafanaDashboard(dashboardService.getDashboardByMonitorId(id));
} else {
boolean isStatic =
CommonConstants.SCRAPE_STATIC.equals(monitor.getScrape()) ||
!StringUtils.hasText(monitor.getScrape());
String type = isStatic ? monitor.getApp() :
monitor.getScrape();
Job job = appService.getAppDefine(type);
- List<String> metrics = job.getMetrics().stream()
+ metricsInfos = job.getMetrics().stream()
.filter(Metrics::isVisible)
- .map(Metrics::getName).collect(Collectors.toList());
- monitorDto.setMetrics(metrics);
+ .map(t ->
MetricsInfo.builder().name(t.getName()).favorited(favoritedMetrics.contains(t.getName())).build())
+ .collect(Collectors.toList());
}
+ monitorDto.setMetrics(metricsInfos);
monitorDto.setMonitor(monitor);
Optional<CollectorMonitorBind> bindOptional =
collectorMonitorBindDao.findCollectorMonitorBindByMonitorId(monitor.getId());
bindOptional.ifPresent(bind ->
monitorDto.setCollector(bind.getCollector()));
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/GlobalExceptionHandler.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/GlobalExceptionHandler.java
index eb51c3a198..403302ae5f 100644
---
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/GlobalExceptionHandler.java
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/support/GlobalExceptionHandler.java
@@ -251,6 +251,6 @@ public class GlobalExceptionHandler {
}
log.error("[monitor]-[unknown error happen]-{}", errorMessage,
exception);
Message<Void> message = Message.fail(MONITOR_CONFLICT_CODE,
errorMessage);
- return ResponseEntity.status(HttpStatus.CONFLICT).body(message);
+ return
ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(message);
}
}
diff --git
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MetricsFavoriteControllerTest.java
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MetricsFavoriteControllerTest.java
new file mode 100644
index 0000000000..e784f1a11d
--- /dev/null
+++
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/controller/MetricsFavoriteControllerTest.java
@@ -0,0 +1,252 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.controller;
+
+import com.usthe.sureness.subject.SubjectSum;
+import com.usthe.sureness.util.SurenessContextHolder;
+import org.apache.hertzbeat.common.constants.CommonConstants;
+import org.apache.hertzbeat.manager.service.MetricsFavoriteService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.MediaType;
+import
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.Set;
+
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Test case for {@link MetricsFavoriteController}
+ */
+@ExtendWith(MockitoExtension.class)
+class MetricsFavoriteControllerTest {
+
+ private MockMvc mockMvc;
+
+ @Mock
+ private MetricsFavoriteService metricsFavoriteService;
+
+ @InjectMocks
+ private MetricsFavoriteController metricsFavoriteController;
+
+ private final Long testMonitorId = 1L;
+ private final String testMetricsName = "cpu";
+ private final String testUserId = "testUser";
+
+ @BeforeEach
+ void setUp() {
+ this.mockMvc =
MockMvcBuilders.standaloneSetup(metricsFavoriteController)
+ .setMessageConverters(new
MappingJackson2HttpMessageConverter())
+ .build();
+ }
+
+ @Test
+ void testAddMetricsFavoriteSuccess() throws Exception {
+ SubjectSum subjectSum = mock(SubjectSum.class);
+ when(subjectSum.getPrincipal()).thenReturn(testUserId);
+ try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+
doNothing().when(metricsFavoriteService).addMetricsFavorite(testUserId,
testMonitorId, testMetricsName);
+
+
mockMvc.perform(post("/api/metrics/favorite/{monitorId}/{metricsName}",
testMonitorId, testMetricsName)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value((int)
CommonConstants.SUCCESS_CODE))
+ .andExpect(jsonPath("$.msg").value("Metrics added to
favorites successfully"));
+
+ verify(metricsFavoriteService).addMetricsFavorite(testUserId,
testMonitorId, testMetricsName);
+ }
+ }
+
+ @Test
+ void testAddMetricsFavoriteAlreadyExists() throws Exception {
+ SubjectSum subjectSum = mock(SubjectSum.class);
+ when(subjectSum.getPrincipal()).thenReturn(testUserId);
+ try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+ doThrow(new RuntimeException("Metrics favorite already exists: " +
testMetricsName))
+
.when(metricsFavoriteService).addMetricsFavorite(testUserId, testMonitorId,
testMetricsName);
+
+
mockMvc.perform(post("/api/metrics/favorite/{monitorId}/{metricsName}",
testMonitorId, testMetricsName)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value((int)
CommonConstants.FAIL_CODE))
+ .andExpect(jsonPath("$.msg").value("Add failed! Metrics
favorite already exists: " + testMetricsName));
+
+ verify(metricsFavoriteService).addMetricsFavorite(testUserId,
testMonitorId, testMetricsName);
+ }
+ }
+
+ @Test
+ void testAddMetricsFavoriteMissingParameters() throws Exception {
+ mockMvc.perform(post("/api/metrics/favorite")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNotFound());
+
+ verify(metricsFavoriteService,
never()).addMetricsFavorite(anyString(), anyLong(), anyString());
+ }
+
+ @Test
+ void testRemoveMetricsFavoriteSuccess() throws Exception {
+ SubjectSum subjectSum = mock(SubjectSum.class);
+ when(subjectSum.getPrincipal()).thenReturn(testUserId);
+ try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+ // Given
+
doNothing().when(metricsFavoriteService).removeMetricsFavorite(testUserId,
testMonitorId, testMetricsName);
+
+ // When & Then
+
mockMvc.perform(delete("/api/metrics/favorite/{monitorId}/{metricsName}",
testMonitorId, testMetricsName)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value((int)
CommonConstants.SUCCESS_CODE))
+ .andExpect(jsonPath("$.msg").value("Metrics removed from
favorites successfully"));
+
+ verify(metricsFavoriteService).removeMetricsFavorite(testUserId,
testMonitorId, testMetricsName);
+ }
+ }
+
+ @Test
+ void testRemoveMetricsFavoriteException() throws Exception {
+ SubjectSum subjectSum = mock(SubjectSum.class);
+ when(subjectSum.getPrincipal()).thenReturn(testUserId);
+ try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+ doThrow(new RuntimeException("Database error"))
+
.when(metricsFavoriteService).removeMetricsFavorite(testUserId, testMonitorId,
testMetricsName);
+
+
mockMvc.perform(delete("/api/metrics/favorite/{monitorId}/{metricsName}",
testMonitorId, testMetricsName)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value((int)
CommonConstants.FAIL_CODE))
+ .andExpect(jsonPath("$.msg").value("Remove failed!
Database error"));
+
+ verify(metricsFavoriteService).removeMetricsFavorite(testUserId,
testMonitorId, testMetricsName);
+ }
+ }
+
+ @Test
+ void testGetUserFavoritedMetricsSuccess() throws Exception {
+ SubjectSum subjectSum = mock(SubjectSum.class);
+ when(subjectSum.getPrincipal()).thenReturn(testUserId);
+ try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+ Set<String> favoriteMetrics = Set.of("cpu", "memory", "disk");
+ when(metricsFavoriteService.getUserFavoritedMetrics(testUserId,
testMonitorId))
+ .thenReturn(favoriteMetrics);
+
+ mockMvc.perform(get("/api/metrics/favorite/{monitorId}",
testMonitorId)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value((int)
CommonConstants.SUCCESS_CODE))
+ .andExpect(jsonPath("$.data").isArray())
+ .andExpect(jsonPath("$.data.length()").value(3));
+
+ verify(metricsFavoriteService).getUserFavoritedMetrics(testUserId,
testMonitorId);
+ }
+ }
+
+ @Test
+ void testGetUserFavoritedMetricsEmptyResult() throws Exception {
+ SubjectSum subjectSum = mock(SubjectSum.class);
+ when(subjectSum.getPrincipal()).thenReturn(testUserId);
+ try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+ Set<String> favoriteMetrics = Set.of();
+ when(metricsFavoriteService.getUserFavoritedMetrics(testUserId,
testMonitorId))
+ .thenReturn(favoriteMetrics);
+
+ mockMvc.perform(get("/api/metrics/favorite/{monitorId}",
testMonitorId)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value((int)
CommonConstants.SUCCESS_CODE))
+ .andExpect(jsonPath("$.data").isArray())
+ .andExpect(jsonPath("$.data.length()").value(0));
+
+ verify(metricsFavoriteService).getUserFavoritedMetrics(testUserId,
testMonitorId);
+ }
+ }
+
+ @Test
+ void testGetUserFavoritedMetricsException() throws Exception {
+ SubjectSum subjectSum = mock(SubjectSum.class);
+ when(subjectSum.getPrincipal()).thenReturn(testUserId);
+ try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(subjectSum);
+
+ when(metricsFavoriteService.getUserFavoritedMetrics(testUserId,
testMonitorId))
+ .thenThrow(new RuntimeException("Service error"));
+
+ mockMvc.perform(get("/api/metrics/favorite/{monitorId}",
testMonitorId)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value((int)
CommonConstants.FAIL_CODE))
+ .andExpect(jsonPath("$.msg").value("Failed to get
favorited metrics!"));
+
+ verify(metricsFavoriteService).getUserFavoritedMetrics(testUserId,
testMonitorId);
+ }
+ }
+
+ @Test
+ void testGetUserFavoritedMetricsMissingMonitorId() throws Exception {
+ mockMvc.perform(get("/api/metrics/favorite")
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isNotFound());
+
+ verify(metricsFavoriteService,
never()).getUserFavoritedMetrics(anyString(), anyLong());
+ }
+
+ @Test
+ void testUnauthenticatedAccess() throws Exception {
+ try (var mockedStatic = mockStatic(SurenessContextHolder.class)) {
+
mockedStatic.when(SurenessContextHolder::getBindSubject).thenReturn(null);
+
mockMvc.perform(post("/api/metrics/favorite/{monitorId}/{metricsName}",
testMonitorId, testMetricsName)
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value((int)
CommonConstants.LOGIN_FAILED_CODE))
+ .andExpect(jsonPath("$.msg").value("User not
authenticated"));
+
+ verify(metricsFavoriteService,
never()).addMetricsFavorite(anyString(), anyLong(), anyString());
+ }
+ }
+}
\ No newline at end of file
diff --git
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/dao/MetricsFavoriteDaoTest.java
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/dao/MetricsFavoriteDaoTest.java
new file mode 100644
index 0000000000..5d42dd4ada
--- /dev/null
+++
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/dao/MetricsFavoriteDaoTest.java
@@ -0,0 +1,218 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.dao;
+
+import jakarta.annotation.Resource;
+import org.apache.hertzbeat.common.entity.manager.MetricsFavorite;
+import org.apache.hertzbeat.manager.AbstractSpringIntegrationTest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.annotation.Rollback;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Test case for {@link MetricsFavoriteDao}
+ */
+@Transactional
+class MetricsFavoriteDaoTest extends AbstractSpringIntegrationTest {
+
+ @Resource
+ private MetricsFavoriteDao metricsFavoriteDao;
+
+ private MetricsFavorite testFavorite1;
+ private MetricsFavorite testFavorite2;
+ private MetricsFavorite testFavorite3;
+
+ private final String testCreator1 = "user1";
+ private final String testCreator2 = "user2";
+ private final Long testMonitorId1 = 1L;
+ private final Long testMonitorId2 = 2L;
+ private final String testMetricsName1 = "cpu";
+ private final String testMetricsName2 = "memory";
+
+ @BeforeEach
+ void setUp() {
+ testFavorite1 = MetricsFavorite.builder()
+ .creator(testCreator1)
+ .monitorId(testMonitorId1)
+ .metricsName(testMetricsName1)
+ .createTime(LocalDateTime.now())
+ .build();
+ testFavorite2 = MetricsFavorite.builder()
+ .creator(testCreator1)
+ .monitorId(testMonitorId1)
+ .metricsName(testMetricsName2)
+ .createTime(LocalDateTime.now())
+ .build();
+ testFavorite3 = MetricsFavorite.builder()
+ .creator(testCreator2)
+ .monitorId(testMonitorId2)
+ .metricsName(testMetricsName1)
+ .createTime(LocalDateTime.now())
+ .build();
+ }
+
+ @AfterEach
+ void tearDown() {
+ metricsFavoriteDao.deleteAll();
+ }
+
+ @Test
+ void testSaveAndFindById() {
+ MetricsFavorite saved = metricsFavoriteDao.saveAndFlush(testFavorite1);
+
+ assertNotNull(saved.getId());
+ Optional<MetricsFavorite> found =
metricsFavoriteDao.findById(saved.getId());
+ assertTrue(found.isPresent());
+ assertEquals(testCreator1, found.get().getCreator());
+ assertEquals(testMonitorId1, found.get().getMonitorId());
+ assertEquals(testMetricsName1, found.get().getMetricsName());
+ }
+
+ @Test
+ void testFindByCreatorAndMonitorIdAndMetricsName() {
+ metricsFavoriteDao.saveAndFlush(testFavorite1);
+
+ Optional<MetricsFavorite> found = metricsFavoriteDao
+ .findByCreatorAndMonitorIdAndMetricsName(testCreator1,
testMonitorId1, testMetricsName1);
+
+ assertTrue(found.isPresent());
+ assertEquals(testCreator1, found.get().getCreator());
+ assertEquals(testMonitorId1, found.get().getMonitorId());
+ assertEquals(testMetricsName1, found.get().getMetricsName());
+ }
+
+ @Test
+ void testFindByCreatorAndMonitorIdAndMetricsNameNotFound() {
+ Optional<MetricsFavorite> found =
metricsFavoriteDao.findByCreatorAndMonitorIdAndMetricsName("nonexistent", 999L,
"nonexistent");
+ assertFalse(found.isPresent());
+ }
+
+ @Test
+ void testFindByCreatorAndMonitorId() {
+ metricsFavoriteDao.saveAndFlush(testFavorite1);
+ metricsFavoriteDao.saveAndFlush(testFavorite2);
+ metricsFavoriteDao.saveAndFlush(testFavorite3);
+
+ List<MetricsFavorite> found =
metricsFavoriteDao.findByCreatorAndMonitorId(testCreator1, testMonitorId1);
+
+ assertNotNull(found);
+ assertEquals(2, found.size());
+ assertTrue(found.stream().allMatch(f ->
f.getCreator().equals(testCreator1)));
+ assertTrue(found.stream().allMatch(f ->
f.getMonitorId().equals(testMonitorId1)));
+ assertTrue(found.stream().anyMatch(f ->
f.getMetricsName().equals(testMetricsName1)));
+ assertTrue(found.stream().anyMatch(f ->
f.getMetricsName().equals(testMetricsName2)));
+ }
+
+ @Test
+ void testFindByCreatorAndMonitorIdEmptyResult() {
+ List<MetricsFavorite> found =
metricsFavoriteDao.findByCreatorAndMonitorId("nonexistent", 999L);
+ assertNotNull(found);
+ assertTrue(found.isEmpty());
+ }
+
+ @Test
+ void testDeleteByUserIdAndMonitorIdAndMetricsName() {
+ MetricsFavorite saved = metricsFavoriteDao.saveAndFlush(testFavorite1);
+
+ assertTrue(metricsFavoriteDao.findById(saved.getId()).isPresent());
+
+
metricsFavoriteDao.deleteByUserIdAndMonitorIdAndMetricsName(testCreator1,
testMonitorId1, testMetricsName1);
+ assertFalse(metricsFavoriteDao.findById(saved.getId()).isPresent());
+ }
+
+ @Test
+ void testDeleteByUserIdAndMonitorIdAndMetricsNameNotExists() {
+ // When - Should not throw exception even if record doesn't exist
+ assertDoesNotThrow(() -> {
+
metricsFavoriteDao.deleteByUserIdAndMonitorIdAndMetricsName("nonexistent",
999L, "nonexistent");
+ });
+ }
+
+ @Test
+ void testDeleteFavoritesByMonitorIdIn() {
+ MetricsFavorite saved1 =
metricsFavoriteDao.saveAndFlush(testFavorite1);
+ MetricsFavorite saved2 =
metricsFavoriteDao.saveAndFlush(testFavorite2);
+ MetricsFavorite saved3 =
metricsFavoriteDao.saveAndFlush(testFavorite3);
+
+ assertEquals(3, metricsFavoriteDao.findAll().size());
+
+ Set<Long> monitorIds = Set.of(testMonitorId1, testMonitorId2);
+ metricsFavoriteDao.deleteFavoritesByMonitorIdIn(monitorIds);
+
+ assertEquals(0, metricsFavoriteDao.findAll().size());
+ assertFalse(metricsFavoriteDao.findById(saved1.getId()).isPresent());
+ assertFalse(metricsFavoriteDao.findById(saved2.getId()).isPresent());
+ assertFalse(metricsFavoriteDao.findById(saved3.getId()).isPresent());
+ }
+
+ @Test
+ void testDeleteFavoritesByMonitorIdInPartialDelete() {
+ MetricsFavorite saved1 =
metricsFavoriteDao.saveAndFlush(testFavorite1);
+ MetricsFavorite saved3 =
metricsFavoriteDao.saveAndFlush(testFavorite3);
+
+ Set<Long> monitorIds = Set.of(testMonitorId1);
+ metricsFavoriteDao.deleteFavoritesByMonitorIdIn(monitorIds);
+
+ assertEquals(1, metricsFavoriteDao.findAll().size());
+ assertFalse(metricsFavoriteDao.findById(saved1.getId()).isPresent());
+ assertTrue(metricsFavoriteDao.findById(saved3.getId()).isPresent());
+ }
+
+ @Test
+ void testDeleteFavoritesByMonitorIdInNonExistentIds() {
+ metricsFavoriteDao.saveAndFlush(testFavorite1);
+
+ Set<Long> nonExistentIds = Set.of(999L, 1000L);
+ metricsFavoriteDao.deleteFavoritesByMonitorIdIn(nonExistentIds);
+
+ assertEquals(1, metricsFavoriteDao.findAll().size());
+ }
+
+ @Test
+ @Rollback(false)
+ void testUniqueConstraint() {
+ metricsFavoriteDao.saveAndFlush(testFavorite1);
+
+ MetricsFavorite duplicate = MetricsFavorite.builder()
+ .creator(testCreator1)
+ .monitorId(testMonitorId1)
+ .metricsName(testMetricsName1)
+ .createTime(LocalDateTime.now()).build();
+
+ assertThrows(java.sql.SQLException.class, () -> {
+ metricsFavoriteDao.saveAndFlush(duplicate);
+ });
+
+ // Clean up the test data
+ metricsFavoriteDao.deleteAll();
+ }
+}
\ No newline at end of file
diff --git
a/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/impl/MetricsFavoriteServiceImplTest.java
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/impl/MetricsFavoriteServiceImplTest.java
new file mode 100644
index 0000000000..9e500fda49
--- /dev/null
+++
b/hertzbeat-manager/src/test/java/org/apache/hertzbeat/manager/service/impl/MetricsFavoriteServiceImplTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hertzbeat.manager.service.impl;
+
+import org.apache.hertzbeat.common.entity.manager.MetricsFavorite;
+import org.apache.hertzbeat.manager.dao.MetricsFavoriteDao;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test case for {@link MetricsFavoriteServiceImpl}
+ */
+@ExtendWith(MockitoExtension.class)
+class MetricsFavoriteServiceImplTest {
+
+ @Mock
+ private MetricsFavoriteDao metricsFavoriteDao;
+
+ @InjectMocks
+ private MetricsFavoriteServiceImpl metricsFavoriteService;
+
+ private MetricsFavorite testFavorite;
+ private final String testCreator = "testUser";
+ private final Long testMonitorId = 1L;
+ private final String testMetricsName = "cpu";
+
+ @BeforeEach
+ void setUp() {
+ testFavorite = MetricsFavorite.builder()
+ .id(1L)
+ .creator(testCreator)
+ .monitorId(testMonitorId)
+ .metricsName(testMetricsName)
+ .createTime(LocalDateTime.now())
+ .build();
+ }
+
+ @Test
+ void testAddMetricsFavoriteSuccess() {
+
when(metricsFavoriteDao.findByCreatorAndMonitorIdAndMetricsName(testCreator,
testMonitorId, testMetricsName))
+ .thenReturn(Optional.empty());
+
when(metricsFavoriteDao.save(any(MetricsFavorite.class))).thenReturn(testFavorite);
+
+ assertDoesNotThrow(() ->
metricsFavoriteService.addMetricsFavorite(testCreator, testMonitorId,
testMetricsName));
+
+
verify(metricsFavoriteDao).findByCreatorAndMonitorIdAndMetricsName(testCreator,
testMonitorId, testMetricsName);
+ verify(metricsFavoriteDao).save(any(MetricsFavorite.class));
+ }
+
+ @Test
+ void testAddMetricsFavoriteAlreadyExists() {
+
when(metricsFavoriteDao.findByCreatorAndMonitorIdAndMetricsName(testCreator,
testMonitorId, testMetricsName))
+ .thenReturn(Optional.of(testFavorite));
+
+ RuntimeException exception = assertThrows(RuntimeException.class,
+ () -> metricsFavoriteService.addMetricsFavorite(testCreator,
testMonitorId, testMetricsName));
+
+ assertEquals("Metrics favorite already exists: " + testMetricsName,
exception.getMessage());
+
verify(metricsFavoriteDao).findByCreatorAndMonitorIdAndMetricsName(testCreator,
testMonitorId, testMetricsName);
+ verify(metricsFavoriteDao, never()).save(any(MetricsFavorite.class));
+ }
+
+ @Test
+ void testRemoveMetricsFavorite() {
+ metricsFavoriteService.removeMetricsFavorite(testCreator,
testMonitorId, testMetricsName);
+
verify(metricsFavoriteDao).deleteByUserIdAndMonitorIdAndMetricsName(testCreator,
testMonitorId, testMetricsName);
+ }
+
+ @Test
+ void testGetUserFavoritedMetricsWithData() {
+ MetricsFavorite favorite1 = MetricsFavorite.builder()
+ .creator(testCreator)
+ .monitorId(testMonitorId)
+ .metricsName("cpu")
+ .build();
+ MetricsFavorite favorite2 = MetricsFavorite.builder()
+ .creator(testCreator)
+ .monitorId(testMonitorId)
+ .metricsName("memory")
+ .build();
+ List<MetricsFavorite> favorites = Arrays.asList(favorite1, favorite2);
+
+ when(metricsFavoriteDao.findByCreatorAndMonitorId(testCreator,
testMonitorId))
+ .thenReturn(favorites);
+
+ Set<String> result =
metricsFavoriteService.getUserFavoritedMetrics(testCreator, testMonitorId);
+
+ assertNotNull(result);
+ assertEquals(2, result.size());
+ assertTrue(result.contains("cpu"));
+ assertTrue(result.contains("memory"));
+ verify(metricsFavoriteDao).findByCreatorAndMonitorId(testCreator,
testMonitorId);
+ }
+
+ @Test
+ void testGetUserFavoritedMetricsEmptyData() {
+ when(metricsFavoriteDao.findByCreatorAndMonitorId(testCreator,
testMonitorId))
+ .thenReturn(List.of());
+
+ Set<String> result =
metricsFavoriteService.getUserFavoritedMetrics(testCreator, testMonitorId);
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ verify(metricsFavoriteDao).findByCreatorAndMonitorId(testCreator,
testMonitorId);
+ }
+
+ @Test
+ void testGetUserFavoritedMetricsNullData() {
+ when(metricsFavoriteDao.findByCreatorAndMonitorId(testCreator,
testMonitorId))
+ .thenReturn(null);
+
+ Set<String> result =
metricsFavoriteService.getUserFavoritedMetrics(testCreator, testMonitorId);
+
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ verify(metricsFavoriteDao).findByCreatorAndMonitorId(testCreator,
testMonitorId);
+ }
+
+ @Test
+ void testGetUserFavoritedMetricsFilterBlankNames() {
+ MetricsFavorite favorite1 = MetricsFavorite.builder()
+ .creator(testCreator)
+ .monitorId(testMonitorId)
+ .metricsName("cpu")
+ .build();
+ MetricsFavorite favorite2 = MetricsFavorite.builder()
+ .creator(testCreator)
+ .monitorId(testMonitorId)
+ .metricsName("")
+ .build();
+ MetricsFavorite favorite3 = MetricsFavorite.builder()
+ .creator(testCreator)
+ .monitorId(testMonitorId)
+ .metricsName(null)
+ .build();
+ List<MetricsFavorite> favorites = Arrays.asList(favorite1, favorite2,
favorite3);
+
+ when(metricsFavoriteDao.findByCreatorAndMonitorId(testCreator,
testMonitorId))
+ .thenReturn(favorites);
+
+ Set<String> result =
metricsFavoriteService.getUserFavoritedMetrics(testCreator, testMonitorId);
+
+ assertNotNull(result);
+ assertEquals(1, result.size());
+ assertTrue(result.contains("cpu"));
+ verify(metricsFavoriteDao).findByCreatorAndMonitorId(testCreator,
testMonitorId);
+ }
+
+ @Test
+ void testDeleteFavoritesByMonitorIdIn() {
+ Set<Long> monitorIds = Set.of(1L, 2L, 3L);
+
+ metricsFavoriteService.deleteFavoritesByMonitorIdIn(monitorIds);
+
+ verify(metricsFavoriteDao).deleteFavoritesByMonitorIdIn(monitorIds);
+ }
+}
\ No newline at end of file
diff --git
a/web-app/src/app/routes/monitor/monitor-data-table/monitor-data-table.component.html
b/web-app/src/app/routes/monitor/monitor-data-table/monitor-data-table.component.html
index 10e7430b34..82a65020f8 100644
---
a/web-app/src/app/routes/monitor/monitor-data-table/monitor-data-table.component.html
+++
b/web-app/src/app/routes/monitor/monitor-data-table/monitor-data-table.component.html
@@ -156,7 +156,17 @@
</ng-template>
<ng-template #metrics_card_extra>
- <div style="display: flex; gap: 10px">
+ <div style="display: flex; gap: 10px; align-items: center">
+ <div style="cursor: pointer" (click)="toggleFavorite()">
+ <i
+ nz-icon
+ nzType="star"
+ [nzTheme]="isFavorite() ? 'fill' : 'outline'"
+ [style.color]="isFavorite() ? '#faad14' : '#8c8c8c'"
+ nz-tooltip
+ [nzTooltipTitle]="isFavorite() ? ('monitor.favorite.remove' | i18n) :
('monitor.favorite.add' | i18n)"
+ ></i>
+ </div>
<div nz-popover [nzPopoverContent]="('monitor.collect.time.tip' | i18n) +
': ' + (time | _date : 'yyyy-MM-dd HH:mm:ss')">
<a><i nz-icon nzType="field-time" nzTheme="outline"></i></a>
<i style="font-size: 13px; font-weight: normal; color: rgba(112, 112,
112, 0.89)">
diff --git
a/web-app/src/app/routes/monitor/monitor-data-table/monitor-data-table.component.ts
b/web-app/src/app/routes/monitor/monitor-data-table/monitor-data-table.component.ts
index 2ead3f9e03..cfb4885bb4 100644
---
a/web-app/src/app/routes/monitor/monitor-data-table/monitor-data-table.component.ts
+++
b/web-app/src/app/routes/monitor/monitor-data-table/monitor-data-table.component.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { Component, Input, OnInit } from '@angular/core';
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { NzNotificationService } from 'ng-zorro-antd/notification';
import { finalize } from 'rxjs/operators';
@@ -52,6 +52,10 @@ export class MonitorDataTableComponent implements OnInit {
metrics!: string;
@Input()
height: string = '100%';
+ @Input()
+ favoriteStatus: boolean = false;
+ @Output()
+ readonly favoriteToggle = new EventEmitter<string>();
showModal!: boolean;
time!: any;
@@ -106,4 +110,14 @@ export class MonitorDataTableComponent implements OnInit {
if (!obj) return [];
return Object.entries(obj);
}
+
+ toggleFavorite() {
+ if (this.metrics) {
+ this.favoriteToggle.emit(this.metrics);
+ }
+ }
+
+ isFavorite(): boolean {
+ return this.favoriteStatus;
+ }
}
diff --git
a/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.html
b/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.html
index 6b62bae6dd..a97ad24c9f 100755
---
a/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.html
+++
b/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.html
@@ -71,6 +71,8 @@
[metrics]="metric"
[monitorId]="monitorId"
[app]="app"
+ [favoriteStatus]="favoriteMetricsSet.has(metric)"
+ (favoriteToggle)="toggleFavorite($event)"
></app-monitor-data-table>
<!-- IO sentinel for lazy loading -->
<div id="metrics-load-sentinel" style="width: 100%; height:
1px"></div>
@@ -95,6 +97,85 @@
<div id="charts-load-sentinel" style="width: 100%; height:
1px"></div>
</div>
</nz-tab>
+ <nz-tab [nzTitle]="favoriteTabTemplate"
(nzClick)="loadFavoriteMetrics()">
+ <ng-template #favoriteTabTemplate>
+ <i nz-icon nzType="star" nzTheme="outline"></i>
+ {{ 'monitor.detail.favorite' | i18n }}
+ </ng-template>
+ <div class="favorite-content">
+ <div class="favorite-selector">
+ <nz-select
+ [(ngModel)]="favoriteTabIndex"
+ (ngModelChange)="onFavoriteTabChange($event)"
+ style="width: 200px; margin-bottom: 16px"
+ >
+ <nz-option [nzValue]="0" [nzLabel]="'monitor.detail.realtime'
| i18n">
+ <i nz-icon nzType="pic-right"></i>
+ {{ 'monitor.detail.realtime' | i18n }}
+ </nz-option>
+ <nz-option [nzValue]="1" [nzLabel]="'monitor.detail.history' |
i18n">
+ <i nz-icon nzType="line-chart"></i>
+ {{ 'monitor.detail.history' | i18n }}
+ </nz-option>
+ </nz-select>
+ </div>
+
+ <!-- realtime -->
+ <div *ngIf="favoriteTabIndex === 0">
+ <div class="cards lists" *ngIf="displayedFavoriteMetrics.length
> 0">
+ <app-monitor-data-table
+ class="card"
+ [height]="'400px'"
+ *ngFor="let metric of displayedFavoriteMetrics; let i =
index"
+ [metrics]="metric"
+ [monitorId]="monitorId"
+ [app]="app"
+ [favoriteStatus]="favoriteMetricsSet.has(metric)"
+ (favoriteToggle)="toggleFavorite($event)"
+ ></app-monitor-data-table>
+ <div
+ id="favoriteMetricsLoadSentinel"
+ style="width: 100%; height: 1px"
+ *ngIf="hasMoreFavorites && !isLoadingMoreFavorites"
+ ></div>
+ </div>
+ <div class="empty-favorite" *ngIf="favoriteMetrics.length === 0">
+ <nz-empty [nzNotFoundContent]="emptyRealtimeTemplate">
+ <ng-template #emptyRealtimeTemplate>
+ <p style="margin-top: 16px; color: #999">{{
'monitor.favorite.empty.realtime' | i18n }}</p>
+ </ng-template>
+ </nz-empty>
+ </div>
+ </div>
+
+ <!-- history -->
+ <div *ngIf="favoriteTabIndex === 1">
+ <div class="cards" *ngIf="displayedFavoriteChartMetrics.length >
0">
+ <app-monitor-data-chart
+ class="card"
+ *ngFor="let metric of displayedFavoriteChartMetrics"
+ [app]="app"
+ [metrics]="metric.metrics"
+ [metric]="metric.metric"
+ [unit]="metric.unit"
+ [monitorId]="monitorId"
+ ></app-monitor-data-chart>
+ </div>
+ <div
+ id="favoriteChartsLoadSentinel"
+ style="width: 100%; height: 1px"
+ *ngIf="hasMoreFavoriteCharts && !isLoadingMoreFavoriteCharts"
+ ></div>
+ <div class="empty-favorite" *ngIf="favoriteChartMetrics.length
=== 0">
+ <nz-empty [nzNotFoundContent]="emptyHistoryTemplate">
+ <ng-template #emptyHistoryTemplate>
+ <p style="margin-top: 16px; color: #999">{{
'monitor.favorite.empty.history' | i18n }}</p>
+ </ng-template>
+ </nz-empty>
+ </div>
+ </div>
+ </div>
+ </nz-tab>
<nz-tab *ngIf="grafanaDashboard.enabled" [nzTitle]="title3Template">
<ng-template #title3Template>
<i nz-icon nzType="radar-chart" style="margin-left: 10px"></i>
diff --git
a/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.less
b/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.less
index c7cc8f28d1..9a86a2bd33 100644
---
a/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.less
+++
b/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.less
@@ -30,11 +30,49 @@ p {
background: #141414;
}
}
+
+ .favorite-content {
+ background: #1f1f1f !important;
+
+ :host ::ng-deep .ant-tabs {
+ background: #262626 !important;
+ }
+
+ .favorite-selector {
+ :host ::ng-deep .ant-select {
+ .ant-select-selector {
+ background: #1f1f1f !important;
+ border-color: #434343 !important;
+ color: #fff !important;
+
+ &:hover {
+ border-color: #40a9ff !important;
+ }
+ }
+
+ &.ant-select-focused .ant-select-selector {
+ border-color: #1890ff !important;
+ box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2) !important;
+ }
+ }
+ }
+
+ .favorite-section {
+ .favorite-section-title {
+ color: #40a9ff !important;
+ }
+ }
+
+ .empty-favorite {
+ background: #262626 !important;
+ color: #fff !important;
+ }
+ }
}
.cards {
gap: 8px;
- margin: 8px;
+ margin: 0 8px 8px 8px;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
@@ -43,3 +81,36 @@ p {
width: calc(50% - 4px);
}
}
+
+.favorite-content {
+ padding: 8px;
+ border-radius: 6px;
+ margin: 8px 0;
+
+ .favorite-selector {
+ display: flex;
+ justify-content: flex-start;
+ margin-bottom: 0px;
+ padding-left: 8px;
+ }
+
+ .favorite-section {
+ margin-bottom: 24px;
+
+ .favorite-section-title {
+ font-weight: 500;
+ color: #1890ff;
+
+ i {
+ margin-right: 8px;
+ }
+ }
+ }
+
+ .empty-favorite {
+ padding: 40px 0;
+ text-align: center;
+ border-radius: 4px;
+ margin: 8px 0;
+ }
+}
diff --git
a/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.ts
b/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.ts
index c31c8875dd..d7740ea50e 100644
--- a/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.ts
+++ b/web-app/src/app/routes/monitor/monitor-detail/monitor-detail.component.ts
@@ -55,6 +55,7 @@ export class MonitorDetailComponent implements OnInit,
OnDestroy {
options: any;
port: number | undefined;
metrics!: string[];
+ metricsInfo: any[] = [];
chartMetrics: any[] = [];
deadline = 90;
countDownTime: number = 0;
@@ -76,13 +77,32 @@ export class MonitorDetailComponent implements OnInit,
OnDestroy {
isLoadingMoreCharts: boolean = false;
hasMoreCharts: boolean = true;
+ favoriteMetricsSet: Set<string> = new Set();
+
+ favoriteMetrics: any[] = [];
+ favoriteChartMetrics: any[] = [];
+ displayedFavoriteMetrics: any[] = [];
+ displayedFavoriteChartMetrics: any[] = [];
+
+ favoritePageSize: number = 6;
+ favoriteChartPageSize: number = 6;
+ hasMoreFavorites: boolean = true;
+ hasMoreFavoriteCharts: boolean = true;
+ isLoadingMoreFavorites: boolean = false;
+ isLoadingMoreFavoriteCharts: boolean = false;
+
+ favoriteTabIndex: number = 0;
+
private io?: IntersectionObserver;
private chartIo?: IntersectionObserver;
+ private favoriteIo: IntersectionObserver | undefined;
+ private favoriteChartIo: IntersectionObserver | undefined;
ngOnInit(): void {
this.countDownTime = this.deadline;
this.loadRealTimeMetric();
this.getGrafana();
+ this.loadFavoriteMetricsFromBackend();
}
loadMetricChart() {
@@ -183,15 +203,13 @@ export class MonitorDetailComponent implements OnInit,
OnDestroy {
this.hasMoreCharts = true;
this.isLoadingMoreCharts = false;
- this.metrics = message.data.metrics || [];
+ this.metricsInfo = message.data.metrics || [];
+ this.metrics = this.metricsInfo.map((metric: any) => metric.name);
- setTimeout(() => {
- this.cdr.detectChanges();
- if (this.metrics && this.metrics.length > 0) {
- this.loadInitialMetrics();
- this.setupIntersectionObserver();
- }
- }, 0);
+ if (this.metrics && this.metrics.length > 0) {
+ this.loadInitialMetrics();
+ this.setupIntersectionObserver();
+ }
} else {
console.warn(message.msg);
}
@@ -239,6 +257,67 @@ export class MonitorDetailComponent implements OnInit,
OnDestroy {
}, 0);
}
+ private setupFavoriteObserver(type: 'metrics' | 'charts') {
+ const isMetrics = type === 'metrics';
+ const observer = isMetrics ? this.favoriteIo : this.favoriteChartIo;
+ const selector = isMetrics ? '#favoriteMetricsLoadSentinel' :
'#favoriteChartsLoadSentinel';
+ const hasMore = isMetrics ? this.hasMoreFavorites :
this.hasMoreFavoriteCharts;
+ const isLoading = isMetrics ? this.isLoadingMoreFavorites :
this.isLoadingMoreFavoriteCharts;
+ const loadMore = isMetrics ? () => this.loadMoreFavorites() : () =>
this.loadMoreFavoriteCharts();
+
+ if (observer) {
+ observer.disconnect();
+ }
+
+ setTimeout(() => {
+ const sentinel = document.querySelector(selector);
+ if (sentinel) {
+ const newObserver = new IntersectionObserver(
+ entries => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting && hasMore && !isLoading) {
+ loadMore();
+ }
+ });
+ },
+ { threshold: 0.1 }
+ );
+
+ if (isMetrics) {
+ this.favoriteIo = newObserver;
+ } else {
+ this.favoriteChartIo = newObserver;
+ }
+
+ newObserver.observe(sentinel);
+ }
+ }, 100);
+ }
+
+ loadMoreFavorites() {
+ if (this.isLoadingMoreFavorites || !this.hasMoreFavorites) return;
+ this.isLoadingMoreFavorites = true;
+ const start = this.displayedFavoriteMetrics.length;
+ const end = Math.min(start + this.favoritePageSize,
this.favoriteMetrics.length);
+ const nextChunk = this.favoriteMetrics.slice(start, end);
+ this.displayedFavoriteMetrics =
this.displayedFavoriteMetrics.concat(nextChunk);
+ this.hasMoreFavorites = end < this.favoriteMetrics.length;
+ this.isLoadingMoreFavorites = false;
+ this.cdr.detectChanges();
+ }
+
+ loadMoreFavoriteCharts() {
+ if (this.isLoadingMoreFavoriteCharts || !this.hasMoreFavoriteCharts)
return;
+ this.isLoadingMoreFavoriteCharts = true;
+ const start = this.displayedFavoriteChartMetrics.length;
+ const end = Math.min(start + this.favoriteChartPageSize,
this.favoriteChartMetrics.length);
+ const nextChunk = this.favoriteChartMetrics.slice(start, end);
+ this.displayedFavoriteChartMetrics =
this.displayedFavoriteChartMetrics.concat(nextChunk);
+ this.hasMoreFavoriteCharts = end < this.favoriteChartMetrics.length;
+ this.isLoadingMoreFavoriteCharts = false;
+ this.cdr.detectChanges();
+ }
+
private initChartObserver(retryCount: number = 0): void {
const maxRetries = 3;
const sentinel = document.getElementById('charts-load-sentinel');
@@ -370,6 +449,8 @@ export class MonitorDetailComponent implements OnInit,
OnDestroy {
refreshMetrics() {
if (this.whichTabIndex == 1) {
this.loadMetricChart();
+ } else if (this.whichTabIndex == 2) {
+ this.loadFavoriteMetrics();
} else {
this.loadRealTimeMetric();
}
@@ -396,6 +477,158 @@ export class MonitorDetailComponent implements OnInit,
OnDestroy {
);
}
+ loadFavoriteMetrics() {
+ this.whichTabIndex = 2;
+
+ this.favoriteMetrics = [];
+ this.favoriteChartMetrics = [];
+ this.displayedFavoriteMetrics = [];
+ this.displayedFavoriteChartMetrics = [];
+ this.hasMoreFavorites = false;
+ this.hasMoreFavoriteCharts = false;
+
+ if (this.favoriteMetricsSet.size === 0) {
+ return;
+ }
+
+ // Convert favorites indicator to array
+ this.favoriteMetrics = Array.from(this.favoriteMetricsSet);
+ this.displayedFavoriteMetrics = this.favoriteMetrics.slice(0,
this.favoritePageSize);
+ this.hasMoreFavorites = this.favoriteMetrics.length >
this.favoritePageSize;
+
+ this.loadFavoriteChartDefinitions();
+
+ setTimeout(() => this.onFavoriteTabChange(this.favoriteTabIndex), 100);
+ }
+
+ private loadFavoriteChartDefinitions() {
+ // Chart definition for the independent request collection metric,
completely decoupled from the History tab
+ const favoriteMetricsList = Array.from(this.favoriteMetricsSet);
+
+ this.monitorSvc
+ .getWarehouseStorageServerStatus()
+ .pipe(
+ switchMap((message: Message<any>) => {
+ if (message.code == 0) {
+ if (this.app == 'push') {
+ return this.appDefineSvc.getPushDefine(this.monitorId);
+ } else if (this.app == 'prometheus') {
+ return this.appDefineSvc.getAppDynamicDefine(this.monitorId);
+ } else {
+ return this.appDefineSvc.getAppDefine(this.app);
+ }
+ } else {
+ return throwError(message.msg);
+ }
+ })
+ )
+ .subscribe(
+ message => {
+ if (message.code === 0 && message.data != undefined) {
+ this.favoriteChartMetrics = [];
+ let metrics = message.data.metrics;
+
+ metrics.forEach((metric: { name: any; fields: any; visible:
boolean }) => {
+ let fields = metric.fields;
+ if (fields != undefined && metric.visible) {
+ fields.forEach((field: { type: number; field: any; unit: any
}) => {
+ if (field.type == 0) {
+ const fullPath = `${metric.name}.${field.field}`;
+ if (
+ favoriteMetricsList.includes(fullPath) ||
+ favoriteMetricsList.includes(metric.name) ||
+ favoriteMetricsList.includes(field.field)
+ ) {
+ this.favoriteChartMetrics.push({
+ metrics: metric.name,
+ metric: field.field,
+ unit: field.unit
+ });
+ }
+ }
+ });
+ }
+ });
+
+ this.displayedFavoriteChartMetrics =
this.favoriteChartMetrics.slice(0, this.favoriteChartPageSize);
+ this.hasMoreFavoriteCharts = this.favoriteChartMetrics.length >
this.favoriteChartPageSize;
+ }
+ },
+ error => {
+ console.warn('Failed to load favorite chart definitions:', error);
+ }
+ );
+ }
+
+ onFavoriteTabChange(index: number) {
+ this.favoriteTabIndex = index;
+ if (index === 0) {
+ this.setupFavoriteObserver('metrics');
+ } else if (index === 1) {
+ this.setupFavoriteObserver('charts');
+ }
+ }
+
+ toggleFavorite(metric: string) {
+ if (this.favoriteMetricsSet.has(metric)) {
+ this.removeFavoriteMetric(metric);
+ } else {
+ this.addFavoriteMetric(metric);
+ }
+ }
+
+ private addFavoriteMetric(metric: string) {
+ this.monitorSvc.addMetricsFavorite(this.monitorId, metric).subscribe(
+ message => {
+ if (message.code === 0) {
+ this.favoriteMetricsSet.add(metric);
+
this.notifySvc.success(this.i18nSvc.fanyi('monitor.favorite.add.success'), '');
+ if (this.whichTabIndex === 2) {
+ this.loadFavoriteMetrics();
+ }
+ } else {
+
this.notifySvc.error(this.i18nSvc.fanyi('monitor.favorite.add.failed'),
message.msg || '');
+ }
+ },
+ error => {
+
this.notifySvc.error(this.i18nSvc.fanyi('monitor.favorite.add.failed'),
error.message || '');
+ }
+ );
+ }
+
+ private removeFavoriteMetric(metric: string) {
+ this.monitorSvc.removeMetricsFavorite(this.monitorId, metric).subscribe(
+ message => {
+ if (message.code === 0) {
+ this.favoriteMetricsSet.delete(metric);
+
this.notifySvc.success(this.i18nSvc.fanyi('monitor.favorite.remove.success'),
'');
+ if (this.whichTabIndex === 2) {
+ this.loadFavoriteMetrics();
+ }
+ } else {
+
this.notifySvc.error(this.i18nSvc.fanyi('monitor.favorite.remove.failed'),
message.msg || '');
+ }
+ },
+ error => {
+
this.notifySvc.error(this.i18nSvc.fanyi('monitor.favorite.remove.failed'),
error.message || '');
+ }
+ );
+ }
+
+ private loadFavoriteMetricsFromBackend() {
+ this.monitorSvc.getUserFavoritedMetrics(this.monitorId).subscribe(
+ message => {
+ if (message.code === 0 && message.data) {
+ const favoritedMetrics = Array.isArray(message.data) ? message.data
: Array.from(message.data);
+ favoritedMetrics.forEach(metric => {
+ this.favoriteMetricsSet.add(metric);
+ });
+ }
+ },
+ error => {}
+ );
+ }
+
ngOnDestroy(): void {
if (this.interval$) {
clearInterval(this.interval$);
@@ -422,13 +655,40 @@ export class MonitorDetailComponent implements OnInit,
OnDestroy {
}
}
+ if (this.favoriteIo) {
+ try {
+ this.favoriteIo.disconnect();
+ } catch (error) {
+ console.warn('Error disconnecting favorite metrics observer:', error);
+ } finally {
+ this.favoriteIo = undefined;
+ }
+ }
+
+ if (this.favoriteChartIo) {
+ try {
+ this.favoriteChartIo.disconnect();
+ } catch (error) {
+ console.warn('Error disconnecting favorite chart observer:', error);
+ } finally {
+ this.favoriteChartIo = undefined;
+ }
+ }
+
this.isLoadingMore = false;
this.isLoadingMoreCharts = false;
+ this.isLoadingMoreFavorites = false;
+ this.isLoadingMoreFavoriteCharts = false;
this.isSpinning = false;
this.displayedMetrics = [];
this.displayedChartMetrics = [];
+ this.displayedFavoriteMetrics = [];
+ this.displayedFavoriteChartMetrics = [];
this.metrics = [];
this.chartMetrics = [];
+ this.favoriteMetrics = [];
+ this.favoriteChartMetrics = [];
+ this.favoriteMetricsSet.clear();
}
}
diff --git a/web-app/src/app/routes/monitor/monitor.module.ts
b/web-app/src/app/routes/monitor/monitor.module.ts
index 5eece14bb7..2eb78dcc8e 100644
--- a/web-app/src/app/routes/monitor/monitor.module.ts
+++ b/web-app/src/app/routes/monitor/monitor.module.ts
@@ -25,6 +25,7 @@ import { NzBreadCrumbModule } from 'ng-zorro-antd/breadcrumb';
import { NzCollapseModule } from 'ng-zorro-antd/collapse';
import { NzDescriptionsModule } from 'ng-zorro-antd/descriptions';
import { NzDividerModule } from 'ng-zorro-antd/divider';
+import { NzEmptyModule } from 'ng-zorro-antd/empty';
import { NzLayoutModule } from 'ng-zorro-antd/layout';
import { NzListModule } from 'ng-zorro-antd/list';
import { NzPaginationModule } from 'ng-zorro-antd/pagination';
@@ -61,6 +62,7 @@ const COMPONENTS: Array<Type<void>> = [
MonitorRoutingModule,
NzBreadCrumbModule,
NzDividerModule,
+ NzEmptyModule,
NzSwitchModule,
NzTagModule,
NzRadioModule,
diff --git a/web-app/src/app/service/monitor.service.ts
b/web-app/src/app/service/monitor.service.ts
index 571e35e1ae..1914312873 100644
--- a/web-app/src/app/service/monitor.service.ts
+++ b/web-app/src/app/service/monitor.service.ts
@@ -34,6 +34,7 @@ const export_all_monitors_uri = '/monitors/export/all';
const summary_uri = '/summary';
const warehouse_storage_status_uri = '/warehouse/storage/status';
const grafana_dashboard_uri = '/grafana/dashboard';
+const metrics_favorite_uri = '/metrics/favorite';
@Injectable({
providedIn: 'root'
@@ -199,4 +200,16 @@ export class MonitorService {
copyMonitor(id: number): Observable<any> {
return this.http.post<Message<any>>(`${monitor_uri}/copy/${id}`, null);
}
+
+ public addMetricsFavorite(monitorId: number, metricsName: string):
Observable<Message<any>> {
+ return
this.http.post<Message<any>>(`${metrics_favorite_uri}/${monitorId}/${metricsName}`,
null);
+ }
+
+ public removeMetricsFavorite(monitorId: number, metricsName: string):
Observable<Message<any>> {
+ return
this.http.delete<Message<any>>(`${metrics_favorite_uri}/${monitorId}/${metricsName}`);
+ }
+
+ public getUserFavoritedMetrics(monitorId: number):
Observable<Message<Set<string>>> {
+ return
this.http.get<Message<Set<string>>>(`${metrics_favorite_uri}/${monitorId}`);
+ }
}
diff --git a/web-app/src/assets/i18n/en-US.json
b/web-app/src/assets/i18n/en-US.json
index 5f7b77ed21..914e4c0dca 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -776,6 +776,16 @@
"monitor.scrape.type.eureka_sd": "Eureka Service Discovery",
"monitor.scrape.type.consul_sd": "Consul Service Discovery",
"monitor.scrape.type.zookeeper_sd": "Zookeeper Service Discovery",
+ "monitor.favorite.add.success": "Added to favorites successfully",
+ "monitor.favorite.add.failed": "Failed to add to favorites",
+ "monitor.favorite.remove.success": "Removed from favorites successfully",
+ "monitor.favorite.remove.failed": "Failed to remove from favorites",
+ "monitor.detail.favorite": "Favorites",
+ "monitor.favorite.empty": "No favorite metrics yet",
+ "monitor.favorite.empty.realtime": "No favorite real-time metrics yet",
+ "monitor.favorite.empty.history": "No favorite historical charts yet",
+ "monitor.favorite.add": "Add to Favorites",
+ "monitor.favorite.remove": "Remove from Favorites",
"placeholder.key": "Key",
"placeholder.value": "Value",
"plugin.delete": "Delete Plugin",
diff --git a/web-app/src/assets/i18n/zh-CN.json
b/web-app/src/assets/i18n/zh-CN.json
index 169d99b402..bd7a840af6 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -776,6 +776,16 @@
"monitor.scrape.type.eureka_sd": "Eureka 服务发现",
"monitor.scrape.type.consul_sd": "Consul 服务发现",
"monitor.scrape.type.zookeeper_sd": "Zookeeper 服务发现",
+ "monitor.favorite.add.success": "收藏成功",
+ "monitor.favorite.add.failed": "收藏失败",
+ "monitor.favorite.remove.success": "取消收藏成功",
+ "monitor.favorite.remove.failed": "取消收藏失败",
+ "monitor.detail.favorite": "收藏",
+ "monitor.favorite.empty": "暂无收藏的指标",
+ "monitor.favorite.empty.realtime": "暂无收藏的实时指标",
+ "monitor.favorite.empty.history": "暂无收藏的历史图表",
+ "monitor.favorite.add": "添加收藏",
+ "monitor.favorite.remove": "取消收藏",
"placeholder.key": "键",
"placeholder.value": "值",
"plugin.delete": "刪除插件",
diff --git a/web-app/src/assets/i18n/zh-TW.json
b/web-app/src/assets/i18n/zh-TW.json
index a2084d6203..0e232035f9 100644
--- a/web-app/src/assets/i18n/zh-TW.json
+++ b/web-app/src/assets/i18n/zh-TW.json
@@ -773,6 +773,16 @@
"monitor.scrape.type.eureka_sd": "Eureka 服務發現",
"monitor.scrape.type.consul_sd": "Consul 服務發現",
"monitor.scrape.type.zookeeper_sd": "Zookeeper 服務發現",
+ "monitor.favorite.add.success": "收藏成功",
+ "monitor.favorite.add.failed": "收藏失敗",
+ "monitor.favorite.remove.success": "取消收藏成功",
+ "monitor.favorite.remove.failed": "取消收藏失敗",
+ "monitor.detail.favorite": "收藏",
+ "monitor.favorite.empty": "暫無收藏的指標",
+ "monitor.favorite.empty.realtime": "暫無收藏的即時指標",
+ "monitor.favorite.empty.history": "暫無收藏的歷史圖表",
+ "monitor.favorite.add": "添加收藏",
+ "monitor.favorite.remove": "取消收藏",
"placeholder.key": "鍵",
"placeholder.value": "值",
"plugin.delete": "刪除插件",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]