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]