This is an automated email from the ASF dual-hosted git repository.
Aias00 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 bf6c451c94 [feat] support editing definitions and simplify metric
selection (#4158)
bf6c451c94 is described below
commit bf6c451c947c932b896f4a78aa056a64b4f7f729
Author: Duansg <[email protected]>
AuthorDate: Wed Jun 17 23:41:46 2026 +0800
[feat] support editing definitions and simplify metric selection (#4158)
---
.../manager/service/impl/BulletinServiceImpl.java | 3 +-
.../app/routes/bulletin/bulletin.component.html | 74 +++----
.../app/routes/bulletin/bulletin.component.less | 21 ++
.../src/app/routes/bulletin/bulletin.component.ts | 243 +++++++--------------
web-app/src/assets/i18n/en-US.json | 1 +
web-app/src/assets/i18n/ja-JP.json | 1 +
web-app/src/assets/i18n/pt-BR.json | 1 +
web-app/src/assets/i18n/zh-CN.json | 1 +
web-app/src/assets/i18n/zh-TW.json | 1 +
9 files changed, 132 insertions(+), 214 deletions(-)
diff --git
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java
index e5cc79dd94..f28cb309f9 100644
---
a/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java
+++
b/hertzbeat-manager/src/main/java/org/apache/hertzbeat/manager/service/impl/BulletinServiceImpl.java
@@ -78,7 +78,8 @@ public class BulletinServiceImpl implements BulletinService {
if (bulletin.getMonitorIds() == null ||
bulletin.getMonitorIds().isEmpty()) {
throw new IllegalArgumentException("Bulletin monitorIds cannot be
null or empty");
}
- if (bulletinDao.countByName(bulletin.getName()) > 0) {
+ Bulletin existBulletin = bulletinDao.findByName(bulletin.getName());
+ if (existBulletin != null &&
!existBulletin.getId().equals(bulletin.getId())) {
throw new IllegalArgumentException("Bulletin name duplicated");
}
}
diff --git a/web-app/src/app/routes/bulletin/bulletin.component.html
b/web-app/src/app/routes/bulletin/bulletin.component.html
index cb7479f64f..866a054af7 100644
--- a/web-app/src/app/routes/bulletin/bulletin.component.html
+++ b/web-app/src/app/routes/bulletin/bulletin.component.html
@@ -40,6 +40,12 @@
</button>
<nz-dropdown-menu #more_menu="nzDropdownMenu">
<ul nz-menu>
+ <li nz-menu-item>
+ <button nz-button (click)="onEditBulletinDefine()">
+ <i nz-icon nzType="edit" nzTheme="outline"></i>
+ {{ 'common.button.edit' | i18n }}
+ </button>
+ </li>
<li nz-menu-item>
<button nz-button nzDanger (click)="onDeleteBulletinDefines()">
<i nz-icon nzType="delete" nzTheme="outline"></i>
@@ -169,6 +175,7 @@
nzAllowClear
nzShowSearch
name="appDefines"
+ [nzDisabled]="!isManageModalAdd"
[(ngModel)]="define.app"
(nzOnSearch)="onSearchAppDefines()"
(ngModelChange)="onAppChange($event)"
@@ -181,71 +188,46 @@
</nz-form-control>
</nz-form-item>
- <nz-form-item>
- <nz-form-label nzSpan="7" [nzFor]="'filterLabels'">{{
'monitor.search.label' | i18n }} </nz-form-label>
- <nz-form-control nzSpan="8" [nzErrorTip]="'validation.required' |
i18n">
- <app-form-field
- [item]="{
- field: 'labels',
- type: 'label-selector',
- }"
- [name]="'labels'"
- [(ngModel)]="filterLabels"
- (ngModelChange)="onFilterInputChange($event)"
- />
- </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
+ nzServerSearch
name="monitors"
+ nzPlaceHolder="{{ 'bulletin.monitor.name.placeholder' | i18n }}"
[(ngModel)]="define.monitorIds"
- (nzOnSearch)="onSearchMonitorsByApp(define.app)"
+ (nzOnSearch)="onMonitorSearch($event)"
nzMode="multiple"
required
>
<ng-container *ngFor="let monitor of filteredMonitors">
- <nz-option *ngIf="isMonitorListLoading" [nzValue]="monitor.id"
[nzLabel]="monitor.name"></nz-option>
+ <nz-option *ngIf="isMonitorListLoading" [nzValue]="monitor.id"
[nzLabel]="monitor.name" nzCustomContent>
+ <span>{{ monitor.name }}</span>
+ <ng-container *ngFor="let label of monitor.labels | keyvalue">
+ <nz-tag class="monitor-label-tag">{{ label.key }}:{{
label.value }}</nz-tag>
+ </ng-container>
+ </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-label [nzSpan]="7" nzFor="metrics" 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)"
- (nzSelectChange)="onSelectChange($event)"
- >
- <ng-template #leftRenderList let-items
let-onItemSelect="onItemSelect" let-stat="stat">
- <div *ngIf="updateTransferStat(stat)"></div>
- <nz-tree
- [nzData]="treeNodes"
- nzExpandAll
- nzBlockNode
- nzCheckable
- nzCheckStrictly
- 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>
+ <div class="metrics-tree-box">
+ <nz-tree
+ #metricsTree
+ [nzData]="treeNodes"
+ [nzCheckedKeys]="checkedKeys"
+ nzCheckable
+ nzExpandAll
+ nzBlockNode
+ (nzCheckBoxChange)="onMetricsCheckChange()"
+ ></nz-tree>
+ </div>
</nz-form-control>
</nz-form-item>
</form>
diff --git a/web-app/src/app/routes/bulletin/bulletin.component.less
b/web-app/src/app/routes/bulletin/bulletin.component.less
index 0b13dcbfc0..289ad21893 100644
--- a/web-app/src/app/routes/bulletin/bulletin.component.less
+++ b/web-app/src/app/routes/bulletin/bulletin.component.less
@@ -1,4 +1,25 @@
@table-padding: 8px;
+
+// keep the metric tree looking like the inputs above: bordered, same min
height, scroll when filled
+.metrics-tree-box {
+ box-sizing: border-box;
+ min-height: 32px;
+ max-height: 260px;
+ padding: 4px 8px;
+ overflow: auto;
+ border: 1px solid #d9d9d9;
+ border-radius: 6px;
+ transition: border-color 0.3s;
+
+ &:hover {
+ border-color: #40a9ff;
+ }
+}
+
+.monitor-label-tag {
+ margin-left: 8px;
+}
+
.table {
width: 100%;
table-layout: auto;
diff --git a/web-app/src/app/routes/bulletin/bulletin.component.ts
b/web-app/src/app/routes/bulletin/bulletin.component.ts
index 36ee678fa4..353b1995a6 100644
--- a/web-app/src/app/routes/bulletin/bulletin.component.ts
+++ b/web-app/src/app/routes/bulletin/bulletin.component.ts
@@ -17,16 +17,14 @@
* under the License.
*/
-import { ChangeDetectorRef, Component, Inject, OnInit, OnDestroy } from
'@angular/core';
+import { ChangeDetectorRef, Component, Inject, OnInit, OnDestroy, ViewChild }
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, TransferSelectChange, TransferStat } from
'ng-zorro-antd/transfer';
-import { NzFormatEmitEvent, NzTreeNode, NzTreeNodeOptions } from
'ng-zorro-antd/tree';
-import { Subject } from 'rxjs';
-import { finalize, debounceTime, distinctUntilChanged } from 'rxjs/operators';
+import { NzTreeComponent, NzTreeNode, NzTreeNodeOptions } from
'ng-zorro-antd/tree';
+import { finalize } from 'rxjs/operators';
import { BulletinDefine } from '../../pojo/BulletinDefine';
import { Fields } from '../../pojo/Fields';
@@ -59,11 +57,12 @@ export class BulletinComponent implements OnInit, OnDestroy
{
deleteBulletinIds: number[] = [];
isAppListLoading = false;
isMonitorListLoading = false;
+ @ViewChild('metricsTree') metricsTree?: NzTreeComponent;
treeNodes!: NzTreeNodeOptions[];
+ checkedKeys: string[] = [];
hierarchies: NzTreeNodeOptions[] = [];
appMap = new Map<string, string>();
appEntries: Array<{ value: any; key: string }> = [];
- checkedNodeList: NzTreeNode[] = [];
monitors: Monitor[] = [];
metrics = new Set<string>();
fields: Fields = {};
@@ -73,36 +72,23 @@ export class BulletinComponent implements OnInit, OnDestroy
{
refreshInterval: any;
deadline = 30;
countDownTime: number = 0;
- filterLabels: Record<string, string> = {};
filteredMonitors: Monitor[] = [];
- private filterSubject = new Subject<string>(); //filter logic debouncing
- currentStat: TransferStat | null = null; //transfer component status
ngOnInit() {
this.loadTabs();
this.refreshInterval = setInterval(() => {
this.countDown();
}, 1000); // every 30 seconds refresh the tabs
- this.filterSubject
- .pipe(
- debounceTime(300), // triggered after the user stops typing for 300ms
- distinctUntilChanged()
- )
- .subscribe(value => {
- this.filterMonitors();
- });
}
ngOnDestroy() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
- this.filterSubject.complete();
}
sync() {
this.loadCurrentBulletinData();
- this.clearFilters();
}
configRefreshDeadline(deadlineTime: number) {
@@ -132,8 +118,12 @@ export class BulletinComponent implements OnInit,
OnDestroy {
onEditBulletinDefine() {
if (this.currentBulletin) {
- this.define = this.currentBulletin;
- this.onAppChange(this.define.app);
+ this.resetManageModalData();
+ this.define = JSON.parse(JSON.stringify(this.currentBulletin));
+ this.define.monitorIds = this.define.monitorIds || [];
+ this.fields = this.define.fields ?
JSON.parse(JSON.stringify(this.define.fields)) : {};
+ this.onSearchAppDefines();
+ this.onAppChange(this.define.app, true);
this.isManageModalAdd = false;
this.isManageModalVisible = true;
this.isManageModalOkLoading = false;
@@ -169,9 +159,7 @@ export class BulletinComponent implements OnInit, OnDestroy
{
onManageModalCancel() {
this.isManageModalVisible = false;
- // clear fields
this.fields = {};
- this.clearFilters();
}
resetManageModalData() {
@@ -179,12 +167,15 @@ export class BulletinComponent implements OnInit,
OnDestroy {
this.define.monitorIds = [];
this.hierarchies = [];
this.treeNodes = [];
- // clear fields
+ this.checkedKeys = [];
this.fields = {};
}
onManageModalOk() {
this.isManageModalOkLoading = true;
+ if (this.metricsTree) {
+ this.fields = this.collectFieldsFromTree();
+ }
this.define.fields = this.fields;
if (this.isManageModalAdd) {
const modalOk$ = this.bulletinDefineSvc
@@ -200,7 +191,6 @@ export class BulletinComponent implements OnInit, OnDestroy
{
if (message.code === 0) {
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.new-success'), '');
this.isManageModalVisible = false;
- // clear fields
this.fields = {};
this.resetManageModalData();
this.loadTabs();
@@ -268,7 +258,7 @@ export class BulletinComponent implements OnInit, OnDestroy
{
message => {
if (message.code === 0) {
this.monitors = message.data;
- this.filterMonitors();
+ this.filteredMonitors = [...this.monitors];
if (this.monitors != null) {
this.isMonitorListLoading = true;
}
@@ -282,17 +272,17 @@ export class BulletinComponent implements OnInit,
OnDestroy {
);
}
- onAppChange(app: string): void {
+ onAppChange(app: string, backfill: boolean = false): void {
if (app) {
this.onSearchMonitorsByApp(app);
- this.onSearchTreeNodes(app);
+ this.onSearchTreeNodes(app, backfill);
} else {
this.hierarchies = [];
this.treeNodes = [];
}
}
- onSearchTreeNodes(app: string): void {
+ onSearchTreeNodes(app: string, backfill: boolean = false): void {
this.appDefineSvc
.getAppHierarchyByName(this.i18nSvc.defaultLang, app)
.pipe()
@@ -300,7 +290,10 @@ export class BulletinComponent implements OnInit,
OnDestroy {
message => {
if (message.code === 0) {
this.hierarchies = this.transformToTransferItems(message.data);
+ // when editing, pre-check the metrics already saved on the
bulletin (drives parent cascade)
+ this.checkedKeys = backfill ? this.computeCheckedKeys() : [];
this.treeNodes = this.generateTree(this.hierarchies);
+ this.cdr.detectChanges();
} else {
console.warn(message.msg);
}
@@ -311,27 +304,38 @@ export class BulletinComponent implements OnInit,
OnDestroy {
);
}
+ // keys of the leaves whose field is already saved on the bulletin, used to
pre-check the tree when editing
+ private computeCheckedKeys(): string[] {
+ if (!this.fields || Object.keys(this.fields).length === 0) {
+ return [];
+ }
+ return this.hierarchies
+ .filter(item => item.isLeaf && this.fields[item.metric] &&
this.fields[item.metric].includes(item.value))
+ .map(item => item.key);
+ }
+
transformToTransferItems(data: any[]): NzTreeNodeOptions[] {
const result: NzTreeNodeOptions[] = [];
let currentId = 1;
- const traverse = (nodes: any[], parentKey: string | null = null, parentId:
number | null = null) => {
+ const traverse = (nodes: any[], parentValue: string | null = null,
parentId: number | null = null) => {
nodes.forEach(node => {
- const key = parentKey ? `${parentKey}` : node.value;
- const isRootNode = parentId === null;
+ const id = currentId++;
+ // `metric` carries the metric name (the parent group's value); leaves
use it as their fields key.
+ // `key` must be unique per node, otherwise nz-tree cannot track the
checkbox state correctly.
const item: NzTreeNodeOptions = {
- id: currentId++,
- key,
+ id,
+ key: `${id}`,
+ metric: parentValue ?? node.value,
value: node.value,
title: node.label,
isLeaf: node.isLeaf,
- parentId,
- disabled: isRootNode
+ parentId
};
result.push(item);
if (node.children) {
- traverse(node.children, key, item.id);
+ traverse(node.children, node.value, id);
}
});
};
@@ -369,108 +373,34 @@ export class BulletinComponent implements OnInit,
OnDestroy {
return tree;
}
- updateTransferStat(stat: TransferStat): boolean {
- this.currentStat = stat;
- // stat.shownCount = 33;
- return true;
- }
-
- private collectLeafNodes(nodes: NzTreeNodeOptions[], results:
NzTreeNodeOptions[]): void {
- nodes.forEach(node => {
- if (node.isLeaf && !node.disabled) {
- results.push(node);
- }
- if (node.children) {
- this.collectLeafNodes(node.children, results);
- }
- });
- }
-
- onSelectChange(event: TransferSelectChange): void {
- if (event.direction === 'left' && event.checked &&
this.currentStat?.checkAll) {
- const allLeafNodes: NzTreeNodeOptions[] = [];
- this.collectLeafNodes(this.treeNodes, allLeafNodes);
-
- allLeafNodes.forEach(node => {
- const existing = this.checkedNodeList.find(n => n.origin.id ===
node.id);
- if (!existing) {
- const treeNode = new NzTreeNode(node);
- treeNode.isChecked = true;
- this.checkedNodeList.push(treeNode);
- }
- });
- }
- if (event.direction === 'left' && !event.checked &&
this.currentStat?.checkAll === false) {
- this.checkedNodeList = [];
- }
- }
-
- treeCheckBoxChange(event: NzFormatEmitEvent, onItemSelect: (item:
NzTreeNodeOptions) => void): void {
- this.checkBoxChange(event.node!, onItemSelect);
+ onMetricsCheckChange(): void {
+ this.fields = this.collectFieldsFromTree();
}
- 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 => {
- // Check if each transferred node is in the left selected nodes
- const item = ret.list.find(w => w.value === node.origin.value);
- if (item) {
- // If it exists, disable the node and set it to checked
- node.isDisabled = true;
- node.isChecked = true;
- // If the key does not exist, create an empty array
- if (!this.fields[item.key]) {
- this.fields[item.key] = [];
- }
- // If the key exists but the value is not saved, add it to the value
array
- if (!this.fields[item.key].includes(item.value)) {
- this.fields[item.key].push(item.value);
- }
- }
- });
- }
- // delete
- else if (ret.to === 'left') {
- this.checkedNodeList.forEach(node => {
- // Check if each transferred node is in the left selected nodes
- const item = ret.list.find(w => w.value === node.origin.value);
- if (item) {
- // If it exists, enable the node and set it to unchecked
- node.isDisabled = false;
- node.isChecked = false;
- // If the key exists, delete the value
- if (this.fields[item.key]) {
- const index = this.fields[item.key].indexOf(item.value);
- if (index > -1) {
- this.fields[item.key].splice(index, 1);
+ // collect the checked leaf nodes of the metric tree into the `{ metric:
[field, ...] }` shape
+ private collectFieldsFromTree(): Fields {
+ const fields: Fields = {};
+ const roots = this.metricsTree ? this.metricsTree.getTreeNodes() : [];
+ const walk = (nodes: NzTreeNode[]) => {
+ nodes.forEach(node => {
+ if (node.isLeaf) {
+ if (node.isChecked) {
+ const metric = node.origin.metric;
+ const value = node.origin.value;
+ if (!fields[metric]) {
+ fields[metric] = [];
}
- // If the array under this key is empty, delete the key
- if (this.fields[item.key].length === 0) {
- delete this.fields[item.key];
+ if (!fields[metric].includes(value)) {
+ fields[metric].push(value);
}
}
+ } else if (node.children) {
+ walk(node.children);
}
});
- this.checkedNodeList = this.checkedNodeList.filter(item =>
item.isChecked);
- }
+ };
+ walk(roots);
+ return fields;
}
loadTabs() {
@@ -630,46 +560,25 @@ export class BulletinComponent implements OnInit,
OnDestroy {
return result;
}
- onFilterInputChange(value: string): void {
- this.filterSubject.next(value); // push the input value to the debounce
Subject
- }
-
- filterMonitors(): void {
- const validLabels = this.cleanFilterLabels(this.filterLabels);
- const activeFilters = Object.entries(validLabels).filter(([k, v]) => k !==
'' && k !== 'null'); // Second filtering ensures key validity
-
- if (activeFilters.length === 0) {
+ // search the monitor list on the client side, matching the term against the
monitor name,
+ // any label key, or any label value (the monitor list API already returns
labels with the data)
+ onMonitorSearch(value: string): void {
+ const term = (value ?? '').trim().toLowerCase();
+ if (!term) {
this.filteredMonitors = [...this.monitors];
return;
}
-
this.filteredMonitors = this.monitors.filter(monitor => {
- if (!monitor.labels || typeof monitor.labels !== 'object') return false;
-
- return activeFilters.every(([filterKey, filterValue]) => {
- const keyExists = Object.prototype.hasOwnProperty.call(monitor.labels,
filterKey);
- const actualValue = (monitor.labels[filterKey] ?? '').toLowerCase();
- return filterValue === '' ? keyExists :
actualValue.includes(filterValue.toLowerCase());
- });
- });
- }
-
- private cleanFilterLabels(labels: any): Record<string, string> {
- const cleaned: Record<string, string> = {};
- Object.entries(labels ?? {}).forEach(([k, v]) => {
- if (typeof k === 'string') {
- const trimmedKey = k.trim();
- if (trimmedKey !== '' && trimmedKey !== 'null') {
- cleaned[trimmedKey] = (v ?? '').toString().trim();
- }
+ if (monitor.name && monitor.name.toLowerCase().includes(term)) {
+ return true;
+ }
+ const labels = monitor.labels;
+ if (labels && typeof labels === 'object') {
+ return Object.entries(labels).some(
+ ([key, val]) => (key ?? '').toLowerCase().includes(term) || (val ??
'').toString().toLowerCase().includes(term)
+ );
}
+ return false;
});
- return cleaned;
- }
-
- clearFilters(): void {
- this.filterLabels = {};
- this.filterMonitors();
- this.cdr.detectChanges();
}
}
diff --git a/web-app/src/assets/i18n/en-US.json
b/web-app/src/assets/i18n/en-US.json
index 7095b07d2a..f3a8781df1 100644
--- a/web-app/src/assets/i18n/en-US.json
+++ b/web-app/src/assets/i18n/en-US.json
@@ -432,6 +432,7 @@
"bulletin.help.link": "https://hertzbeat.apache.org/docs/help/bulletin",
"bulletin.monitor.metrics": "Monitor Metrics",
"bulletin.monitor.name": "Monitor Task Name",
+ "bulletin.monitor.name.placeholder": "Search by name / label / value",
"bulletin.monitor.type": "Monitor Type",
"bulletin.name": "Bulletin Name",
"bulletin.name.placeholder": "Please enter a custom bulletin name",
diff --git a/web-app/src/assets/i18n/ja-JP.json
b/web-app/src/assets/i18n/ja-JP.json
index 3901bce547..1d0fc78779 100644
--- a/web-app/src/assets/i18n/ja-JP.json
+++ b/web-app/src/assets/i18n/ja-JP.json
@@ -393,6 +393,7 @@
"bulletin.help.link": "https://hertzbeat.apache.org/docs/help/bulletin",
"bulletin.monitor.metrics": "モニターメトリクス",
"bulletin.monitor.name": "モニタータスク名",
+ "bulletin.monitor.name.placeholder": "名前 / ラベル / 値で検索",
"bulletin.monitor.type": "モニタータイプ",
"bulletin.name": "速報名",
"bulletin.name.placeholder": "カスタム速報名を入力してください",
diff --git a/web-app/src/assets/i18n/pt-BR.json
b/web-app/src/assets/i18n/pt-BR.json
index 7b4acec48b..c6d1e0da48 100644
--- a/web-app/src/assets/i18n/pt-BR.json
+++ b/web-app/src/assets/i18n/pt-BR.json
@@ -1224,6 +1224,7 @@
"bulletin.help.link": "https://hertzbeat.apache.org/docs/help/bulletin",
"bulletin.monitor.metrics": "Métricas do Monitor",
"bulletin.monitor.name": "Nome do Monitor",
+ "bulletin.monitor.name.placeholder": "Pesquisar por nome / rótulo / valor",
"bulletin.monitor.type": "Tipo de Monitor",
"bulletin.name": "Nome do Boletim",
"bulletin.name.placeholder": "Digite o nome do boletim",
diff --git a/web-app/src/assets/i18n/zh-CN.json
b/web-app/src/assets/i18n/zh-CN.json
index 7921a5dab4..6d4a71744d 100644
--- a/web-app/src/assets/i18n/zh-CN.json
+++ b/web-app/src/assets/i18n/zh-CN.json
@@ -435,6 +435,7 @@
"bulletin.help.link":
"https://hertzbeat.apache.org/zh-cn/docs/help/bulletin",
"bulletin.monitor.metrics": "监控指标",
"bulletin.monitor.name": "监控任务名称",
+ "bulletin.monitor.name.placeholder": "按名称 / 标签 / 值搜索",
"bulletin.monitor.type": "监控类型",
"bulletin.name": "看板名称",
"bulletin.name.placeholder": "请输入自定义看板名称",
diff --git a/web-app/src/assets/i18n/zh-TW.json
b/web-app/src/assets/i18n/zh-TW.json
index 219d38891d..ce8ee447ff 100644
--- a/web-app/src/assets/i18n/zh-TW.json
+++ b/web-app/src/assets/i18n/zh-TW.json
@@ -397,6 +397,7 @@
"bulletin.help.link": "https://hertzbeat.apache.org/docs/help/bulletin",
"bulletin.monitor.metrics": "監控指標",
"bulletin.monitor.name": "監控任務名稱",
+ "bulletin.monitor.name.placeholder": "按名稱 / 標籤 / 值搜尋",
"bulletin.monitor.type": "監控類型",
"bulletin.name": "看板名稱",
"bulletin.name.placeholder": "請輸入自定義看板名稱",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]