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

kerwin612 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 5d416e855 [Feature]  Supports Bulletin to view all metrics. (#2584)
5d416e855 is described below

commit 5d416e855b2475de0414e85b838c6313754c57c5
Author: Logic <[email protected]>
AuthorDate: Tue Aug 27 06:25:38 2024 +0800

    [Feature]  Supports Bulletin to view all metrics. (#2584)
    
    Signed-off-by: tomsun28 <[email protected]>
    Co-authored-by: YuLuo <[email protected]>
    Co-authored-by: tomsun28 <[email protected]>
    Co-authored-by: kerwin612 <[email protected]>
---
 .../hertzbeat/common/entity/manager/Monitor.java   |   4 +-
 .../common/entity/manager/bulletin/Bulletin.java   |  97 ++++
 .../entity/manager/bulletin/BulletinDto.java       |  52 +++
 .../manager/bulletin/BulletinMetricsData.java      | 135 ++++++
 .../common/entity/manager/bulletin/BulletinVo.java |  65 +++
 .../org/apache/hertzbeat/common/util/JsonUtil.java |   8 +-
 .../apache/hertzbeat/common/util/JsonUtilTest.java |   3 +-
 .../manager/controller/AppController.java          |  33 +-
 .../manager/controller/BulletinController.java     | 148 ++++++
 .../apache/hertzbeat/manager/dao/BulletinDao.java  |  41 ++
 .../hertzbeat/manager/service/AppService.java      |  17 +
 .../hertzbeat/manager/service/BulletinService.java |  84 ++++
 .../manager/service/impl/AppServiceImpl.java       | 161 ++++---
 .../manager/service/impl/BulletinServiceImpl.java  | 258 +++++++++++
 manager/src/main/resources/sureness.yml            |   4 +
 script/sureness.yml                                |   6 +-
 web-app/src/app/pojo/BulletinDefine.ts             |  28 ++
 web-app/src/app/pojo/Fields.ts                     |  22 +
 .../app/routes/bulletin/bulletin.component.html    | 237 ++++++++++
 .../app/routes/bulletin/bulletin.component.less    |  17 +
 .../app/routes/bulletin/bulletin.component.spec.ts |  43 ++
 .../src/app/routes/bulletin/bulletin.component.ts  | 509 +++++++++++++++++++++
 web-app/src/app/routes/routes-routing.module.ts    |   2 +
 web-app/src/app/routes/routes.module.ts            |  19 +-
 web-app/src/app/service/app-define.service.ts      |  18 +
 web-app/src/app/service/bulletin-define.service.ts |  87 ++++
 web-app/src/app/service/monitor.service.ts         |   4 +
 web-app/src/assets/app-data.json                   |   6 +
 web-app/src/assets/i18n/en-US.json                 |  15 +
 web-app/src/assets/i18n/zh-CN.json                 |  15 +
 web-app/src/assets/i18n/zh-TW.json                 |  15 +
 31 files changed, 2081 insertions(+), 72 deletions(-)

diff --git 
a/common/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java 
b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java
index dd59207da..2d3665bf1 100644
--- 
a/common/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java
+++ 
b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/Monitor.java
@@ -139,14 +139,14 @@ public class Monitor {
     /**
      * Record create time
      */
-    @Schema(title = "Record create time", example = "1612198922000", 
accessMode = READ_ONLY)
+    @Schema(title = "Record create time", example = 
"2024-07-02T20:09:34.903217", accessMode = READ_ONLY)
     @CreatedDate
     private LocalDateTime gmtCreate;
 
     /**
      * Record the latest modification time (timestamp in milliseconds)
      */
-    @Schema(title = "Record modify time", example = "1612198444000", 
accessMode = READ_ONLY)
+    @Schema(title = "Record modify time", example = 
"2024-07-02T20:09:34.903217", accessMode = READ_ONLY)
     @LastModifiedDate
     private LocalDateTime gmtUpdate;
 
diff --git 
a/common/src/main/java/org/apache/hertzbeat/common/entity/manager/bulletin/Bulletin.java
 
b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/bulletin/Bulletin.java
new file mode 100644
index 000000000..a500a6320
--- /dev/null
+++ 
b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/bulletin/Bulletin.java
@@ -0,0 +1,97 @@
+/*
+ * 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.bulletin;
+
+import static io.swagger.v3.oas.annotations.media.Schema.AccessMode.READ_WRITE;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.persistence.Column;
+import jakarta.persistence.Convert;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import java.time.LocalDateTime;
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import 
org.apache.hertzbeat.common.entity.manager.JsonLongListAttributeConverter;
+import 
org.apache.hertzbeat.common.entity.manager.JsonTagListAttributeConverter;
+import org.apache.hertzbeat.common.entity.manager.TagItem;
+import org.springframework.data.annotation.CreatedBy;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedBy;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+/**
+ * Bulletin
+ */
+@Entity
+@Data
+@Schema(description = "Bulletin")
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@EntityListeners(AuditingEntityListener.class)
+@Table(name = "hzb_bulletin")
+public class Bulletin {
+
+    @Id
+    @Schema(description = "Bulletin ID", example = "1")
+    private Long id;
+
+    @Schema(description = "Bulletin Name", example = "Bulletin1", accessMode = 
READ_WRITE)
+    private String name;
+
+    @Schema(description = "Monitor IDs", example = "1")
+    @Column(name = "monitor_ids", length = 5000)
+    @Convert(converter = JsonLongListAttributeConverter.class)
+    private List<Long> monitorIds;
+
+    @Schema(description = "Monitor Type eg: jvm, tomcat", example = "jvm", 
accessMode = READ_WRITE)
+    private String app;
+
+
+    @Schema(description = "Monitor Fields")
+    @Column(length = 4096, columnDefinition = "json")
+    private String fields;
+
+    @Schema(description = "Tags(status:success,env:prod)", example = "{name: 
key1, value: value1}",
+            accessMode = READ_WRITE)
+    @Convert(converter = JsonTagListAttributeConverter.class)
+    @Column(length = 2048)
+    private List<TagItem> tags;
+
+    @Schema(title = "The creator of this record", example = "tom", accessMode 
= READ_WRITE)
+    @CreatedBy
+    private String creator;
+
+    @Schema(title = "The modifier of this record", example = "tom", accessMode 
= READ_WRITE)
+    @LastModifiedBy
+    private String modifier;
+
+    @Schema(title = "Record create time", example = 
"2024-07-02T20:09:34.903217", accessMode = READ_WRITE)
+    @CreatedDate
+    private LocalDateTime gmtCreate;
+
+    @Schema(title = "Record modify time", example = 
"2024-07-02T20:09:34.903217", accessMode = READ_WRITE)
+    @LastModifiedDate
+    private LocalDateTime gmtUpdate;
+}
diff --git 
a/common/src/main/java/org/apache/hertzbeat/common/entity/manager/bulletin/BulletinDto.java
 
b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/bulletin/BulletinDto.java
new file mode 100644
index 000000000..f077fd726
--- /dev/null
+++ 
b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/bulletin/BulletinDto.java
@@ -0,0 +1,52 @@
+/*
+ * 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.bulletin;
+
+import java.util.List;
+import java.util.Map;
+import lombok.Data;
+
+/**
+ * Bulletin DTO
+ */
+@Data
+public class BulletinDto {
+
+    /**
+     * Bulletin name
+     */
+    private String name;
+
+    /**
+     * Monitor type eg: jvm, tomcat
+     */
+    private String app;
+
+
+    /**
+     * Monitor fields
+     */
+    private Map<String, List<String>> fields;
+
+    /**
+     * Monitor ids
+     */
+    private List<Long> monitorIds;
+
+}
diff --git 
a/common/src/main/java/org/apache/hertzbeat/common/entity/manager/bulletin/BulletinMetricsData.java
 
b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/bulletin/BulletinMetricsData.java
new file mode 100644
index 000000000..33f93a200
--- /dev/null
+++ 
b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/bulletin/BulletinMetricsData.java
@@ -0,0 +1,135 @@
+/*
+ * 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.bulletin;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * Bulletin Metrics Data
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Schema(description = "Bulletin Metrics Data")
+public class BulletinMetricsData {
+
+    /**
+     * Bulletin Name
+     */
+    @Schema(title = "Bulletin Name")
+    private String name;
+
+    /**
+     * Content Data
+     */
+    @Schema(description = "Content Data")
+    private List<Data> content;
+
+    /**
+     * Bulletin Metrics Data
+     */
+    @lombok.Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class Data {
+
+        /**
+         * Monitor Name
+         */
+        @Schema(title = "Monitor name")
+        private String monitorName;
+
+        /**
+         * Monitor ID
+         */
+        @Schema(title = "Monitor ID")
+        private Long monitorId;
+
+        /**
+         * Monitor IP
+         */
+        @Schema(title = "Monitor IP")
+        private String host;
+
+        /**
+         * Monitor Metrics
+         */
+        @Schema(title = "Monitor Metrics")
+        private List<Metric> metrics;
+    }
+
+    /**
+     * Metrics Data
+     */
+    @lombok.Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Schema(description = "Metrics Data")
+    public static class Metric{
+
+        /**
+         * Metric type
+         */
+        @Schema(title = "Metric type")
+        private String name;
+
+        /**
+         * Metric fields
+         */
+        @Schema(title = "Metric fields")
+        private List<List<Field>> fields;
+    }
+
+
+    /**
+     * Metrics field
+     */
+    @lombok.Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    @Schema(description = "Metrics field")
+    public static class Field{
+
+        /**
+         * Field name
+         */
+        @Schema(title = "Field name")
+        private String key;
+
+        /**
+         * Field unit
+         */
+        @Schema(title = "Field unit")
+        private String unit;
+
+        /**
+         * Field value
+         */
+        @Schema(title = "Field value")
+        private String value;
+    }
+}
diff --git 
a/common/src/main/java/org/apache/hertzbeat/common/entity/manager/bulletin/BulletinVo.java
 
b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/bulletin/BulletinVo.java
new file mode 100644
index 000000000..a49fbb18a
--- /dev/null
+++ 
b/common/src/main/java/org/apache/hertzbeat/common/entity/manager/bulletin/BulletinVo.java
@@ -0,0 +1,65 @@
+/*
+ * 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.bulletin;
+
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.hertzbeat.common.entity.manager.TagItem;
+
+/**
+ * Bulletin Vo
+ */
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class BulletinVo {
+
+    /**
+     * Bulletin ID
+     */
+    private Long id;
+
+    /**
+     * Bulletin name
+     */
+    private String name;
+
+    /**
+     * Bulletin metrics
+     */
+    private List<String> metrics;
+
+    /**
+     * Bulletin tags
+     */
+    private List<TagItem> tags;
+
+    /**
+     * Bulletin monitor ID
+     */
+    private List<Long> monitorId;
+
+    /**
+     * Bulletin monitor name
+     */
+    private String app;
+
+}
diff --git 
a/common/src/main/java/org/apache/hertzbeat/common/util/JsonUtil.java 
b/common/src/main/java/org/apache/hertzbeat/common/util/JsonUtil.java
index 7e8fbed52..b95619bb2 100644
--- a/common/src/main/java/org/apache/hertzbeat/common/util/JsonUtil.java
+++ b/common/src/main/java/org/apache/hertzbeat/common/util/JsonUtil.java
@@ -100,11 +100,15 @@ public final class JsonUtil {
      * @param jsonStr json string
      * @return true if the string is a json string
      */
+
+
     public static boolean isJsonStr(String jsonStr) {
-        if (!StringUtils.hasText(jsonStr)) {
+        if (jsonStr == null || jsonStr.trim().isEmpty()) {
             return false;
         }
-        if (!jsonStr.startsWith("{") || !jsonStr.endsWith("}")) {
+        jsonStr = jsonStr.trim();
+        if (!(jsonStr.startsWith("{") && jsonStr.endsWith("}"))
+                && !(jsonStr.startsWith("[") && jsonStr.endsWith("]"))) {
             return false;
         }
         try {
diff --git 
a/common/src/test/java/org/apache/hertzbeat/common/util/JsonUtilTest.java 
b/common/src/test/java/org/apache/hertzbeat/common/util/JsonUtilTest.java
index 628a10e10..d4acb6089 100644
--- a/common/src/test/java/org/apache/hertzbeat/common/util/JsonUtilTest.java
+++ b/common/src/test/java/org/apache/hertzbeat/common/util/JsonUtilTest.java
@@ -20,6 +20,7 @@ package org.apache.hertzbeat.common.util;
 import static org.apache.hertzbeat.common.util.JsonUtil.isJsonStr;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import com.fasterxml.jackson.core.type.TypeReference;
 import java.util.ArrayList;
 import java.util.List;
@@ -66,7 +67,7 @@ class JsonUtilTest {
         assertFalse(isJsonStr(jsonString));
 
         String jsonStringArrays = "[{\"name\":\"John\"}, {\"name\":\"Doe\"}]";
-        assertFalse(isJsonStr(jsonStringArrays));
+        assertTrue(isJsonStr(jsonStringArrays));
     }
 
 }
diff --git 
a/manager/src/main/java/org/apache/hertzbeat/manager/controller/AppController.java
 
b/manager/src/main/java/org/apache/hertzbeat/manager/controller/AppController.java
index d3d98a955..5472f0f39 100644
--- 
a/manager/src/main/java/org/apache/hertzbeat/manager/controller/AppController.java
+++ 
b/manager/src/main/java/org/apache/hertzbeat/manager/controller/AppController.java
@@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.validation.Valid;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import org.apache.hertzbeat.common.entity.dto.Message;
 import org.apache.hertzbeat.common.entity.job.Job;
 import org.apache.hertzbeat.common.entity.manager.ParamDefine;
@@ -155,6 +156,35 @@ public class AppController {
             @Parameter(description = "en: language type",
                     example = "zh-CN")
             @RequestParam(name = "lang", required = false) String lang) {
+        lang = getLang(lang);
+        List<Hierarchy> appHierarchies = appService.getAllAppHierarchy(lang);
+        return ResponseEntity.ok(Message.success(appHierarchies));
+    }
+
+    @GetMapping(path = "/hierarchy/{app}")
+    @Operation(summary = "Query all monitor metrics level, output in a 
hierarchical structure", description = "Query all monitor metrics level, output 
in a hierarchical structure")
+    public ResponseEntity<Message<List<Hierarchy>>> queryAppsHierarchyByApp(
+            @Parameter(description = "en: language type",
+                    example = "zh-CN")
+            @RequestParam(name = "lang", required = false) String lang,
+            @Parameter(description = "en: Monitoring type name", example = 
"api") @PathVariable("app") final String app) {
+        lang = getLang(lang);
+        List<Hierarchy> appHierarchies = appService.getAppHierarchy(app, lang);
+        return ResponseEntity.ok(Message.success(appHierarchies));
+    }
+
+    @GetMapping(path = "/defines")
+    @Operation(summary = "Query all monitor types", description = "Query all 
monitor types")
+    public ResponseEntity<Message<Map<String, String>>> getAllAppDefines(
+            @Parameter(description = "en: language type",
+                    example = "zh-CN")
+            @RequestParam(name = "lang", required = false) String lang) {
+        lang = getLang(lang);
+        Map<String, String> allAppDefines = appService.getI18nApps(lang);
+        return ResponseEntity.ok(Message.success(allAppDefines));
+    }
+
+    private String getLang(@RequestParam(name = "lang", required = false) 
@Parameter(description = "en: language type", example = "zh-CN") String lang) {
         if (lang == null || lang.isEmpty()) {
             lang = "zh-CN";
         }
@@ -165,7 +195,6 @@ public class AppController {
         } else {
             lang = "en-US";
         }
-        List<Hierarchy> appHierarchies = appService.getAllAppHierarchy(lang);
-        return ResponseEntity.ok(Message.success(appHierarchies));
+        return lang;
     }
 }
diff --git 
a/manager/src/main/java/org/apache/hertzbeat/manager/controller/BulletinController.java
 
b/manager/src/main/java/org/apache/hertzbeat/manager/controller/BulletinController.java
new file mode 100644
index 000000000..604905976
--- /dev/null
+++ 
b/manager/src/main/java/org/apache/hertzbeat/manager/controller/BulletinController.java
@@ -0,0 +1,148 @@
+/*
+ * 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 static org.apache.hertzbeat.common.constants.CommonConstants.FAIL_CODE;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import jakarta.validation.Valid;
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.entity.dto.Message;
+import org.apache.hertzbeat.common.entity.manager.bulletin.Bulletin;
+import org.apache.hertzbeat.common.entity.manager.bulletin.BulletinDto;
+import org.apache.hertzbeat.common.entity.manager.bulletin.BulletinMetricsData;
+import org.apache.hertzbeat.manager.service.BulletinService;
+import org.apache.hertzbeat.manager.service.MonitorService;
+import org.apache.hertzbeat.warehouse.store.realtime.RealTimeDataReader;
+import org.springframework.beans.factory.annotation.Autowired;
+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.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Bulletin Controller
+ */
+@Slf4j
+@RestController
+@RequestMapping(value = "/api/bulletin", produces = {APPLICATION_JSON_VALUE})
+public class BulletinController {
+
+    @Autowired
+    private BulletinService bulletinService;
+
+    @Autowired
+    private RealTimeDataReader realTimeDataReader;
+
+    @Autowired
+    private MonitorService monitorService;
+
+    /**
+     * add a new bulletin
+     */
+    @PostMapping
+    public ResponseEntity<Message<Void>> addNewBulletin(@Valid @RequestBody 
BulletinDto bulletinDto) {
+        try {
+            bulletinService.validate(bulletinDto);
+            bulletinService.addBulletin(bulletinDto);
+        } catch (Exception e) {
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "Add failed! " + 
e.getMessage()));
+        }
+        return ResponseEntity.ok(Message.success("Add success!"));
+    }
+
+    /**
+     * edit a exist bulletin
+     */
+    @PutMapping
+    public ResponseEntity<Message<Void>> editBulletin(@Valid @RequestBody 
BulletinDto bulletinDto) {
+        try {
+            bulletinService.validate(bulletinDto);
+            bulletinService.editBulletin(bulletinDto);
+        } catch (Exception e) {
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "Add failed! " + 
e.getMessage()));
+        }
+        return ResponseEntity.ok(Message.success("Add success!"));
+    }
+
+    /**
+     * edit a exist bulletin
+     */
+    @GetMapping("/{name}")
+    public ResponseEntity<Message<Bulletin>> getBulletinByName(@Valid 
@PathVariable String name) {
+        try {
+            return 
ResponseEntity.ok(Message.success(bulletinService.getBulletinByName(name)));
+        } catch (Exception e) {
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "Add failed! " + 
e.getMessage()));
+        }
+    }
+
+    /**
+     * get All Names
+     */
+    @Operation(summary = "Get All Bulletin Names", description = "Get All 
Bulletin Names")
+    @GetMapping("/names")
+    public ResponseEntity<Message<List<String>>> getAllNames() {
+        List<String> names = bulletinService.getAllNames();
+        return ResponseEntity.ok(Message.success(names));
+    }
+
+    /**
+     * delete bulletin by name
+     */
+    @Operation(summary = "Delete Bulletin by Name", description = "Delete 
Bulletin by Name")
+    @DeleteMapping
+    public ResponseEntity<Message<Void>> deleteBulletin(
+            @Parameter(description = "Bulletin Name", example = 
"402372614668544")
+            @RequestParam List<String> names) {
+        try {
+            bulletinService.deleteBulletinByName(names);
+        } catch (Exception e) {
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "Delete failed!" 
+ e.getMessage()));
+        }
+        return ResponseEntity.ok(Message.success("Delete success!"));
+    }
+
+    @GetMapping("/metrics")
+    @Operation(summary = "Query All Bulletin Real Time Metrics Data", 
description = "Query All Bulletin real-time metrics data of monitoring 
indicators")
+    public ResponseEntity<Message<?>> getAllMetricsData(
+            @RequestParam(name = "name") String name,
+            @RequestParam(defaultValue = "0", name = "pageIndex") int 
pageIndex,
+            @RequestParam(defaultValue = "10", name = "pageSize") int 
pageSize) {
+        if (!realTimeDataReader.isServerAvailable()) {
+            return ResponseEntity.ok(Message.fail(FAIL_CODE, "real time store 
not available"));
+        }
+
+        Bulletin bulletin = bulletinService.getBulletinByName(name);
+
+        BulletinMetricsData.BulletinMetricsDataBuilder contentBuilder = 
BulletinMetricsData.builder()
+                .name(bulletin.getName());
+        BulletinMetricsData data = 
bulletinService.buildBulletinMetricsData(contentBuilder, bulletin);
+        return ResponseEntity.ok(Message.success(data));
+    }
+
+}
diff --git 
a/manager/src/main/java/org/apache/hertzbeat/manager/dao/BulletinDao.java 
b/manager/src/main/java/org/apache/hertzbeat/manager/dao/BulletinDao.java
new file mode 100644
index 000000000..0be6a8320
--- /dev/null
+++ b/manager/src/main/java/org/apache/hertzbeat/manager/dao/BulletinDao.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.dao;
+
+
+import java.util.List;
+import org.apache.hertzbeat.common.entity.manager.bulletin.Bulletin;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+/**
+ * BulletinDao
+ */
+public interface BulletinDao extends JpaRepository<Bulletin, Long>, 
JpaSpecificationExecutor<Bulletin> {
+    /**
+     * Delete Bulletin by name
+     */
+    void deleteByNameIn(List<String> names);
+
+    /**
+     * Get Bulletin by name
+     */
+    Bulletin findByName(String name);
+
+
+}
diff --git 
a/manager/src/main/java/org/apache/hertzbeat/manager/service/AppService.java 
b/manager/src/main/java/org/apache/hertzbeat/manager/service/AppService.java
index d34df783c..21144afb6 100644
--- a/manager/src/main/java/org/apache/hertzbeat/manager/service/AppService.java
+++ b/manager/src/main/java/org/apache/hertzbeat/manager/service/AppService.java
@@ -80,6 +80,14 @@ public interface AppService {
      */
     Map<String, String> getI18nResources(String lang);
 
+    /**
+     * Get the I 18 N resources of the monitoring type
+     *
+     * @param lang Language type
+     * @return I18N Resources
+     */
+    Map<String, String> getI18nApps(String lang);
+
     /**
      * Query all types of monitoring hierarchy
      *
@@ -88,6 +96,15 @@ public interface AppService {
      */
     List<Hierarchy> getAllAppHierarchy(String lang);
 
+    /**
+     * Get the monitoring hierarchy based on the monitoring type
+     *
+     * @param app monitoring type
+     * @param lang language
+     * @return hierarchy information
+     */
+    List<Hierarchy> getAppHierarchy(String app, String lang);
+
     /**
      * Get all app define
      *
diff --git 
a/manager/src/main/java/org/apache/hertzbeat/manager/service/BulletinService.java
 
b/manager/src/main/java/org/apache/hertzbeat/manager/service/BulletinService.java
new file mode 100644
index 000000000..b7022fb29
--- /dev/null
+++ 
b/manager/src/main/java/org/apache/hertzbeat/manager/service/BulletinService.java
@@ -0,0 +1,84 @@
+/*
+ * 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.List;
+import java.util.Optional;
+import org.apache.hertzbeat.common.entity.manager.bulletin.Bulletin;
+import org.apache.hertzbeat.common.entity.manager.bulletin.BulletinDto;
+import org.apache.hertzbeat.common.entity.manager.bulletin.BulletinMetricsData;
+import org.apache.hertzbeat.common.entity.manager.bulletin.BulletinVo;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.jpa.domain.Specification;
+
+/**
+ * Bulletin Service
+ */
+public interface BulletinService {
+
+    /**
+     * validate Bulletin
+     */
+    void validate(BulletinDto bulletindto) throws IllegalArgumentException;
+
+    /**
+     * Get Bulletin by name
+     */
+    Bulletin getBulletinByName(String name);
+
+    /**
+     * Get Bulletin by id
+     */
+    Optional<Bulletin> getBulletinById(Long id);
+
+    /**
+     * Get all names
+     */
+    List<String> getAllNames();
+
+
+    /**
+     * delete Bulletin by id
+     */
+    void deleteBulletinByName(List<String> names);
+
+
+    /**
+     * Save Bulletin
+     */
+    void editBulletin(BulletinDto bulletinDto);
+
+    /**
+     * Add Bulletin
+     */
+    void addBulletin(BulletinDto bulletinDto);
+
+    /**
+     * Dynamic conditional query
+     * @param specification Query conditions
+     * @param pageRequest Paging parameters
+     * @return The query results
+     */
+    Page<BulletinVo> getBulletins(Specification<Bulletin> specification, 
PageRequest pageRequest);
+
+    /**
+     * deal with the bulletin
+     */
+    BulletinMetricsData 
buildBulletinMetricsData(BulletinMetricsData.BulletinMetricsDataBuilder 
contentBuilder, Bulletin bulletin);
+}
diff --git 
a/manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AppServiceImpl.java
 
b/manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AppServiceImpl.java
index 713172b9c..a3a2b9113 100644
--- 
a/manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AppServiceImpl.java
+++ 
b/manager/src/main/java/org/apache/hertzbeat/manager/service/impl/AppServiceImpl.java
@@ -255,6 +255,20 @@ public class AppServiceImpl implements AppService, 
CommandLineRunner {
         return i18nMap;
     }
 
+    @Override
+    public Map<String, String> getI18nApps(String lang) {
+        Map<String, String> i18nMap = new HashMap<>(128);
+        for (var job : appDefines.values()) {
+            var name = job.getName();
+            var i18nName = CommonUtil.getLangMappingValueFromI18nMap(lang, 
name);
+            if (i18nName != null) {
+                i18nMap.put(job.getApp(), i18nName);
+            }
+        }
+        return i18nMap;
+    }
+
+
     @Override
     public List<Hierarchy> getAllAppHierarchy(String lang) {
         LinkedList<Hierarchy> hierarchies = new LinkedList<>();
@@ -263,82 +277,104 @@ public class AppServiceImpl implements AppService, 
CommandLineRunner {
             if 
(DispatchConstants.PROTOCOL_PUSH.equalsIgnoreCase(job.getApp())) {
                 continue;
             }
-            var hierarchyApp = new Hierarchy();
-            hierarchyApp.setCategory(job.getCategory());
-            hierarchyApp.setValue(job.getApp());
-            hierarchyApp.setHide(job.isHide());
-            var nameMap = job.getName();
-            if (nameMap != null && !nameMap.isEmpty()) {
-                var i18nName = CommonUtil.getLangMappingValueFromI18nMap(lang, 
nameMap);
-                if (i18nName != null) {
-                    hierarchyApp.setLabel(i18nName);
-                }
-            }
-            List<Hierarchy> hierarchyMetricList = new LinkedList<>();
-            if 
(DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(job.getApp())) {
-                List<Monitor> monitors = 
monitorDao.findMonitorsByAppEquals(job.getApp());
-                for (Monitor monitor : monitors) {
-                    List<CollectRep.MetricsData> metricsDataList = 
warehouseService.queryMonitorMetricsData(monitor.getId());
-                    for (CollectRep.MetricsData metricsData : metricsDataList) 
{
-                        var hierarchyMetric = new Hierarchy();
-                        hierarchyMetric.setValue(metricsData.getMetrics());
-                        hierarchyMetric.setLabel(metricsData.getMetrics());
-                        List<Hierarchy> hierarchyFieldList = 
metricsData.getFieldsList().stream()
-                                .map(item -> {
-                                    var hierarchyField = new Hierarchy();
-                                    hierarchyField.setValue(item.getName());
-                                    hierarchyField.setLabel(item.getName());
-                                    hierarchyField.setIsLeaf(true);
-                                    hierarchyField.setType((byte) 
item.getType());
-                                    hierarchyField.setUnit(item.getUnit());
-                                    return hierarchyField;
-                                }).collect(Collectors.toList());
-                        hierarchyMetric.setChildren(hierarchyFieldList);
-                        // combine Hierarchy Metrics
-                        combineHierarchyMetrics(hierarchyMetricList, 
hierarchyMetric);
-                    }
-                }
-                hierarchyApp.setChildren(hierarchyMetricList);
-                hierarchies.addFirst(hierarchyApp);
-            } else {
-                if (job.getMetrics() != null) {
-                    for (var metrics : job.getMetrics()) {
-                        var hierarchyMetric = new Hierarchy();
-                        hierarchyMetric.setValue(metrics.getName());
-                        var metricsI18nName = 
CommonUtil.getLangMappingValueFromI18nMap(lang, metrics.getI18n());
-                        hierarchyMetric.setLabel(metricsI18nName != null ? 
metricsI18nName : metrics.getName());
-                        List<Hierarchy> hierarchyFieldList = new 
LinkedList<>();
-                        if (metrics.getFields() != null) {
-                            for (var field : metrics.getFields()) {
+            queryAppHierarchy(lang, hierarchies, job);
+        }
+        return hierarchies;
+    }
+
+    @Override
+    public List<Hierarchy> getAppHierarchy(String app, String lang) {
+        LinkedList<Hierarchy> hierarchies = new LinkedList<>();
+        Job job = appDefines.get(app.toLowerCase());
+        // TODO temporarily filter out push to solve the front-end problem, 
and open it after the subsequent design optimization
+        if (DispatchConstants.PROTOCOL_PUSH.equalsIgnoreCase(job.getApp())) {
+            return hierarchies;
+        }
+        queryAppHierarchy(lang, hierarchies, job);
+        return hierarchies;
+    }
+
+    private void queryAppHierarchy(String lang, LinkedList<Hierarchy> 
hierarchies, Job job) {
+        var hierarchyApp = new Hierarchy();
+        hierarchyApp.setCategory(job.getCategory());
+        hierarchyApp.setValue(job.getApp());
+        hierarchyApp.setHide(job.isHide());
+        var nameMap = job.getName();
+        if (nameMap != null && !nameMap.isEmpty()) {
+            var i18nName = CommonUtil.getLangMappingValueFromI18nMap(lang, 
nameMap);
+            if (i18nName != null) {
+                hierarchyApp.setLabel(i18nName);
+            }
+        }
+        List<Hierarchy> hierarchyMetricList = new LinkedList<>();
+        if 
(DispatchConstants.PROTOCOL_PROMETHEUS.equalsIgnoreCase(job.getApp())) {
+            List<Monitor> monitors = 
monitorDao.findMonitorsByAppEquals(job.getApp());
+            for (Monitor monitor : monitors) {
+                List<CollectRep.MetricsData> metricsDataList = 
warehouseService.queryMonitorMetricsData(monitor.getId());
+                for (CollectRep.MetricsData metricsData : metricsDataList) {
+                    var hierarchyMetric = new Hierarchy();
+                    hierarchyMetric.setValue(metricsData.getMetrics());
+                    hierarchyMetric.setLabel(metricsData.getMetrics());
+                    List<Hierarchy> hierarchyFieldList = 
metricsData.getFieldsList().stream()
+                            .map(item -> {
                                 var hierarchyField = new Hierarchy();
-                                hierarchyField.setValue(field.getField());
-                                var metricI18nName = 
CommonUtil.getLangMappingValueFromI18nMap(lang, field.getI18n());
-                                hierarchyField.setLabel(metricI18nName != null 
? metricI18nName : field.getField());
+                                hierarchyField.setValue(item.getName());
+                                hierarchyField.setLabel(item.getName());
                                 hierarchyField.setIsLeaf(true);
-                                // for metric
-                                hierarchyField.setType(field.getType());
-                                hierarchyField.setUnit(field.getUnit());
-                                hierarchyFieldList.add(hierarchyField);
-                            }
-                            hierarchyMetric.setChildren(hierarchyFieldList);
+                                hierarchyField.setType((byte) item.getType());
+                                hierarchyField.setUnit(item.getUnit());
+                                return hierarchyField;
+                            }).collect(Collectors.toList());
+                    hierarchyMetric.setChildren(hierarchyFieldList);
+                    // combine Hierarchy Metrics
+                    combineHierarchyMetrics(hierarchyMetricList, 
hierarchyMetric);
+                }
+            }
+            hierarchyApp.setChildren(hierarchyMetricList);
+            hierarchies.addFirst(hierarchyApp);
+        } else {
+            if (job.getMetrics() != null) {
+                for (var metrics : job.getMetrics()) {
+                    var hierarchyMetric = new Hierarchy();
+                    hierarchyMetric.setValue(metrics.getName());
+                    var metricsI18nName = 
CommonUtil.getLangMappingValueFromI18nMap(lang, metrics.getI18n());
+                    hierarchyMetric.setLabel(metricsI18nName != null ? 
metricsI18nName : metrics.getName());
+                    List<Hierarchy> hierarchyFieldList = new LinkedList<>();
+                    if (metrics.getFields() != null) {
+                        for (var field : metrics.getFields()) {
+                            var hierarchyField = new Hierarchy();
+                            hierarchyField.setValue(field.getField());
+                            var metricI18nName = 
CommonUtil.getLangMappingValueFromI18nMap(lang, field.getI18n());
+                            hierarchyField.setLabel(metricI18nName != null ? 
metricI18nName : field.getField());
+                            hierarchyField.setIsLeaf(true);
+                            // for metric
+                            hierarchyField.setType(field.getType());
+                            hierarchyField.setUnit(field.getUnit());
+                            hierarchyFieldList.add(hierarchyField);
                         }
-                        hierarchyMetricList.add(hierarchyMetric);
+                        hierarchyMetric.setChildren(hierarchyFieldList);
                     }
+                    hierarchyMetricList.add(hierarchyMetric);
                 }
-                hierarchyApp.setChildren(hierarchyMetricList);
-                hierarchies.add(hierarchyApp);
             }
+            hierarchyApp.setChildren(hierarchyMetricList);
+            hierarchies.add(hierarchyApp);
         }
-        return hierarchies;
     }
 
+
     private void combineHierarchyMetrics(List<Hierarchy> hierarchyMetricList, 
Hierarchy hierarchyMetric) {
         Optional<Hierarchy> preHierarchyOptional = hierarchyMetricList.stream()
-                .filter(item -> 
item.getValue().equals(hierarchyMetric.getValue())).findFirst();
+                .filter(item -> 
item.getValue().equals(hierarchyMetric.getValue()))
+                .findFirst();
+
         if (preHierarchyOptional.isPresent()) {
             Hierarchy preHierarchy = preHierarchyOptional.get();
             List<Hierarchy> children = preHierarchy.getChildren();
-            Set<String> childrenKey = 
children.stream().map(Hierarchy::getValue).collect(Collectors.toSet());
+            Set<String> childrenKey = children.stream()
+                    .map(Hierarchy::getValue)
+                    .collect(Collectors.toSet());
+
             for (Hierarchy child : hierarchyMetric.getChildren()) {
                 if (!childrenKey.contains(child.getValue())) {
                     children.add(child);
@@ -349,6 +385,7 @@ public class AppServiceImpl implements AppService, 
CommandLineRunner {
         }
     }
 
+
     @Override
     public Map<String, Job> getAllAppDefines() {
         return appDefines;
diff --git 
a/manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java
 
b/manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java
new file mode 100644
index 000000000..6142df260
--- /dev/null
+++ 
b/manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java
@@ -0,0 +1,258 @@
+/*
+ * 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 com.fasterxml.jackson.core.type.TypeReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.hertzbeat.common.entity.manager.Monitor;
+import org.apache.hertzbeat.common.entity.manager.bulletin.Bulletin;
+import org.apache.hertzbeat.common.entity.manager.bulletin.BulletinDto;
+import org.apache.hertzbeat.common.entity.manager.bulletin.BulletinMetricsData;
+import org.apache.hertzbeat.common.entity.manager.bulletin.BulletinVo;
+import org.apache.hertzbeat.common.entity.message.CollectRep;
+import org.apache.hertzbeat.common.util.JsonUtil;
+import org.apache.hertzbeat.common.util.SnowFlakeIdGenerator;
+import org.apache.hertzbeat.manager.dao.BulletinDao;
+import org.apache.hertzbeat.manager.service.BulletinService;
+import org.apache.hertzbeat.manager.service.MonitorService;
+import org.apache.hertzbeat.warehouse.store.realtime.RealTimeDataReader;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Bulletin Service Implementation
+ */
+
+@Service
+@Slf4j
+public class BulletinServiceImpl implements BulletinService {
+
+    private static final String NO_DATA = "No Data";
+
+    private static final String EMPTY_STRING = "";
+
+    @Autowired
+    private BulletinDao bulletinDao;
+
+    @Autowired
+    private MonitorService monitorService;
+
+    @Autowired
+    private RealTimeDataReader realTimeDataReader;
+
+
+    /**
+     * validate Bulletin
+     */
+    @Override
+    public void validate(BulletinDto bulletinDto) throws 
IllegalArgumentException {
+        if (bulletinDto == null) {
+            throw new IllegalArgumentException("Bulletin cannot be null");
+        }
+        if (bulletinDto.getApp() == null || bulletinDto.getApp().isEmpty()) {
+            throw new IllegalArgumentException("Bulletin app cannot be null or 
empty");
+        }
+        if (bulletinDto.getFields() == null || 
bulletinDto.getFields().isEmpty()) {
+            throw new IllegalArgumentException("Bulletin fields cannot be null 
or empty");
+        }
+        if (bulletinDto.getMonitorIds() == null || 
bulletinDto.getMonitorIds().isEmpty()) {
+            throw new IllegalArgumentException("Bulletin monitorIds cannot be 
null or empty");
+        }
+    }
+
+
+    /**
+     * Pageable query Bulletin
+     */
+    @Override
+    public Bulletin getBulletinByName(String name) {
+        return bulletinDao.findByName(name);
+    }
+
+    /**
+     * Get all names
+     */
+    @Override
+    public List<String> getAllNames() {
+        return 
bulletinDao.findAll().stream().map(Bulletin::getName).distinct().toList();
+    }
+
+
+    /**
+     * Save Bulletin
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void editBulletin(BulletinDto bulletinDto) {
+        try {
+            //TODO: update bulletin
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Add Bulletin
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void addBulletin(BulletinDto bulletinDto) {
+        try {
+            Bulletin bulletin = new Bulletin();
+            bulletin.setName(bulletinDto.getName());
+            bulletin.setId(SnowFlakeIdGenerator.generateId());
+            Map<String, List<String>> map = bulletinDto.getFields();
+            Map<String, List<String>> sortedMap = new TreeMap<>(map);
+            String fields = JsonUtil.toJson(sortedMap);
+            bulletin.setFields(fields);
+            bulletin.setMonitorIds(bulletinDto.getMonitorIds());
+            bulletin.setApp(bulletinDto.getApp());
+            bulletinDao.save(bulletin);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Dynamic conditional query
+     *
+     * @param specification Query conditions
+     * @param pageRequest   Paging parameters
+     * @return The query results
+     */
+    @Override
+    public Page<BulletinVo> getBulletins(Specification<Bulletin> 
specification, PageRequest pageRequest) {
+        List<BulletinVo> voList = new ArrayList<>();
+        Page<Bulletin> bulletinPage = Page.empty(pageRequest);
+        try {
+            bulletinPage = bulletinDao.findAll(specification, pageRequest);
+            voList = bulletinPage.stream().map(bulletin -> {
+                BulletinVo vo = new BulletinVo();
+                vo.setId(bulletin.getId());
+                vo.setName(bulletin.getName());
+                vo.setTags(bulletin.getTags());
+                vo.setMonitorId(bulletin.getMonitorIds());
+                vo.setApp(bulletin.getApp());
+                return vo;
+            }).collect(Collectors.toList());
+        } catch (Exception e) {
+            log.error("Failed to query bulletin: {}", e.getLocalizedMessage(), 
e);
+        }
+        long total = bulletinPage.getTotalElements();
+        return new PageImpl<>(voList, pageRequest, total);
+    }
+
+    /**
+     * deal with the bulletin
+     *
+     */
+    @Override
+    public BulletinMetricsData 
buildBulletinMetricsData(BulletinMetricsData.BulletinMetricsDataBuilder 
contentBuilder, Bulletin bulletin) {
+        List<BulletinMetricsData.Data> dataList = new ArrayList<>();
+        for (Long monitorId : bulletin.getMonitorIds()) {
+            Monitor monitor = monitorService.getMonitor(monitorId);
+            BulletinMetricsData.Data.DataBuilder dataBuilder = 
BulletinMetricsData.Data.builder()
+                    .monitorId(monitorId)
+                    .monitorName(monitor.getName())
+                    .host(monitor.getHost());
+
+            List<BulletinMetricsData.Metric> metrics = new ArrayList<>();
+            Map<String, List<String>> fieldMap = 
JsonUtil.fromJson(bulletin.getFields(), new TypeReference<>() {});
+
+            if (fieldMap != null) {
+                for (Map.Entry<String, List<String>> entry : 
fieldMap.entrySet()) {
+                    String metric = entry.getKey();
+                    List<String> fields = entry.getValue();
+                    BulletinMetricsData.Metric.MetricBuilder metricBuilder = 
BulletinMetricsData.Metric.builder()
+                            .name(metric);
+                    CollectRep.MetricsData currentMetricsData = 
realTimeDataReader.getCurrentMetricsData(monitorId, metric);
+
+                    List<List<BulletinMetricsData.Field>> fieldsList;
+                    if (currentMetricsData != null) {
+                        fieldsList = 
currentMetricsData.getValuesList().stream()
+                                .map(valueRow -> {
+                                    List<BulletinMetricsData.Field> fieldList 
= currentMetricsData.getFieldsList().stream()
+                                            .map(field -> 
BulletinMetricsData.Field.builder()
+                                                    .key(field.getName())
+                                                    .unit(field.getUnit())
+                                                    .build())
+                                            .toList();
+
+                                    for (int i = 0; i < fieldList.size(); i++) 
{
+                                        
fieldList.get(i).setValue(valueRow.getColumns(i));
+                                    }
+                                    return fieldList;
+                                })
+                                .toList();
+                    } else {
+                        fieldsList = Collections.singletonList(fields.stream()
+                                .map(field -> 
BulletinMetricsData.Field.builder()
+                                        .key(field)
+                                        .unit("")
+                                        .value("NO_DATA")
+                                        .build())
+                                .toList());
+                    }
+
+                    metricBuilder.fields(fieldsList);
+                    metrics.add(metricBuilder.build());
+                }
+            }
+            dataBuilder.metrics(metrics);
+            dataList.add(dataBuilder.build());
+        }
+        contentBuilder.content(dataList);
+        return contentBuilder.build();
+    }
+
+
+    /**
+     * Get Bulletin by id
+     *
+     */
+    @Override
+    public Optional<Bulletin> getBulletinById(Long id) {
+        return bulletinDao.findById(id);
+    }
+
+    /**
+     * delete Bulletin by names
+     *
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteBulletinByName(List<String> names) {
+        try {
+            bulletinDao.deleteByNameIn(names);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/manager/src/main/resources/sureness.yml 
b/manager/src/main/resources/sureness.yml
index c7253ff88..d96f2a331 100644
--- a/manager/src/main/resources/sureness.yml
+++ b/manager/src/main/resources/sureness.yml
@@ -58,6 +58,10 @@ resourceRole:
   - /api/status/page/**===post===[admin,user]
   - /api/status/page/**===put===[admin,user]
   - /api/status/page/**===delete===[admin]
+  - /api/bulletin/**===get===[admin,user,guest]
+  - /api/bulletin/**===post===[admin,user]
+  - /api/bulletin/**===put===[admin,user]
+  - /api/bulletin/**===delete===[admin]
 
 # config the resource restful api that need bypass auth protection
 # rule: api===method 
diff --git a/script/sureness.yml b/script/sureness.yml
index c7253ff88..cb4024c5a 100644
--- a/script/sureness.yml
+++ b/script/sureness.yml
@@ -58,7 +58,11 @@ resourceRole:
   - /api/status/page/**===post===[admin,user]
   - /api/status/page/**===put===[admin,user]
   - /api/status/page/**===delete===[admin]
-
+  - /api/bulletin/**===get===[admin,user,guest]
+  - /api/bulletin/**===post===[admin,user]
+  - /api/bulletin/**===put===[admin,user]
+  - /api/bulletin/**===delete===[admin]
+  
 # config the resource restful api that need bypass auth protection
 # rule: api===method 
 # eg: /api/v1/source3===get means /api/v1/source3===get can be access by 
anyone, no need auth.
diff --git a/web-app/src/app/pojo/BulletinDefine.ts 
b/web-app/src/app/pojo/BulletinDefine.ts
new file mode 100644
index 000000000..0c4149577
--- /dev/null
+++ b/web-app/src/app/pojo/BulletinDefine.ts
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+import { Fields } from './Fields';
+
+export class BulletinDefine {
+  id!: number;
+  name!: string;
+  monitorIds!: number[];
+  app!: string;
+  fields: Fields = {};
+}
diff --git a/web-app/src/app/pojo/Fields.ts b/web-app/src/app/pojo/Fields.ts
new file mode 100644
index 000000000..19c5160c3
--- /dev/null
+++ b/web-app/src/app/pojo/Fields.ts
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+export interface Fields {
+  [key: string]: string[];
+}
diff --git a/web-app/src/app/routes/bulletin/bulletin.component.html 
b/web-app/src/app/routes/bulletin/bulletin.component.html
new file mode 100644
index 000000000..f4db66833
--- /dev/null
+++ b/web-app/src/app/routes/bulletin/bulletin.component.html
@@ -0,0 +1,237 @@
+<!--
+  ~ 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.
+-->
+
+<app-help-message-show
+  [help_message_content]="'bulletin.help.message.content' | i18n"
+  [guild_link]="'bulletin.help.link' | i18n"
+  [module_name]="'menu.alert.setting'"
+  [icon_name]="'calculator'"
+></app-help-message-show>
+
+<nz-divider></nz-divider>
+
+<app-toolbar>
+  <ng-template #left>
+    <button nz-button nzType="primary" (click)="sync()" nz-tooltip 
[nzTooltipTitle]="'common.refresh' | i18n">
+      <i nz-icon nzType="sync" nzTheme="outline"></i>
+    </button>
+    <button nz-button nzType="primary" (click)="onNewBulletinDefine()">
+      <i nz-icon nzType="appstore-add" nzTheme="outline"></i>
+      {{ 'bulletin.new' | i18n }}
+    </button>
+    <!--TODO: Wait until the backend interface is improved before releasing 
it.-->
+    <!--<button *ngIf="currentDefine" nz-button nzType="primary" 
(click)="onEditBulletinDefine()">
+      <i nz-icon nzType="edit" nzTheme="outline"></i>
+      {{ 'bulletin.edit' | i18n }}
+    </button>-->
+    <button *ngIf="currentDefine" nz-button nzType="primary" nzDanger 
(click)="onDeleteBulletinDefines()">
+      <i nz-icon nzType="delete" nzTheme="outline"></i>
+      {{ 'bulletin.delete' | i18n }}
+    </button>
+    <button *ngIf="currentDefine" nz-button nzType="primary" nzDanger 
(click)="onBatchDeleteBulletinDefines()">
+      <i nz-icon nzType="delete" nzTheme="outline"></i>
+      {{ 'bulletin.batch.delete' | i18n }}
+    </button>
+  </ng-template>
+</app-toolbar>
+
+<nz-tabset nzType="card" (nzSelectedIndexChange)="onTabChange($event)">
+  <nz-tab *ngFor="let tab of tabs" [nzTitle]="tab">
+    <ng-container *ngIf="metricsData">
+      <nz-table
+        #fixedTable
+        [nzData]="metricsData"
+        [nzPageIndex]="pageIndex"
+        [nzPageSize]="pageSize"
+        [nzTotal]="total"
+        nzFrontPagination="false"
+        [nzLoading]="tableLoading"
+        nzShowSizeChanger
+        [nzPageSizeOptions]="[10, 20, 50]"
+        (nzQueryParams)="onTablePageChange($event)"
+        nzShowPagination="true"
+        [nzScroll]="{ x: 'auto' }"
+        nzBordered
+        class="table"
+      >
+        <thead>
+          <tr>
+            <th nzAlign="center" nzWidth="7%" [rowSpan]="2">App</th>
+            <th nzAlign="center" nzWidth="7%" [rowSpan]="2">Host</th>
+            <ng-container *ngFor="let metric of metrics">
+              <th nzAlign="center" [colSpan]="getKeys(metric).length">
+                {{ metric }}
+              </th>
+            </ng-container>
+          </tr>
+          <tr>
+            <ng-container *ngFor="let metric of metrics">
+              <ng-container *ngFor="let field of getKeys(metric)">
+                <th nzAlign="center" [colSpan]="1">
+                  {{ field }}
+                </th>
+              </ng-container>
+            </ng-container>
+          </tr>
+        </thead>
+        <tbody>
+          <ng-container *ngFor="let content of fixedTable.data">
+            <tr>
+              <td nzAlign="center" [rowSpan]="1">{{ content.monitorName }}</td>
+              <td nzAlign="center" [rowSpan]="1">{{ content.host }}</td>
+              <ng-container *ngFor="let metric of content.metrics">
+                <ng-container *ngFor="let field of metric.fields">
+                  <ng-container *ngFor="let item of field">
+                    <td nzAlign="center">
+                      <ng-container *ngIf="item.value === 'NO_DATA'; else 
hasData">
+                        <nz-tag nzColor="warning">No Data Available</nz-tag>
+                      </ng-container>
+                      <ng-template #hasData>
+                        {{ item.value }}
+                        <nz-tag *ngIf="item.unit !== ''" nzColor="success">{{ 
item.unit }}</nz-tag>
+                      </ng-template>
+                    </td>
+                  </ng-container>
+                </ng-container>
+              </ng-container>
+            </tr>
+          </ng-container>
+        </tbody>
+      </nz-table>
+    </ng-container>
+  </nz-tab>
+</nz-tabset>
+
+<!-- new bulletin modal -->
+<nz-modal
+  [(nzVisible)]="isManageModalVisible"
+  [nzTitle]="(isManageModalAdd ? 'bulletin.new' : 'bulletin.edit') | i18n"
+  (nzOnCancel)="onManageModalCancel()"
+  (nzOnOk)="onManageModalOk()"
+  nzMaskClosable="false"
+  nzWidth="70%"
+  [nzOkLoading]="isManageModalOkLoading"
+>
+  <div *nzModalContent class="-inner-content">
+    <form nz-form #defineForm="ngForm">
+      <nz-form-item>
+        <nz-form-label [nzSpan]="7" nzFor="textInput" nzRequired="true">{{ 
'bulletin.name' | i18n }}</nz-form-label>
+        <nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | 
i18n">
+          <input nz-input placeholder="{{ 'bulletin.name.placeholder' | i18n 
}}" [(ngModel)]="define.name" name="inputText" required />
+        </nz-form-control>
+      </nz-form-item>
+
+      <nz-form-item>
+        <nz-form-label [nzSpan]="7" nzFor="dropdown" nzRequired="true">{{ 
'bulletin.monitor.type' | i18n }}</nz-form-label>
+        <nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | 
i18n">
+          <nz-select
+            nzAllowClear
+            nzShowSearch
+            name="appDefines"
+            [(ngModel)]="define.app"
+            (nzOnSearch)="onSearchAppDefines()"
+            (ngModelChange)="onAppChange($event)"
+            required
+          >
+            <ng-container *ngFor="let app of appEntries">
+              <nz-option *ngIf="isAppListLoading" [nzValue]="app.key" 
[nzLabel]="app.key + '/' + app.value"></nz-option>
+            </ng-container>
+          </nz-select>
+        </nz-form-control>
+      </nz-form-item>
+
+      <nz-form-item>
+        <nz-form-label [nzSpan]="7" nzFor="dropdown" nzRequired="true">{{ 
'bulletin.monitor.name' | i18n }}</nz-form-label>
+        <nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | 
i18n">
+          <nz-select
+            nzAllowClear
+            nzShowSearch
+            name="monitors"
+            [(ngModel)]="define.monitorIds"
+            (nzOnSearch)="onSearchMonitorsByApp(define.app)"
+            nzMode="multiple"
+            required
+          >
+            <ng-container *ngFor="let monitor of monitors">
+              <nz-option *ngIf="isMonitorListLoading" [nzValue]="monitor.id" 
[nzLabel]="monitor.name"></nz-option>
+            </ng-container>
+          </nz-select>
+        </nz-form-control>
+      </nz-form-item>
+
+      <nz-form-item>
+        <nz-form-label [nzSpan]="7" nzFor="dropdown" nzRequired="true">{{ 
'bulletin.monitor.metrics' | i18n }}</nz-form-label>
+        <nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | 
i18n">
+          <nz-transfer [nzDataSource]="hierarchies" 
[nzRenderList]="[leftRenderList, null]" (nzChange)="transferChange($event)">
+            <ng-template #leftRenderList let-items 
let-onItemSelect="onItemSelect">
+              <nz-tree
+                [nzData]="treeNodes"
+                nzExpandAll
+                nzBlockNode
+                nzCheckable
+                nzCheckStrictly
+                [(ngModel)]="tempMetrics"
+                name="metrics"
+                (nzCheckBoxChange)="treeCheckBoxChange($event, onItemSelect)"
+              >
+                <ng-template let-node>
+                  <span
+                    (click)="checkBoxChange(node, onItemSelect)"
+                    class="ant-tree-node-content-wrapper 
ant-tree-node-content-wrapper-open"
+                  >
+                    {{ node.title }}
+                  </span>
+                </ng-template>
+              </nz-tree>
+            </ng-template>
+          </nz-transfer>
+        </nz-form-control>
+      </nz-form-item>
+    </form>
+  </div>
+</nz-modal>
+
+<!-- delete bulletin modal -->
+<nz-modal
+  [(nzVisible)]="isBatchDeleteModalVisible"
+  [nzTitle]="'bulletin.batch.delete' | i18n"
+  (nzOnCancel)="onBatchDeleteModalCancel()"
+  (nzOnOk)="onBatchDeleteModalOk()"
+  nzMaskClosable="false"
+  nzWidth="70%"
+>
+  <div *nzModalContent class="-inner-content">
+    <form nz-form #defineForm="ngForm">
+      <nz-form-item>
+        <nz-form-label [nzSpan]="7" nzFor="textInput" nzRequired="true">{{ 
'bulletin.name' | i18n }}</nz-form-label>
+        <nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | 
i18n">
+          <nz-select
+            nzMode="multiple"
+            nzPlaceHolder="Please select"
+            [(ngModel)]="this.deleteBulletinNames"
+            [nzShowSearch]="true"
+            name="delete"
+          >
+            <nz-option *ngFor="let tab of tabs" [nzLabel]="tab" 
[nzValue]="tab"></nz-option>
+          </nz-select>
+        </nz-form-control>
+      </nz-form-item>
+    </form>
+  </div>
+</nz-modal>
diff --git a/web-app/src/app/routes/bulletin/bulletin.component.less 
b/web-app/src/app/routes/bulletin/bulletin.component.less
new file mode 100644
index 000000000..0b13dcbfc
--- /dev/null
+++ b/web-app/src/app/routes/bulletin/bulletin.component.less
@@ -0,0 +1,17 @@
+@table-padding: 8px;
+.table {
+  width: 100%;
+  table-layout: auto;
+
+  th {
+    white-space: nowrap;
+    padding: @table-padding;
+    text-align: center;
+  }
+
+  td {
+    white-space: nowrap;
+    padding: @table-padding;
+    text-align: center;
+  }
+}
diff --git a/web-app/src/app/routes/bulletin/bulletin.component.spec.ts 
b/web-app/src/app/routes/bulletin/bulletin.component.spec.ts
new file mode 100644
index 000000000..5e1fc1040
--- /dev/null
+++ b/web-app/src/app/routes/bulletin/bulletin.component.spec.ts
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { BulletinComponent } from './bulletin.component';
+
+describe('BulletinComponent', () => {
+  let component: BulletinComponent;
+  let fixture: ComponentFixture<BulletinComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [BulletinComponent]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(BulletinComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/web-app/src/app/routes/bulletin/bulletin.component.ts 
b/web-app/src/app/routes/bulletin/bulletin.component.ts
new file mode 100644
index 000000000..f48f6ec9e
--- /dev/null
+++ b/web-app/src/app/routes/bulletin/bulletin.component.ts
@@ -0,0 +1,509 @@
+/*
+ * 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.
+ */
+
+import { Component, Inject, OnInit } from '@angular/core';
+import { I18NService } from '@core';
+import { ALAIN_I18N_TOKEN } from '@delon/theme';
+import { NzModalService } from 'ng-zorro-antd/modal';
+import { NzNotificationService } from 'ng-zorro-antd/notification';
+import { NzTableQueryParams } from 'ng-zorro-antd/table';
+import { TransferChange } from 'ng-zorro-antd/transfer';
+import { NzFormatEmitEvent, NzTreeNode, NzTreeNodeOptions } from 
'ng-zorro-antd/tree';
+import { finalize } from 'rxjs/operators';
+
+import { BulletinDefine } from '../../pojo/BulletinDefine';
+import { Fields } from '../../pojo/Fields';
+import { Monitor } from '../../pojo/Monitor';
+import { AppDefineService } from '../../service/app-define.service';
+import { BulletinDefineService } from '../../service/bulletin-define.service';
+import { MonitorService } from '../../service/monitor.service';
+
+@Component({
+  selector: 'app-bulletin',
+  templateUrl: './bulletin.component.html',
+  styleUrls: ['./bulletin.component.less']
+})
+export class BulletinComponent implements OnInit {
+  constructor(
+    private modal: NzModalService,
+    private notifySvc: NzNotificationService,
+    private appDefineSvc: AppDefineService,
+    private monitorSvc: MonitorService,
+    private bulletinDefineSvc: BulletinDefineService,
+    @Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService
+  ) {}
+  search!: string;
+  tabs!: string[];
+  metricsData!: any;
+  tableLoading: boolean = true;
+  bulletinName!: string;
+  deleteBulletinNames: string[] = [];
+  isAppListLoading = false;
+  isMonitorListLoading = false;
+  treeNodes!: NzTreeNodeOptions[];
+  hierarchies: NzTreeNodeOptions[] = [];
+  appMap = new Map<string, string>();
+  appEntries: Array<{ value: any; key: string }> = [];
+  checkedNodeList: NzTreeNode[] = [];
+  monitors: Monitor[] = [];
+  metrics = new Set<string>();
+  tempMetrics = new Set<string>();
+  fields: Fields = {};
+  pageIndex: number = 1;
+  pageSize: number = 8;
+  total: number = 0;
+
+  ngOnInit() {
+    this.loadTabs();
+  }
+
+  sync() {
+    this.loadData(this.pageIndex - 1, this.pageSize);
+  }
+
+  onNewBulletinDefine() {
+    this.resetManageModalData();
+    this.isManageModalAdd = true;
+    this.isManageModalVisible = true;
+    this.isManageModalOkLoading = false;
+  }
+
+  onEditBulletinDefine() {
+    if (this.currentDefine) {
+      this.define = this.currentDefine;
+      this.onAppChange(this.define.app);
+      // this.tempMetrics.add(...this.define.fields.keys());
+      this.isManageModalAdd = false;
+      this.isManageModalVisible = true;
+      this.isManageModalOkLoading = false;
+    }
+  }
+
+  deleteBulletinDefines(defineNames: string[]) {
+    if (defineNames == null || defineNames.length == 0) {
+      
this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), 
'');
+      return;
+    }
+    const deleteDefines$ = 
this.bulletinDefineSvc.deleteBulletinDefines(defineNames).subscribe(
+      message => {
+        deleteDefines$.unsubscribe();
+        if (message.code === 0) {
+          
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.delete-success'), '');
+          this.loadTabs();
+        } else {
+          
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), 
message.msg);
+        }
+      },
+      error => {
+        deleteDefines$.unsubscribe();
+        this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), 
error.msg);
+      }
+    );
+  }
+
+  isManageModalVisible = false;
+  isManageModalOkLoading = false;
+  isManageModalAdd = true;
+  define: BulletinDefine = new BulletinDefine();
+  currentDefine!: BulletinDefine | null;
+
+  onManageModalCancel() {
+    this.isManageModalVisible = false;
+  }
+
+  resetManageModalData() {
+    this.define = new BulletinDefine();
+    this.define.monitorIds = [];
+    this.hierarchies = [];
+    this.treeNodes = [];
+  }
+
+  onManageModalOk() {
+    this.isManageModalOkLoading = true;
+    this.define.fields = this.fields;
+    if (this.isManageModalAdd) {
+      const modalOk$ = this.bulletinDefineSvc
+        .newBulletinDefine(this.define)
+        .pipe(
+          finalize(() => {
+            modalOk$.unsubscribe();
+            this.isManageModalOkLoading = false;
+          })
+        )
+        .subscribe(
+          message => {
+            if (message.code === 0) {
+              
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.new-success'), '');
+              this.isManageModalVisible = false;
+              this.resetManageModalData();
+              this.loadTabs();
+            } else {
+              
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.new-fail'), message.msg);
+            }
+          },
+          error => {
+            this.notifySvc.error(this.i18nSvc.fanyi('common.notify.new-fail'), 
error.msg);
+          }
+        );
+    } else {
+      const modalOk$ = this.bulletinDefineSvc
+        .editBulletinDefine(this.define)
+        .pipe(
+          finalize(() => {
+            modalOk$.unsubscribe();
+            this.isManageModalOkLoading = false;
+          })
+        )
+        .subscribe(
+          message => {
+            if (message.code === 0) {
+              this.isManageModalVisible = false;
+              
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.edit-success'), '');
+              this.loadData(this.pageIndex - 1, this.pageSize);
+            } else {
+              
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), 
message.msg);
+            }
+          },
+          error => {
+            
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), error.msg);
+          }
+        );
+    }
+  }
+
+  onSearchAppDefines(): void {
+    this.appDefineSvc
+      .getAppDefines(this.i18nSvc.defaultLang)
+      .pipe()
+      .subscribe(
+        message => {
+          if (message.code === 0) {
+            this.appMap = message.data;
+            this.appEntries = Object.entries(this.appMap).map(([key, value]) 
=> ({ key, value }));
+            if (this.appEntries != null) {
+              this.isAppListLoading = true;
+            }
+          } else {
+            console.warn(message.msg);
+          }
+        },
+        error => {
+          console.warn(error.msg);
+        }
+      );
+  }
+
+  onSearchMonitorsByApp(app: string): void {
+    this.monitorSvc
+      .getMonitorsByApp(app)
+      .pipe()
+      .subscribe(
+        message => {
+          if (message.code === 0) {
+            this.monitors = message.data;
+            if (this.monitors != null) {
+              this.isMonitorListLoading = true;
+            }
+          } else {
+            console.warn(message.msg);
+          }
+        },
+        error => {
+          console.warn(error.msg);
+        }
+      );
+  }
+
+  onAppChange(app: string): void {
+    if (app) {
+      this.onSearchMonitorsByApp(app);
+      this.onSearchTreeNodes(app);
+    } else {
+      this.hierarchies = [];
+      this.treeNodes = [];
+    }
+  }
+
+  onSearchTreeNodes(app: string): void {
+    this.appDefineSvc
+      .getAppHierarchyByName(this.i18nSvc.defaultLang, app)
+      .pipe()
+      .subscribe(
+        message => {
+          if (message.code === 0) {
+            this.hierarchies = this.transformToTransferItems(message.data);
+            this.treeNodes = this.generateTree(this.hierarchies);
+          } else {
+            console.warn(message.msg);
+          }
+        },
+        error => {
+          console.warn(error.msg);
+        }
+      );
+  }
+
+  transformToTransferItems(data: any[]): NzTreeNodeOptions[] {
+    const result: NzTreeNodeOptions[] = [];
+    let currentId = 1;
+
+    const traverse = (nodes: any[], parentKey: string | null = null, parentId: 
number | null = null) => {
+      nodes.forEach(node => {
+        const key = parentKey ? `${parentKey}` : node.value;
+        const isRootNode = parentId === null;
+        const item: NzTreeNodeOptions = {
+          id: currentId++,
+          key,
+          value: node.value,
+          title: node.label,
+          isLeaf: node.isLeaf,
+          parentId,
+          disabled: isRootNode
+        };
+        result.push(item);
+
+        if (node.children) {
+          traverse(node.children, key, item.id);
+        }
+      });
+    };
+
+    if (data[0] && data[0].children) {
+      data = data[0].children;
+      traverse(data);
+    }
+
+    return result;
+  }
+
+  private generateTree(arr: NzTreeNodeOptions[]): NzTreeNodeOptions[] {
+    const tree: NzTreeNodeOptions[] = [];
+    const treeNodes: any = {};
+    let leftElem: NzTreeNodeOptions;
+    let rightElem: NzTreeNodeOptions;
+
+    for (let i = 0, len = arr.length; i < len; i++) {
+      leftElem = arr[i];
+      treeNodes[leftElem.id] = { ...leftElem };
+      treeNodes[leftElem.id].children = [];
+    }
+
+    for (const id in treeNodes) {
+      if (treeNodes.hasOwnProperty(id)) {
+        rightElem = treeNodes[id];
+        if (rightElem.parentId) {
+          treeNodes[rightElem.parentId].children.push(rightElem);
+        } else {
+          tree.push(rightElem);
+        }
+      }
+    }
+    return tree;
+  }
+
+  treeCheckBoxChange(event: NzFormatEmitEvent, onItemSelect: (item: 
NzTreeNodeOptions) => void): void {
+    this.checkBoxChange(event.node!, onItemSelect);
+  }
+
+  checkBoxChange(node: NzTreeNode, onItemSelect: (item: NzTreeNodeOptions) => 
void): void {
+    if (node.isDisabled) {
+      return;
+    }
+
+    if (node.isChecked) {
+      this.checkedNodeList.push(node);
+    } else {
+      const idx = this.checkedNodeList.indexOf(node);
+      if (idx !== -1) {
+        this.checkedNodeList.splice(idx, 1);
+      }
+    }
+    const item = this.hierarchies.find(w => w.id === node.origin.id);
+    onItemSelect(item!);
+  }
+
+  transferChange(ret: TransferChange): void {
+    // add
+    if (ret.to === 'right') {
+      this.checkedNodeList.forEach(node => {
+        node.isDisabled = true;
+        node.isChecked = true;
+        this.tempMetrics.add(node.key);
+
+        if (!this.fields[node.key]) {
+          this.fields[node.key] = [];
+        }
+        if (!this.fields[node.key].includes(node.origin.value)) {
+          this.fields[node.key].push(node.origin.value);
+        }
+      });
+    }
+    // delete
+    else if (ret.to === 'left') {
+      this.checkedNodeList.forEach(node => {
+        node.isDisabled = false;
+        node.isChecked = false;
+        this.tempMetrics.delete(node.key);
+
+        if (this.fields[node.key]) {
+          const index = this.fields[node.key].indexOf(node.origin.value);
+          if (index > -1) {
+            this.fields[node.key].splice(index, 1);
+          }
+          // 如果该 key 下的数组为空,则删除该 key
+          if (this.fields[node.key].length === 0) {
+            delete this.fields[node.key];
+          }
+        }
+      });
+    }
+  }
+
+  loadTabs() {
+    const allNames$ = this.bulletinDefineSvc.getAllNames().subscribe(
+      message => {
+        allNames$.unsubscribe();
+        if (message.code === 0) {
+          this.tabs = message.data;
+          if (this.tabs != null) {
+            this.bulletinName = this.tabs[0];
+          }
+          this.loadData(this.pageIndex - 1, this.pageSize);
+        } else {
+          this.notifySvc.error(this.i18nSvc.fanyi('common.notify.get-fail'), 
message.msg);
+        }
+      },
+      error => {
+        allNames$.unsubscribe();
+        this.notifySvc.error(this.i18nSvc.fanyi('common.notify.get-fail'), 
error.msg);
+      }
+    );
+  }
+
+  loadData(page: number, size: number) {
+    this.tableLoading = true;
+    this.metricsData = [];
+    this.currentDefine = null;
+    this.metrics = new Set<string>();
+    if (this.bulletinName != null) {
+      const defineData$ = 
this.bulletinDefineSvc.getBulletinDefine(this.bulletinName).subscribe(
+        message => {
+          if (message.code === 0) {
+            this.currentDefine = message.data;
+
+            const metricData$ = 
this.bulletinDefineSvc.getMonitorMetricsData(this.bulletinName, page, 
size).subscribe(
+              message => {
+                metricData$.unsubscribe();
+                if (message.code === 0 && message.data) {
+                  (this.metricsData = message.data.content).forEach((item: 
any) => {
+                    item.metrics.forEach((metric: any) => {
+                      this.metrics.add(metric.name);
+                    });
+                  });
+                } else if (message.code !== 0) {
+                  this.notifySvc.warning(`${message.msg}`, '');
+                  console.info(`${message.msg}`);
+                }
+                this.tableLoading = false;
+              },
+              error => {
+                console.error(error.msg);
+                metricData$.unsubscribe();
+                this.tableLoading = false;
+              }
+            );
+          } else {
+            this.notifySvc.warning(`${message.msg}`, '');
+            console.info(`${message.msg}`);
+          }
+        },
+        error => {
+          console.error(error.msg);
+          defineData$.unsubscribe();
+          this.tableLoading = false;
+        }
+      );
+    }
+    this.tableLoading = false;
+  }
+
+  getKeys(metricName: string): string[] {
+    const result = new Set<string>();
+    this.metricsData.forEach((item: any) => {
+      item.metrics.forEach((metric: any) => {
+        if (metric.name === metricName) {
+          metric.fields.forEach((fieldGroup: any) => {
+            fieldGroup.forEach((field: any) => {
+              result.add(field.key);
+            });
+          });
+        }
+      });
+    });
+    return Array.from(result);
+  }
+
+  onTablePageChange(params: NzTableQueryParams): void {
+    const { pageSize, pageIndex } = params;
+
+    if (pageIndex !== this.pageIndex || pageSize !== this.pageSize) {
+      this.pageIndex = pageIndex;
+      this.pageSize = pageSize;
+      this.loadData(pageIndex - 1, pageSize);
+    }
+  }
+
+  isBatchDeleteModalVisible: boolean = false;
+  isBatchDeleteModalOkLoading: boolean = false;
+
+  onDeleteBulletinDefines() {
+    this.modal.confirm({
+      nzTitle: this.i18nSvc.fanyi('common.confirm.delete'),
+      nzOkText: this.i18nSvc.fanyi('common.button.ok'),
+      nzCancelText: this.i18nSvc.fanyi('common.button.cancel'),
+      nzOkDanger: true,
+      nzOkType: 'primary',
+      nzClosable: false,
+      nzOnOk: () => this.deleteBulletinDefines([this.bulletinName])
+    });
+  }
+
+  onBatchDeleteBulletinDefines() {
+    this.isBatchDeleteModalVisible = true;
+  }
+
+  onBatchDeleteModalCancel() {
+    this.isBatchDeleteModalVisible = false;
+  }
+
+  onBatchDeleteModalOk() {
+    this.deleteBulletinDefines(this.deleteBulletinNames);
+    this.isBatchDeleteModalOkLoading = false;
+    this.isBatchDeleteModalVisible = false;
+  }
+
+  protected readonly Array = Array;
+
+  onTabChange($event: number) {
+    this.bulletinName = this.tabs[$event];
+    this.metricsData = [];
+    this.loadData(this.pageIndex - 1, this.pageSize);
+    console.log(this.metricsData);
+  }
+}
diff --git a/web-app/src/app/routes/routes-routing.module.ts 
b/web-app/src/app/routes/routes-routing.module.ts
index b8a87eb0f..f4f86a0b9 100644
--- a/web-app/src/app/routes/routes-routing.module.ts
+++ b/web-app/src/app/routes/routes-routing.module.ts
@@ -6,6 +6,7 @@ import { DetectAuthGuard } from 
'../core/guard/detect-auth-guard';
 import { LayoutBasicComponent } from '../layout/basic/basic.component';
 import { LayoutBlankComponent } from '../layout/blank/blank.component';
 import { LayoutPassportComponent } from 
'../layout/passport/passport.component';
+import { BulletinComponent } from './bulletin/bulletin.component';
 import { DashboardComponent } from './dashboard/dashboard.component';
 import { UserLockComponent } from './passport/lock/lock.component';
 import { UserLoginComponent } from './passport/login/login.component';
@@ -19,6 +20,7 @@ const routes: Routes = [
     children: [
       { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
       { path: 'dashboard', component: DashboardComponent, data: { titleI18n: 
'menu.dashboard' } },
+      { path: 'bulletin', component: BulletinComponent, data: { titleI18n: 
'menu.dashboard' } },
       { path: 'exception', loadChildren: () => 
import('./exception/exception.module').then(m => m.ExceptionModule) },
       { path: 'monitors', loadChildren: () => 
import('./monitor/monitor.module').then(m => m.MonitorModule) },
       { path: 'alert', loadChildren: () => 
import('./alert/alert.module').then(m => m.AlertModule) },
diff --git a/web-app/src/app/routes/routes.module.ts 
b/web-app/src/app/routes/routes.module.ts
index 0d45cb640..37999b486 100644
--- a/web-app/src/app/routes/routes.module.ts
+++ b/web-app/src/app/routes/routes.module.ts
@@ -1,24 +1,32 @@
+import { CommonModule } from '@angular/common';
 import { NgModule, Type } from '@angular/core';
 // eslint-disable-next-line import/order
 import { SharedModule } from '@shared';
 
 import { TagCloudComponent } from 'angular-tag-cloud-module';
+import { NzCascaderModule } from 'ng-zorro-antd/cascader';
 import { NzCollapseModule } from 'ng-zorro-antd/collapse';
 import { NzDividerModule } from 'ng-zorro-antd/divider';
 import { NzListModule } from 'ng-zorro-antd/list';
+import { NzRadioModule } from 'ng-zorro-antd/radio';
+import { NzSwitchComponent } from 'ng-zorro-antd/switch';
 import { NzTagModule } from 'ng-zorro-antd/tag';
 import { NzTimelineModule } from 'ng-zorro-antd/timeline';
+import { NzTransferModule } from 'ng-zorro-antd/transfer';
+import { NzTreeComponent } from 'ng-zorro-antd/tree';
+import { NzUploadModule } from 'ng-zorro-antd/upload';
 import { NgxEchartsModule } from 'ngx-echarts';
 import { SlickCarouselModule } from 'ngx-slick-carousel';
 
 import { LayoutModule } from '../layout/layout.module';
+import { BulletinComponent } from './bulletin/bulletin.component';
 import { DashboardComponent } from './dashboard/dashboard.component';
 import { UserLockComponent } from './passport/lock/lock.component';
 import { UserLoginComponent } from './passport/login/login.component';
 import { RouteRoutingModule } from './routes-routing.module';
 import { StatusPublicComponent } from 
'./status-public/status-public.component';
 
-const COMPONENTS: Array<Type<void>> = [DashboardComponent, UserLoginComponent, 
UserLockComponent, StatusPublicComponent];
+const COMPONENTS: Array<Type<void>> = [DashboardComponent, UserLoginComponent, 
UserLockComponent, StatusPublicComponent, BulletinComponent];
 
 @NgModule({
   imports: [
@@ -32,7 +40,14 @@ const COMPONENTS: Array<Type<void>> = [DashboardComponent, 
UserLoginComponent, U
     NzDividerModule,
     LayoutModule,
     NzCollapseModule,
-    NzListModule
+    NzListModule,
+    CommonModule,
+    NzRadioModule,
+    NzUploadModule,
+    NzCascaderModule,
+    NzTransferModule,
+    NzSwitchComponent,
+    NzTreeComponent
   ],
   declarations: COMPONENTS
 })
diff --git a/web-app/src/app/service/app-define.service.ts 
b/web-app/src/app/service/app-define.service.ts
index a68a1998a..a832fd40d 100644
--- a/web-app/src/app/service/app-define.service.ts
+++ b/web-app/src/app/service/app-define.service.ts
@@ -97,4 +97,22 @@ export class AppDefineService {
     const options = { params: httpParams };
     return this.http.get<Message<any>>(app_hierarchy, options);
   }
+
+  public getAppHierarchyByName(lang: string | undefined, app: string): 
Observable<Message<any>> {
+    if (lang == undefined) {
+      lang = 'en_US';
+    }
+    let httpParams = new HttpParams().append('lang', lang);
+    const options = { params: httpParams };
+    return this.http.get<Message<any>>(`${app_hierarchy}/${app}`, options);
+  }
+
+  public getAppDefines(lang: string | undefined): Observable<Message<any>> {
+    if (lang == undefined) {
+      lang = 'en_US';
+    }
+    let httpParams = new HttpParams().append('lang', lang);
+    const options = { params: httpParams };
+    return this.http.get<Message<any>>(`/apps/defines`, options);
+  }
 }
diff --git a/web-app/src/app/service/bulletin-define.service.ts 
b/web-app/src/app/service/bulletin-define.service.ts
new file mode 100644
index 000000000..cd92c4c53
--- /dev/null
+++ b/web-app/src/app/service/bulletin-define.service.ts
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+
+import { BulletinDefine } from '../pojo/BulletinDefine';
+import { Message } from '../pojo/Message';
+import { Monitor } from '../pojo/Monitor';
+import { Page } from '../pojo/Page';
+
+const bulletin_define_uri = '/bulletin';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class BulletinDefineService {
+  constructor(private http: HttpClient) {}
+
+  public newBulletinDefine(body: BulletinDefine) {
+    return this.http.post<Message<any>>(bulletin_define_uri, body);
+  }
+
+  public editBulletinDefine(body: BulletinDefine) {
+    return this.http.put<Message<any>>(bulletin_define_uri, body);
+  }
+
+  public getBulletinDefine(name: string) {
+    return this.http.get<Message<any>>(`${bulletin_define_uri}/${name}`);
+  }
+
+  public deleteBulletinDefines(names: string[]): Observable<Message<any>> {
+    let params = new HttpParams();
+    names.forEach(name => {
+      params = params.append('names', name);
+    });
+
+    return this.http.delete<Message<any>>(bulletin_define_uri, { params });
+  }
+
+  public getMonitorMetricsData(
+    name: string,
+    pageIndex: number,
+    pageSize: number,
+    sortField?: string | null,
+    sortOrder?: string | null
+  ): Observable<Message<Page<Monitor>>> {
+    pageIndex = pageIndex ? pageIndex : 0;
+    pageSize = pageSize ? pageSize : 8;
+    // 注意HttpParams是不可变对象 需要保存set后返回的对象为最新对象
+    let httpParams = new HttpParams();
+    httpParams = httpParams.appendAll({
+      name: name,
+      pageIndex: pageIndex,
+      pageSize: pageSize
+    });
+    if (sortField != null && sortOrder != null) {
+      httpParams = httpParams.appendAll({
+        sort: sortField,
+        order: sortOrder == 'ascend' ? 'asc' : 'desc'
+      });
+    }
+    const options = { params: httpParams };
+    return this.http.get<Message<any>>(`${bulletin_define_uri}/metrics`, 
options);
+  }
+
+  public getAllNames(): Observable<Message<string[]>> {
+    return this.http.get<Message<string[]>>(`${bulletin_define_uri}/names`);
+  }
+}
diff --git a/web-app/src/app/service/monitor.service.ts 
b/web-app/src/app/service/monitor.service.ts
index 6b698be61..0849145cd 100644
--- a/web-app/src/app/service/monitor.service.ts
+++ b/web-app/src/app/service/monitor.service.ts
@@ -47,6 +47,10 @@ export class MonitorService {
     return this.http.put<Message<any>>(monitor_uri, body);
   }
 
+  public getMonitorByApp(app: string): Observable<Message<any>> {
+    return this.http.get<Message<any>>(`${monitor_uri}/metric/${app}`);
+  }
+
   public deleteMonitor(monitorId: number): Observable<Message<any>> {
     return this.http.delete<Message<any>>(`${monitor_uri}/${monitorId}`);
   }
diff --git a/web-app/src/assets/app-data.json b/web-app/src/assets/app-data.json
index 294d9922f..200d52c12 100644
--- a/web-app/src/assets/app-data.json
+++ b/web-app/src/assets/app-data.json
@@ -36,6 +36,12 @@
           "icon": "anticon-laptop",
           "link": "/monitors"
         },
+        {
+          "text": "Bulletin",
+          "i18n": "menu.monitor.bulletin",
+          "icon": "anticon-book",
+          "link": "/bulletin"
+        },
         {
           "text": "Define",
           "i18n": "menu.advanced.define",
diff --git a/web-app/src/assets/i18n/en-US.json 
b/web-app/src/assets/i18n/en-US.json
index 4a9cf506e..d73581fa6 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -10,6 +10,7 @@
     "monitor": {
       "": "Monitoring",
       "center": "Monitor Center",
+      "bulletin": "Bulletin",
       "service": "Service Monitor",
       "db": "DB Monitor",
       "os": "OS Monitor",
@@ -43,6 +44,20 @@
       "silence": "Alarm Silence",
       "dispatch": "Notification"
     },
+    "bulletin": {
+      "new": "Add New Bulletin Item",
+      "edit": "Edit Bulletin Item",
+      "delete": "Delete Bulletin Item",
+      "batch.delete": "Batch delete Bulletin Item",
+      "name": "Bulletin Name",
+      "name.placeholder": "Please enter a custom bulletin name",
+      "monitor.type": "Monitor Type",
+      "monitor.name": "Monitor Task Name",
+      "monitor.metrics": "Monitor Metrics",
+      "help.message.content": "Custom Monitoring Bulletin",
+      "help.content": "Custom monitoring bulletin, displaying selected metrics 
of a specific monitor in table form",
+      "help.link": ""
+    },
     "advanced": {
       "": "Advanced",
       "collector": "Collector Cluster",
diff --git a/web-app/src/assets/i18n/zh-CN.json 
b/web-app/src/assets/i18n/zh-CN.json
index 8218fe133..7271cc1ee 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -10,6 +10,7 @@
     "monitor": {
       "": "监控",
       "center": "监控中心",
+      "bulletin": "自定义看板",
       "service": "应用服务监控",
       "db": "数据库监控",
       "os": "操作系统监控",
@@ -130,6 +131,20 @@
       "2": "警告告警"
     }
   },
+  "bulletin": {
+    "new": "新增看板项",
+    "edit": "编辑看板项",
+    "delete": "删除看板项",
+    "batch.delete": "批量删除看板项",
+    "name": "看板名称",
+    "name.placeholder": "请输入自定义看板名称",
+    "monitor.type": "监控类型",
+    "monitor.name": "监控任务名称",
+    "monitor.metrics": "监控指标",
+    "help.message.content": "自定义监控看板",
+    "help.content": "自定义监控看板,以表格形式展示某种监控的自选指标",
+    "help.link": ""
+  },
   "question.link": "https://hertzbeat.apache.org/zh-cn/docs/help/issue";,
   "alert.setting.new": "新增阈值规则",
   "alert.setting.edit": "编辑阈值规则",
diff --git a/web-app/src/assets/i18n/zh-TW.json 
b/web-app/src/assets/i18n/zh-TW.json
index 671de56b5..60059f4c9 100644
--- a/web-app/src/assets/i18n/zh-TW.json
+++ b/web-app/src/assets/i18n/zh-TW.json
@@ -10,6 +10,7 @@
     "monitor": {
       "": "監控",
       "center": "監控中心",
+      "bulletin": "自定義看板",
       "service": "應用服務監控",
       "db": "數據庫監控",
       "os": "操作系統監控",
@@ -43,6 +44,20 @@
       "silence": "告警靜默",
       "dispatch": "消息通知"
     },
+    "bulletin": {
+      "new": "新增看板項",
+      "edit": "編輯看板項",
+      "delete": "刪除看板項",
+      "batch.delete": "批量刪除看板項",
+      "name": "看板名稱",
+      "name.placeholder": "請輸入自定義看板名稱",
+      "monitor.type": "監控類型",
+      "monitor.name": "監控任務名稱",
+      "monitor.metrics": "監控指標",
+      "help.message.content": "自定義監控看板",
+      "help.content": "自定義監控看板,以表格形式展示某種監控的自選指標",
+      "help.link": ""
+    },
     "advanced": {
       "": "高级",
       "collector": "採集集群",


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

Reply via email to