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]

Reply via email to