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

zhaoqingran pushed a commit to branch bulletin
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git

commit c4f528a41c32adc5e889377785e56825ac0ad30a
Author: zqr10159 <[email protected]>
AuthorDate: Wed Jul 3 17:25:34 2024 +0800

    [feature] Add monitor bulletin feature and related UI components.
    
    - Implement new monitor bulletin functionality.
    - Add corresponding UI components for bulletin management.
    - Update app-data.json to include new menu item for monitor bulletin.
    - Refactor bulletin.component.html and .spec.ts files.
---
 .../Bulletin.ts}                                   |  14 +
 .../app/routes/bulletin/bulletin.component.html    | 659 +++++++++++++++++
 .../app/routes/bulletin/bulletin.component.spec.ts |  25 +
 .../src/app/routes/bulletin/bulletin.component.ts  | 800 +++++++++++++++++++++
 web-app/src/app/routes/routes-routing.module.ts    |   2 +
 web-app/src/app/routes/routes.module.ts            |  17 +-
 web-app/src/assets/app-data.json                   |   6 +
 7 files changed, 1521 insertions(+), 2 deletions(-)

diff --git a/web-app/src/app/routes/bulletin/bulletin.component.spec.ts 
b/web-app/src/app/pojo/Bulletin.ts
similarity index 75%
copy from web-app/src/app/routes/bulletin/bulletin.component.spec.ts
copy to web-app/src/app/pojo/Bulletin.ts
index 042f3ce1f..c63c87185 100644
--- a/web-app/src/app/routes/bulletin/bulletin.component.spec.ts
+++ b/web-app/src/app/pojo/Bulletin.ts
@@ -16,3 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
+export class Bulletin {
+  id!: number;
+  name!: string;
+  app!: string;
+  host!: string;
+  // Monitoring status 0: Paused, 1: Up, 2: Down
+  status!: number;
+  metrics!: string[];
+  creator!: string;
+  modifier!: string;
+  gmtCreate!: number;
+  gmtUpdate!: number;
+}
diff --git a/web-app/src/app/routes/bulletin/bulletin.component.html 
b/web-app/src/app/routes/bulletin/bulletin.component.html
index 3c4a2ea9a..08350b9f7 100644
--- a/web-app/src/app/routes/bulletin/bulletin.component.html
+++ b/web-app/src/app/routes/bulletin/bulletin.component.html
@@ -16,3 +16,662 @@
   ~ specific language governing permissions and limitations
   ~ under the License.
 -->
+
+<app-help-message-show
+  [help_message_content]="'自定义监控看板'"
+  [guild_link]="'alert.help.center.setting' | 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>
+      新增看板项
+    </button>
+    <button nz-button nzType="primary" nzDanger 
(click)="onDeleteAlertDefines()">
+      <i nz-icon nzType="delete" nzTheme="outline"></i>
+      删除看板项
+    </button>
+
+    <button nz-button nz-dropdown [nzDropdownMenu]="more_menu">
+      <span nz-icon nzType="ellipsis"></span>
+    </button>
+    <nz-dropdown-menu #more_menu="nzDropdownMenu">
+      <ul nz-menu>
+        <li nz-menu-item>
+          <button nz-button nzType="primary" (click)="onExportDefines()">
+            <i nz-icon nzType="export" nzTheme="outline"></i>
+            {{ 'alert.setting.export' | i18n }}
+          </button>
+        </li>
+        <li nz-menu-item>
+          <nz-upload nzAction="/alert/defines/import" [nzLimit]="1" 
[nzShowUploadList]="false" (nzChange)="onImportDefines($event)">
+            <button nz-button nzType="primary">
+              <i nz-icon nzType="import" nzTheme="outline"></i>
+              {{ 'alert.setting.import' | i18n }}
+            </button>
+          </nz-upload>
+        </li>
+      </ul>
+    </nz-dropdown-menu>
+  </ng-template>
+  <ng-template #right>
+    <input
+      style="width: 200px; text-align: center"
+      nz-input
+      type="text"
+      [placeholder]="'alert.setting.search' | i18n"
+      nzSize="default"
+      (keyup.enter)="loadAlertDefineTable()"
+      [(ngModel)]="search"
+    />
+    <button nz-button nzType="primary" (click)="loadAlertDefineTable()">
+      {{ 'common.search' | i18n }}
+    </button>
+  </ng-template>
+</app-toolbar>
+
+<nz-table
+  #fixedTable
+  [nzData]="defines"
+  [nzPageIndex]="pageIndex"
+  [nzPageSize]="pageSize"
+  [nzTotal]="total"
+  nzFrontPagination="false"
+  [nzLoading]="tableLoading"
+  nzShowSizeChanger
+  [nzShowTotal]="rangeTemplate"
+  [nzPageSizeOptions]="[8, 15, 25]"
+  (nzQueryParams)="onTablePageChange($event)"
+  nzShowPagination="true"
+  [nzScroll]="{ x: '1240px' }"
+>
+  <thead>
+    <tr>
+      <th nzAlign="center" nzLeft nzWidth="3%" [(nzChecked)]="checkedAll" 
(nzCheckedChange)="onAllChecked($event)"></th>
+      <th nzAlign="center" nzWidth="16%">目标Host</th>
+      <th nzAlign="center" nzWidth="14%">{{ 'alert.setting.expr' | i18n }}</th>
+      <th nzAlign="center" nzWidth="8%">{{ 'alert.priority' | i18n }}</th>
+      <th nzAlign="center" nzWidth="8%">{{ 'alert.setting.times' | i18n }}</th>
+      <th nzAlign="center" nzWidth="20%">{{ 'alert.setting.template' | i18n 
}}</th>
+      <th nzAlign="center" nzWidth="8%">{{ 'alert.setting.default' | i18n 
}}</th>
+      <th nzAlign="center" nzWidth="8%">{{ 'alert.setting.enable' | i18n 
}}</th>
+      <th nzAlign="center" nzWidth="15%">{{ 'common.edit' | i18n }}</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr *ngFor="let data of fixedTable.data">
+      <td nzAlign="center" nzLeft [nzChecked]="checkedDefineIds.has(data.id)" 
(nzCheckedChange)="onItemChecked(data.id, $event)"></td>
+      <td nzAlign="center">
+        <span *ngIf="data.field">
+          {{ 'monitor.app.' + data.app | i18n }} / {{ 'monitor.app.' + 
data.app + '.metrics.' + data.metric | i18nElse : data.metric }} /
+          {{ 'monitor.app.' + data.app + '.metrics.' + data.metric + 
'.metric.' + data.field | i18nElse : data.field }}
+        </span>
+        <span *ngIf="!data.field && data.metric === 'availability'">
+          {{ 'monitor.app.' + data.app | i18n }} / {{ 'monitor.availability' | 
i18n }}
+        </span>
+      </td>
+      <td nzAlign="center">
+        <span>{{ data.expr }}</span>
+      </td>
+      <td nzAlign="center">
+        <nz-tag *ngIf="data.priority == 0" nzColor="red">
+          <i nz-icon nzType="bell" nzTheme="outline"></i>
+          <span>{{ 'alert.priority.0' | i18n }}</span>
+        </nz-tag>
+        <nz-tag *ngIf="data.priority == 1" nzColor="orange">
+          <i nz-icon nzType="bell" nzTheme="outline"></i>
+          <span>{{ 'alert.priority.1' | i18n }}</span>
+        </nz-tag>
+        <nz-tag *ngIf="data.priority == 2" nzColor="yellow">
+          <i nz-icon nzType="bell" nzTheme="outline"></i>
+          <span>{{ 'alert.priority.2' | i18n }}</span>
+        </nz-tag>
+      </td>
+      <td nzAlign="center">{{ data.times }}</td>
+      <td nzAlign="center">{{ data.template }}</td>
+      <td nzAlign="center">
+        <nz-switch [(ngModel)]="data.preset" 
(ngModelChange)="updateAlertDefine(data)" name="preset"></nz-switch>
+      </td>
+      <td nzAlign="center">
+        <nz-switch [(ngModel)]="data.enable" 
(ngModelChange)="updateAlertDefine(data)" name="enable"></nz-switch>
+      </td>
+      <td nzAlign="center">
+        <button
+          nz-button
+          nzType="primary"
+          (click)="onOpenConnectModal(data.id, data.app)"
+          nz-tooltip
+          [disabled]="data.preset"
+          [nzTooltipTitle]="'alert.setting.connect' | i18n"
+        >
+          <i nz-icon nzType="link" nzTheme="outline"></i>
+        </button>
+        <button
+          nz-button
+          nzType="primary"
+          (click)="onEditOneAlertDefine(data.id)"
+          nz-tooltip
+          [nzTooltipTitle]="'alert.setting.edit' | i18n"
+        >
+          <i nz-icon nzType="edit" nzTheme="outline"></i>
+        </button>
+        <button
+          nz-button
+          nzDanger
+          nzType="primary"
+          (click)="onDeleteOneAlertDefine(data.id)"
+          nz-tooltip
+          [nzTooltipTitle]="'alert.setting.delete' | i18n"
+        >
+          <i nz-icon nzType="delete" nzTheme="outline"></i>
+        </button>
+      </td>
+    </tr>
+  </tbody>
+</nz-table>
+
+<ng-template #rangeTemplate> {{ 'common.total' | i18n }} {{ total }} 
</ng-template>
+
+<!-- 新增或修改告警定义弹出框 -->
+<nz-modal
+  [(nzVisible)]="isManageModalVisible"
+  [nzTitle]="isManageModalAdd ? ('alert.setting.new' | i18n) : 
('alert.setting.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="target" nzRequired="true">{{ 
'alert.setting.target' | i18n }}</nz-form-label>
+        <nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | 
i18n">
+          <nz-cascader
+            required
+            name="target"
+            id="target"
+            [nzShowSearch]="{ filter: caseInsensitiveFilter }"
+            [nzPlaceHolder]="'alert.setting.target.place-holder' | i18n"
+            [nzOptions]="appHierarchies"
+            [(ngModel)]="cascadeValues"
+            (ngModelChange)="cascadeOnChange($event)"
+          ></nz-cascader>
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item>
+        <nz-form-label [nzSpan]="7" nzFor="rule" nzRequired="true" 
[nzTooltipTitle]="'alert.setting.rule.label' | i18n">
+          {{ 'alert.setting.rule' | i18n }}
+        </nz-form-label>
+        <nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | 
i18n">
+          <nz-radio-group
+            [(ngModel)]="isExpr"
+            (ngModelChange)="switchAlertRuleShow()"
+            nzButtonStyle="solid"
+            [required]="'true'"
+            name="isExpr"
+            id="isExpr"
+          >
+            <label nz-radio-button [nzValue]="false">
+              {{ 'alert.setting.rule.switch-expr.0' | i18n }}
+            </label>
+            <label nz-radio-button [nzValue]="true">
+              {{ 'alert.setting.rule.switch-expr.1' | i18n }}
+            </label>
+          </nz-radio-group>
+          <div *ngIf="cascadeValues.length != 2 && isExpr" style="margin-top: 
5px">
+            <nz-textarea-count [nzMaxCharacterCount]="100">
+              <textarea
+                [(ngModel)]="define.expr"
+                required
+                rows="3"
+                nz-input
+                name="expr"
+                id="expr"
+                [placeholder]="('alert.setting.expr.example' | i18n) + ': 
responseTime&gt;40'"
+              >
+              </textarea>
+            </nz-textarea-count>
+            <nz-collapse style="margin-top: 20px">
+              <nz-collapse-panel [nzActive]="isManageModalAdd" 
[nzHeader]="'alert.setting.expr.tip' | i18n">
+                <nz-list nzSize="small" nzSplit="false">
+                  <nz-list-item *ngFor="let item of currentMetrics; let i = 
index">
+                    <code>
+                      {{ item.value }} :
+                      {{
+                        item.value == item.label
+                          ? i == 0
+                            ? ('alert.setting.target.tip' | i18n)
+                            : ('alert.setting.target.other' | i18n)
+                          : item.label
+                      }}
+                    </code>
+                    <nz-tag [nzColor]="item.type === 0 ? 'success' : 
'processing'">
+                      {{ item.type === 0 ? ('alert.setting.number' | i18n) : 
('alert.setting.string' | i18n) }}
+                    </nz-tag>
+                  </nz-list-item>
+                  <nz-list-item>
+                    <code>
+                      {{ 'alert.setting.operator' | i18n }} : 
equals(str1,str2), contains(str1,str2), exists(keyName), matches(str,regex),
+                      ==, <, <=, >, >=, !=, ( ), +, -, &&, ||
+                    </code>
+                  </nz-list-item>
+                </nz-list>
+              </nz-collapse-panel>
+            </nz-collapse>
+          </div>
+          <div *ngIf="cascadeValues.length != 2 && !isExpr" style="margin-top: 
5px">
+            <div id="rule">
+              <div style="width: 100%; margin-bottom: 2px" nz-row *ngFor="let 
alertRule of alertRules; let i = index">
+                <nz-select
+                  [(ngModel)]="alertRule.metric"
+                  [ngModelOptions]="{ standalone: true }"
+                  nz-col
+                  nzSpan="8"
+                  [nzDropdownMatchSelectWidth]="false"
+                  [nzPlaceHolder]="'alert.setting.rule.metric.place-holder' | 
i18n"
+                >
+                  <nz-option
+                    *ngFor="let item of currentMetrics"
+                    [nzValue]="item"
+                    [nzLabel]="item.label ? item.label : item.value"
+                    nzCustomContent
+                  >
+                    {{ item.label ? item.label : item.value }}
+                    <nz-tag [nzColor]="item.type === 0 ? 'success' : 
'processing'">
+                      {{
+                        item.type === 0
+                          ? ('alert.setting.number' | i18n)
+                          : item.type === 3
+                          ? ('alert.setting.time' | i18n)
+                          : ('alert.setting.string' | i18n)
+                      }}
+                    </nz-tag>
+                    <nz-tag *ngIf="item.unit">
+                      {{ item.unit }}
+                    </nz-tag>
+                  </nz-option>
+                </nz-select>
+                <nz-select
+                  [(ngModel)]="alertRule.operator"
+                  [ngModelOptions]="{ standalone: true }"
+                  nz-col
+                  nzSpan="4"
+                  [nzShowArrow]="false"
+                  [nzDropdownMatchSelectWidth]="false"
+                  style="text-align: center; font-weight: bolder"
+                  [nzDropdownStyle]="{ 'text-align': 'center', 'font-weight': 
'bolder', 'font-size': 'larger' }"
+                  [nzPlaceHolder]="'alert.setting.rule.operator' | i18n"
+                >
+                  <nz-option
+                    *ngIf="!alertRule.metric || alertRule.metric.type === 0 || 
alertRule.metric.type === 3"
+                    [nzValue]="'>'"
+                    [nzLabel]="'>'"
+                  ></nz-option>
+                  <nz-option
+                    *ngIf="!alertRule.metric || alertRule.metric.type === 0 || 
alertRule.metric.type === 3"
+                    [nzValue]="'<'"
+                    [nzLabel]="'<'"
+                  ></nz-option>
+                  <nz-option
+                    *ngIf="!alertRule.metric || alertRule.metric.type === 0 || 
alertRule.metric.type === 3"
+                    [nzValue]="'=='"
+                    [nzLabel]="'=='"
+                  ></nz-option>
+                  <nz-option
+                    *ngIf="!alertRule.metric || alertRule.metric.type === 0 || 
alertRule.metric.type === 3"
+                    [nzValue]="'!='"
+                    [nzLabel]="'!='"
+                  ></nz-option>
+                  <nz-option
+                    *ngIf="!alertRule.metric || alertRule.metric.type === 0 || 
alertRule.metric.type === 3"
+                    [nzValue]="'<='"
+                    [nzLabel]="'<='"
+                  ></nz-option>
+                  <nz-option
+                    *ngIf="!alertRule.metric || alertRule.metric.type === 0 || 
alertRule.metric.type === 3"
+                    [nzValue]="'>='"
+                    [nzLabel]="'>='"
+                  ></nz-option>
+                  <nz-option
+                    *ngIf="!alertRule.metric || alertRule.metric.type === 1"
+                    [nzValue]="'equals'"
+                    [nzLabel]="'alert.setting.rule.operator.str-equals' | i18n"
+                  ></nz-option>
+                  <nz-option
+                    *ngIf="!alertRule.metric || alertRule.metric.type === 1"
+                    [nzValue]="'!equals'"
+                    [nzLabel]="'alert.setting.rule.operator.str-no-equals' | 
i18n"
+                  ></nz-option>
+                  <nz-option
+                    *ngIf="!alertRule.metric || alertRule.metric.type === 1"
+                    [nzValue]="'contains'"
+                    [nzLabel]="'alert.setting.rule.operator.str-contains' | 
i18n"
+                  ></nz-option>
+                  <nz-option
+                    *ngIf="!alertRule.metric || alertRule.metric.type === 1"
+                    [nzValue]="'!contains'"
+                    [nzLabel]="'alert.setting.rule.operator.str-no-contains' | 
i18n"
+                  ></nz-option>
+                  <nz-option
+                    *ngIf="!alertRule.metric || alertRule.metric.type === 1"
+                    [nzValue]="'matches'"
+                    [nzLabel]="'alert.setting.rule.operator.str-matches' | 
i18n"
+                  ></nz-option>
+                  <nz-option
+                    *ngIf="!alertRule.metric || alertRule.metric.type === 1"
+                    [nzValue]="'!matches'"
+                    [nzLabel]="'alert.setting.rule.operator.str-no-matches' | 
i18n"
+                  ></nz-option>
+                  <nz-option [nzValue]="'exists'" 
[nzLabel]="'alert.setting.rule.operator.exists' | i18n"></nz-option>
+                  <nz-option [nzValue]="'!exists'" 
[nzLabel]="'alert.setting.rule.operator.no-exists' | i18n"></nz-option>
+                </nz-select>
+                <input
+                  nz-input
+                  [disabled]="alertRule.operator == 'exists' || 
alertRule.operator == '!exists'"
+                  [type]="alertRule.metric?.type === 0 ? 'number' : 'text'"
+                  [(ngModel)]="alertRule.value"
+                  [ngModelOptions]="{ standalone: true }"
+                  [placeholder]="
+                    alertRule.operator == 'exists' || alertRule.operator == 
'!exists'
+                      ? ''
+                      : alertRule.metric?.type === 0
+                      ? ('alert.setting.rule.numeric-value.place-holder' | 
i18n)
+                      : ('alert.setting.rule.string-value.place-holder' | i18n)
+                  "
+                  nz-col
+                  nzSpan="10"
+                />
+                <button
+                  *ngIf="i != alertRules.length - 1 || i == 4"
+                  nz-button
+                  nz-col
+                  nzSpan="2"
+                  nzDanger
+                  nzGhost="true"
+                  (click)="onRemoveAlertRule(i)"
+                >
+                  <span nz-icon nzType="minus"></span>
+                </button>
+                <button
+                  *ngIf="i === alertRules.length - 1 && i < 4"
+                  nz-button
+                  nzType="primary"
+                  nz-col
+                  nzSpan="2"
+                  nzGhost="true"
+                  (click)="onAddNewAlertRule()"
+                >
+                  <span nz-icon nzType="plus"></span>
+                </button>
+              </div>
+            </div>
+          </div>
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item *ngIf="cascadeValues.length == 2">
+        <nz-form-label [nzSpan]="7" nzFor="available">
+          {{ 'monitor.availability' | i18n }}
+        </nz-form-label>
+        <nz-form-control [nzSpan]="10">
+          <span style="color: red">
+            <nz-tag nzColor="error">{{ 'monitor.status.down' | i18n }}</nz-tag>
+            <nz-tag nzColor="error">{{ 'monitor.status.unreachable' | i18n 
}}</nz-tag>
+            {{ 'alert.setting.trigger' | i18n }}
+          </span>
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item>
+        <nz-form-label nzSpan="7" nzRequired="true" nzFor="priority" 
[nzTooltipTitle]="'alert.setting.priority.tip' | i18n">
+          {{ 'alert.priority' | i18n }}
+        </nz-form-label>
+        <nz-form-control nzSpan="12" [nzErrorTip]="'validation.required' | 
i18n">
+          <nz-select
+            [(ngModel)]="define.priority"
+            [nzPlaceHolder]="'alert.notice.rule.priority.placeholder' | i18n"
+            name="priority"
+            id="priority"
+          >
+            <nz-option [nzValue]="0" [nzLabel]="'alert.priority.0' | 
i18n"></nz-option>
+            <nz-option [nzValue]="1" [nzLabel]="'alert.priority.1' | 
i18n"></nz-option>
+            <nz-option [nzValue]="2" [nzLabel]="'alert.priority.2' | 
i18n"></nz-option>
+          </nz-select>
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item>
+        <nz-form-label nzSpan="7" nzRequired="true" nzFor="duration" 
[nzTooltipTitle]="'alert.setting.times.tip' | i18n">
+          {{ 'alert.setting.times' | i18n }}
+        </nz-form-label>
+        <nz-form-control nzSpan="12" [nzErrorTip]="'validation.required' | 
i18n">
+          <nz-input-number [(ngModel)]="define.times" [nzMin]="1" 
[nzMax]="999" [nzStep]="1" required name="duration" id="duration">
+          </nz-input-number>
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item>
+        <nz-form-label [nzSpan]="7" nzFor="template" nzRequired="true" 
[nzTooltipTitle]="'alert.setting.template.label' | i18n">
+          {{ 'alert.setting.template' | i18n }}
+        </nz-form-label>
+        <nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | 
i18n">
+          <nz-textarea-count [nzMaxCharacterCount]="200">
+            <textarea
+              [(ngModel)]="define.template"
+              rows="3"
+              nz-input
+              required
+              name="template"
+              id="template"
+              [placeholder]="'alert.setting.template.example' | i18n"
+            >
+            </textarea>
+          </nz-textarea-count>
+          <nz-collapse style="margin-top: 20px">
+            <nz-collapse-panel [nzActive]="isManageModalAdd" 
[nzHeader]="'alert.setting.template.tip' | i18n">
+              <nz-list nzSize="small" nzSplit="false">
+                <nz-list-item>
+                  <code>&#36;&#123;app&#125; : {{ 
'alert.setting.template.monitor-type' | i18n }}</code>
+                </nz-list-item>
+                <nz-list-item>
+                  <code>&#36;&#123;metrics&#125; : {{ 
'alert.setting.template.metrics-name' | i18n }}</code>
+                </nz-list-item>
+                <nz-list-item *ngIf="cascadeValues.length == 3">
+                  <code>&#36;&#123;metric&#125; : {{ 
'alert.setting.template.metric-name' | i18n }}</code>
+                </nz-list-item>
+                <nz-list-item *ngFor="let item of 
filterMetrics(currentMetrics, cascadeValues)">
+                  <code
+                    >&#36;{{ '{' + item.value + '}' }} :
+                    {{
+                      item.value == item.label
+                        ? ('alert.setting.template.other-value' | i18n)
+                        : ('alert.setting.template.metric-value' | i18n) + '-' 
+ item.label
+                    }}</code
+                  >
+                </nz-list-item>
+              </nz-list>
+            </nz-collapse-panel>
+          </nz-collapse>
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item>
+        <nz-form-label nzSpan="7" nzFor="tags" 
[nzTooltipTitle]="'tag.bind.tip' | i18n">
+          {{ 'tag.bind' | i18n }}
+        </nz-form-label>
+        <nz-form-control nzSpan="8">
+          <nz-tag
+            *ngFor="let tag of define.tags; let i = index"
+            [nzMode]="'closeable'"
+            (nzOnClose)="onRemoveTag(tag)"
+            style="margin-top: 4px"
+          >
+            {{ sliceTagName(tag) }}
+          </nz-tag>
+          <a (click)="onShowTagsModal()">
+            <nz-tag style="margin-top: 4px">
+              <i nz-icon nzType="plus"></i>
+              {{ 'tag.new' | i18n }}
+            </nz-tag>
+          </a>
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item>
+        <nz-form-label nzSpan="7" nzFor="preset" 
[nzTooltipTitle]="'alert.setting.default.tip' | i18n">
+          {{ 'alert.setting.default' | i18n }}
+        </nz-form-label>
+        <nz-form-control nzSpan="12">
+          <nz-switch [(ngModel)]="define.preset" name="preset" 
id="preset"></nz-switch>
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item>
+        <nz-form-label nzSpan="7" nzFor="recoverNotice" 
[nzTooltipTitle]="'alert.setting.recover-notice.tip' | i18n">
+          {{ 'alert.setting.recover-notice' | i18n }}
+        </nz-form-label>
+        <nz-form-control nzSpan="12">
+          <nz-switch
+            [(ngModel)]="define.recoverNotice"
+            [ngModelOptions]="{ standalone: true }"
+            name="recoverNotice"
+            id="recoverNotice"
+          ></nz-switch>
+        </nz-form-control>
+      </nz-form-item>
+      <nz-form-item>
+        <nz-form-label nzSpan="7" nzRequired="true" nzFor="enable" 
[nzTooltipTitle]="'alert.setting.enable.tip' | i18n">
+          {{ 'alert.setting.enable' | i18n }}
+        </nz-form-label>
+        <nz-form-control nzSpan="12">
+          <nz-switch [(ngModel)]="define.enable" [ngModelOptions]="{ 
standalone: true }" name="enable" id="enable"></nz-switch>
+        </nz-form-control>
+      </nz-form-item>
+    </form>
+  </div>
+</nz-modal>
+
+<!-- 选择TAG弹出框 -->
+<nz-modal
+  [(nzVisible)]="isTagManageModalVisible"
+  [nzTitle]="'tag.bind' | i18n"
+  (nzOnCancel)="onTagManageModalCancel()"
+  (nzOnOk)="onTagManageModalOk()"
+  nzMaskClosable="false"
+  nzWidth="30%"
+  [nzOkLoading]="isTagManageModalOkLoading"
+>
+  <div *nzModalContent class="-inner-content">
+    <input
+      style="margin-left: 5px; width: 50%; text-align: center"
+      nz-input
+      type="text"
+      [placeholder]="'tag.search' | i18n"
+      nzSize="default"
+      (keyup.enter)="loadTagsTable()"
+      [(ngModel)]="tagSearch"
+    />
+    <button nz-button nzType="primary" routerLink="/setting/tags" 
style="margin-left: 5px">
+      <i nz-icon nzType="setting" nzTheme="outline"></i>
+      {{ 'tag.setting' | i18n }}
+    </button>
+    <nz-table #smallTable nzSize="small" [nzData]="tags" [nzPageSize]="8" 
[nzLoading]="tagTableLoading">
+      <thead>
+        <tr>
+          <th nzAlign="center" nzLeft nzWidth="4%" 
[(nzChecked)]="tagCheckedAll" (nzCheckedChange)="onTagAllChecked($event)"></th>
+          <th nzAlign="left">{{ 'tag' | i18n }}</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr *ngFor="let data of smallTable.data">
+          <td nzAlign="center" nzLeft [nzChecked]="checkedTags.has(data)" 
(nzCheckedChange)="onTagItemChecked(data, $event)"></td>
+          <td nzAlign="left">
+            <nz-tag *ngIf="data.tagValue == undefined || data.tagValue.trim() 
== ''" [nzColor]="data.color">{{ data.name }}</nz-tag>
+            <nz-tag *ngIf="data.tagValue != undefined && data.tagValue.trim() 
!= ''" [nzColor]="data.color">
+              {{ data.name + ':' + data.tagValue }}
+            </nz-tag>
+          </td>
+        </tr>
+      </tbody>
+    </nz-table>
+  </div>
+</nz-modal>
+
+<!-- 关联告警定义与监控关系弹出框 -->
+
+<nz-modal
+  [(nzVisible)]="isConnectModalVisible"
+  [nzTitle]="'alert.setting.connect' | i18n"
+  (nzOnCancel)="onConnectModalCancel()"
+  (nzOnOk)="onConnectModalOk()"
+  nzMaskClosable="false"
+  nzWidth="60%"
+  [nzOkLoading]="isConnectModalOkLoading"
+>
+  <nz-transfer
+    *nzModalContent
+    [nzDataSource]="transferData"
+    nzShowSearch="true"
+    nzShowSelectAll="false"
+    [nzRenderList]="[renderList, renderList]"
+    (nzChange)="change($event)"
+    style="overflow-x: scroll"
+  >
+    <ng-template
+      #renderList
+      let-items
+      let-direction="direction"
+      let-stat="stat"
+      let-onItemSelectAll="onItemSelectAll"
+      let-onItemSelect="onItemSelect"
+    >
+      <nz-table #t [nzData]="$asTransferItems(items)" nzSize="small">
+        <thead>
+          <tr>
+            <th [nzChecked]="stat.checkAll" [nzIndeterminate]="stat.checkHalf" 
(nzCheckedChange)="onItemSelectAll($event)"></th>
+            <th *ngIf="direction == 'left'">{{ 'alert.setting.connect.left' | 
i18n }}</th>
+            <th *ngIf="direction == 'right'">{{ 'alert.setting.connect.right' 
| i18n }}</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr *ngFor="let data of t.data" (click)="onItemSelect(data)">
+            <td [nzChecked]="!!data.checked" 
(nzCheckedChange)="onItemSelect(data)"></td>
+            <td>{{ data.name }}</td>
+          </tr>
+        </tbody>
+      </nz-table>
+    </ng-template>
+  </nz-transfer>
+</nz-modal>
+
+<!-- 导出告警定义弹出框 -->
+<nz-modal
+  [(nzVisible)]="isSwitchExportTypeModalVisible"
+  [nzTitle]="'alert.export.switch-type' | i18n"
+  (nzOnCancel)="onExportTypeModalCancel()"
+  nzOkDisabled="true"
+  [nzFooter]="switchExportTypeModalFooter"
+>
+  <ng-container *nzModalContent>
+    <p style="text-align: center">
+      <button nz-button nzType="primary" nzSize="large" 
(click)="exportDefines('YAML')" [nzLoading]="exportYamlButtonLoading">
+        <span nz-icon nzType="download"></span>
+        {{ 'alert.export.use-type' | i18n : { type: 'YAML' } }}
+      </button>
+    </p>
+    <p style="text-align: center">
+      <button nz-button nzType="primary" nzSize="large" 
(click)="exportDefines('JSON')" [nzLoading]="exportJsonButtonLoading">
+        <span nz-icon nzType="download"></span>
+        {{ 'alert.export.use-type' | i18n : { type: 'JSON' } }}
+      </button>
+    </p>
+    <p style="text-align: center">
+      <button nz-button nzType="primary" nzSize="large" 
(click)="exportDefines('EXCEL')" [nzLoading]="exportExcelButtonLoading">
+        <span nz-icon nzType="download"></span>
+        {{ 'alert.export.use-type' | i18n : { type: 'EXCEL' } }}
+      </button>
+    </p>
+  </ng-container>
+</nz-modal>
diff --git a/web-app/src/app/routes/bulletin/bulletin.component.spec.ts 
b/web-app/src/app/routes/bulletin/bulletin.component.spec.ts
index 042f3ce1f..5e1fc1040 100644
--- a/web-app/src/app/routes/bulletin/bulletin.component.spec.ts
+++ b/web-app/src/app/routes/bulletin/bulletin.component.spec.ts
@@ -16,3 +16,28 @@
  * 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
index 042f3ce1f..0b0348d9d 100644
--- a/web-app/src/app/routes/bulletin/bulletin.component.ts
+++ b/web-app/src/app/routes/bulletin/bulletin.component.ts
@@ -16,3 +16,803 @@
  * 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 { NzCascaderFilter } from 'ng-zorro-antd/cascader';
+import { ModalButtonOptions, NzModalService } from 'ng-zorro-antd/modal';
+import { NzNotificationService } from 'ng-zorro-antd/notification';
+import { NzTableQueryParams } from 'ng-zorro-antd/table';
+import { TransferChange, TransferItem } from 'ng-zorro-antd/transfer';
+import { NzUploadChangeParam } from 'ng-zorro-antd/upload';
+import { zip } from 'rxjs';
+import { finalize, map } from 'rxjs/operators';
+
+import { AlertDefine } from '../../pojo/AlertDefine';
+import { AlertDefineBind } from '../../pojo/AlertDefineBind';
+import { Message } from '../../pojo/Message';
+import { Monitor } from '../../pojo/Monitor';
+import { TagItem } from '../../pojo/NoticeRule';
+import { Tag } from '../../pojo/Tag';
+import { AlertDefineService } from '../../service/alert-define.service';
+import { AppDefineService } from '../../service/app-define.service';
+import { MonitorService } from '../../service/monitor.service';
+import { TagService } from '../../service/tag.service';
+
+const AVAILABILITY = 'availability';
+
+@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 alertDefineSvc: AlertDefineService,
+    private tagSvc: TagService,
+    @Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService
+  ) {}
+  search!: string;
+  pageIndex: number = 1;
+  pageSize: number = 8;
+  total: number = 0;
+  defines!: AlertDefine[];
+  tableLoading: boolean = true;
+  checkedDefineIds = new Set<number>();
+  isSwitchExportTypeModalVisible = false;
+  exportJsonButtonLoading = false;
+  exportYamlButtonLoading = false;
+  exportExcelButtonLoading = false;
+  appHierarchies!: any[];
+  switchExportTypeModalFooter: ModalButtonOptions[] = [
+    { label: this.i18nSvc.fanyi('common.button.cancel'), type: 'default', 
onClick: () => (this.isSwitchExportTypeModalVisible = false) }
+  ];
+  ngOnInit(): void {
+    this.loadAlertDefineTable();
+    // 查询监控层级
+    const getHierarchy$ = this.appDefineSvc
+      .getAppHierarchy(this.i18nSvc.defaultLang)
+      .pipe(
+        finalize(() => {
+          getHierarchy$.unsubscribe();
+        })
+      )
+      .subscribe(
+        message => {
+          if (message.code === 0) {
+            this.appHierarchies = message.data;
+            this.appHierarchies.forEach(item => {
+              if (item.children == undefined) {
+                item.children = [];
+              }
+              item.children.unshift({
+                value: AVAILABILITY,
+                label: this.i18nSvc.fanyi('monitor.availability'),
+                isLeaf: true
+              });
+            });
+          } else {
+            console.warn(message.msg);
+          }
+        },
+        error => {
+          console.warn(error.msg);
+        }
+      );
+  }
+
+  sync() {
+    this.loadAlertDefineTable();
+  }
+
+  loadAlertDefineTable() {
+    this.tableLoading = true;
+    let alertDefineInit$ = this.alertDefineSvc.getAlertDefines(this.search, 
this.pageIndex - 1, this.pageSize).subscribe(
+      message => {
+        this.tableLoading = false;
+        this.checkedAll = false;
+        this.checkedDefineIds.clear();
+        if (message.code === 0) {
+          let page = message.data;
+          this.defines = page.content;
+          this.pageIndex = page.number + 1;
+          this.total = page.totalElements;
+        } else {
+          console.warn(message.msg);
+        }
+        alertDefineInit$.unsubscribe();
+      },
+      error => {
+        this.tableLoading = false;
+        alertDefineInit$.unsubscribe();
+      }
+    );
+  }
+
+  onNewBulletinDefine() {
+    this.define = new AlertDefine();
+    this.define.tags = [];
+    this.isManageModalAdd = true;
+    this.isManageModalVisible = true;
+    this.isManageModalOkLoading = false;
+  }
+
+  onEditOneAlertDefine(alertDefineId: number) {
+    if (alertDefineId == null) {
+      
this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-edit'), '');
+      return;
+    }
+    this.editAlertDefine(alertDefineId);
+  }
+
+  onEditAlertDefine() {
+    // 编辑时只能选中一个
+    if (this.checkedDefineIds == null || this.checkedDefineIds.size === 0) {
+      
this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-edit'), '');
+      return;
+    }
+    if (this.checkedDefineIds.size > 1) {
+      
this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.one-select-edit'), '');
+      return;
+    }
+    let alertDefineId = 0;
+    this.checkedDefineIds.forEach(item => (alertDefineId = item));
+    this.editAlertDefine(alertDefineId);
+  }
+
+  updateAlertDefine(alertDefine: AlertDefine) {
+    this.tableLoading = true;
+    const updateDefine$ = this.alertDefineSvc
+      .editAlertDefine(alertDefine)
+      .pipe(
+        finalize(() => {
+          updateDefine$.unsubscribe();
+          this.tableLoading = false;
+        })
+      )
+      .subscribe(
+        message => {
+          if (message.code === 0) {
+            
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.edit-success'), '');
+          } else {
+            
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), 
message.msg);
+          }
+          this.loadAlertDefineTable();
+          this.tableLoading = false;
+        },
+        error => {
+          this.tableLoading = false;
+          this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), 
error.msg);
+        }
+      );
+  }
+
+  editAlertDefine(alertDefineId: number) {
+    this.isManageModalAdd = false;
+    this.isManageModalVisible = true;
+    this.isManageModalOkLoading = false;
+    // 查询告警定义信息
+    const getDefine$ = this.alertDefineSvc
+      .getAlertDefine(alertDefineId)
+      .pipe(
+        finalize(() => {
+          getDefine$.unsubscribe();
+        })
+      )
+      .subscribe(
+        message => {
+          if (message.code === 0) {
+            this.define = message.data;
+            if (this.define.field) {
+              this.cascadeValues = [this.define.app, this.define.metric, 
this.define.field];
+            } else {
+              this.cascadeValues = [this.define.app, this.define.metric];
+            }
+            if (this.define.tags == undefined) {
+              this.define.tags = [];
+            }
+            this.cascadeOnChange(this.cascadeValues);
+            this.renderAlertRuleExpr(this.define.expr);
+          } else {
+            
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.monitor-fail'), 
message.msg);
+          }
+        },
+        error => {
+          
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.monitor-fail'), 
error.msg);
+        }
+      );
+  }
+
+  onDeleteAlertDefines() {
+    if (this.checkedDefineIds == null || this.checkedDefineIds.size === 0) {
+      
this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), 
'');
+      return;
+    }
+    this.modal.confirm({
+      nzTitle: this.i18nSvc.fanyi('common.confirm.delete-batch'),
+      nzOkText: this.i18nSvc.fanyi('common.button.ok'),
+      nzCancelText: this.i18nSvc.fanyi('common.button.cancel'),
+      nzOkDanger: true,
+      nzOkType: 'primary',
+      nzClosable: false,
+      nzOnOk: () => this.deleteAlertDefines(this.checkedDefineIds)
+    });
+  }
+
+  onDeleteOneAlertDefine(alertDefineId: number) {
+    let defineIds = new Set<number>();
+    defineIds.add(alertDefineId);
+    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.deleteAlertDefines(defineIds)
+    });
+  }
+
+  deleteAlertDefines(defineIds: Set<number>) {
+    if (defineIds == null || defineIds.size == 0) {
+      
this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), 
'');
+      return;
+    }
+    this.tableLoading = true;
+    const deleteDefines$ = 
this.alertDefineSvc.deleteAlertDefines(defineIds).subscribe(
+      message => {
+        deleteDefines$.unsubscribe();
+        if (message.code === 0) {
+          
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.delete-success'), '');
+          this.updatePageIndex(defineIds.size);
+          this.loadAlertDefineTable();
+        } else {
+          this.tableLoading = false;
+          
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), 
message.msg);
+        }
+      },
+      error => {
+        this.tableLoading = false;
+        deleteDefines$.unsubscribe();
+        this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), 
error.msg);
+      }
+    );
+  }
+
+  updatePageIndex(delSize: number) {
+    const lastPage = Math.max(1, Math.ceil((this.total - delSize) / 
this.pageSize));
+    this.pageIndex = this.pageIndex > lastPage ? lastPage : this.pageIndex;
+  }
+
+  onExportDefines() {
+    if (this.checkedDefineIds == null || this.checkedDefineIds.size == 0) {
+      
this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-export'), 
'');
+      return;
+    }
+    this.isSwitchExportTypeModalVisible = true;
+  }
+
+  exportDefines(type: string) {
+    if (this.checkedDefineIds == null || this.checkedDefineIds.size == 0) {
+      
this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-export'), 
'');
+      return;
+    }
+    switch (type) {
+      case 'JSON':
+        this.exportJsonButtonLoading = true;
+        break;
+      case 'EXCEL':
+        this.exportExcelButtonLoading = true;
+        break;
+      case 'YAML':
+        this.exportYamlButtonLoading = true;
+        break;
+    }
+    const exportDefines$ = this.alertDefineSvc
+      .exportAlertDefines(this.checkedDefineIds, type)
+      .pipe(
+        finalize(() => {
+          this.exportYamlButtonLoading = false;
+          this.exportExcelButtonLoading = false;
+          this.exportJsonButtonLoading = false;
+          exportDefines$.unsubscribe();
+        })
+      )
+      .subscribe(
+        response => {
+          const message = response.body!;
+          if (message.type == 'application/json') {
+            
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.export-fail'), '');
+          } else {
+            const blob = new Blob([message], { type: 
response.headers.get('Content-Type')! });
+            const url = window.URL.createObjectURL(blob);
+            const a = document.createElement('a');
+            a.download = 
response.headers.get('Content-Disposition')!.split(';')[1].split('filename=')[1];
+            a.href = url;
+            a.click();
+            window.URL.revokeObjectURL(url);
+            this.isSwitchExportTypeModalVisible = false;
+          }
+        },
+        error => {
+          
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.export-fail'), 
error.msg);
+        }
+      );
+  }
+
+  onImportDefines(info: NzUploadChangeParam): void {
+    if (info.file.response) {
+      this.tableLoading = true;
+      const message = info.file.response;
+      if (message.code === 0) {
+        
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.import-success'), '');
+        this.loadAlertDefineTable();
+      } else {
+        this.tableLoading = false;
+        this.notifySvc.error(this.i18nSvc.fanyi('common.notify.import-fail'), 
message.msg);
+      }
+    }
+  }
+
+  // begin: 列表多选分页逻辑
+  checkedAll: boolean = false;
+  onAllChecked(checked: boolean) {
+    if (checked) {
+      this.defines.forEach(monitor => this.checkedDefineIds.add(monitor.id));
+    } else {
+      this.checkedDefineIds.clear();
+    }
+  }
+  onItemChecked(monitorId: number, checked: boolean) {
+    if (checked) {
+      this.checkedDefineIds.add(monitorId);
+    } else {
+      this.checkedDefineIds.delete(monitorId);
+    }
+  }
+  /**
+   * 分页回调
+   *
+   * @param params 页码信息
+   */
+  onTablePageChange(params: NzTableQueryParams) {
+    const { pageSize, pageIndex, sort, filter } = params;
+    this.pageIndex = pageIndex;
+    this.pageSize = pageSize;
+    this.loadAlertDefineTable();
+  }
+  // end: 列表多选逻辑
+
+  // start 新增修改告警定义model
+  isManageModalVisible = false;
+  isManageModalOkLoading = false;
+  isManageModalAdd = true;
+  define: AlertDefine = new AlertDefine();
+  cascadeValues: string[] = [];
+  currentMetrics: any[] = [];
+  alertRules: any[] = [{}];
+  isExpr = false;
+  caseInsensitiveFilter: NzCascaderFilter = (i, p) => {
+    return p.some(o => {
+      const label = o.label;
+      return !!label && label.toLowerCase().indexOf(i.toLowerCase()) !== -1;
+    });
+  };
+  cascadeOnChange(values: string[]): void {
+    if (values == null || values.length != 3) {
+      return;
+    }
+    this.appHierarchies.forEach(hierarchy => {
+      if (hierarchy.value == values[0]) {
+        hierarchy.children.forEach((metrics: { value: string; children: any[] 
}) => {
+          if (metrics.value == values[1]) {
+            this.currentMetrics = [];
+            if (metrics.children) {
+              metrics.children.forEach(item => {
+                this.currentMetrics.push(item);
+              });
+              this.currentMetrics.push({
+                value: 'system_value_row_count',
+                type: 0,
+                label: 
this.i18nSvc.fanyi('alert.setting.target.system_value_row_count')
+              });
+            }
+          }
+        });
+      }
+    });
+  }
+
+  switchAlertRuleShow() {
+    if (this.isExpr) {
+      let expr = this.calculateAlertRuleExpr();
+      if (expr != '') {
+        this.define.expr = expr;
+      }
+    }
+  }
+
+  onAddNewAlertRule() {
+    this.alertRules.push({});
+  }
+
+  onRemoveAlertRule(index: number) {
+    this.alertRules.splice(index, 1);
+  }
+
+  calculateAlertRuleExpr() {
+    let rules = this.alertRules.filter(rule => rule.metric != undefined && 
rule.operator != undefined);
+    let index = 0;
+    let expr = '';
+    rules.forEach(rule => {
+      let ruleStr = '';
+      if (rule.operator == 'exists' || rule.operator == '!exists') {
+        ruleStr = `${rule.operator}(${rule.metric.value})`;
+      } else {
+        if (rule.metric.type === 0 || rule.metric.type === 3) {
+          ruleStr = `${rule.metric.value} ${rule.operator} ${rule.value} `;
+        } else if (rule.metric.type === 1) {
+          ruleStr = `${rule.operator}(${rule.metric.value},"${rule.value}")`;
+        }
+      }
+      if (ruleStr != '') {
+        expr = expr + ruleStr;
+      }
+      if (index != rules.length - 1) {
+        expr = `${expr} && `;
+      }
+      index++;
+    });
+    return expr;
+  }
+
+  renderAlertRuleExpr(expr: string) {
+    if (expr == undefined || expr == '') {
+      return;
+    }
+    if (expr.indexOf('||') > 0 || expr.indexOf(' + ') > 0 || expr.indexOf(' - 
') > 0) {
+      this.isExpr = true;
+      return;
+    }
+    this.alertRules = [];
+    try {
+      let exprArr: string[] = expr.split('&&');
+      for (let index in exprArr) {
+        let exprStr = exprArr[index].trim();
+        const twoParamExpressionArr = ['equals', '!equals', 'contains', 
'!contains', 'matches', '!matches'];
+        const oneParamExpressionArr = ['exists', '!exists'];
+        let findIndexInTowParamExpression = 
twoParamExpressionArr.findIndex(value => exprStr.startsWith(value));
+        let findIndexInOneParamExpression = 
oneParamExpressionArr.findIndex(value => exprStr.startsWith(value));
+        if (findIndexInTowParamExpression >= 0) {
+          let tmp = exprStr.substring(exprStr.indexOf('(') + 1, exprStr.length 
- 1);
+          let tmpArr = tmp.split(',');
+          if (tmpArr.length == 2) {
+            let metric = this.currentMetrics.find(item => item.value == 
tmpArr[0].trim());
+            let value = tmpArr[1].substring(1, tmpArr[1].length - 1);
+            let rule = { metric: metric, operator: 
twoParamExpressionArr[findIndexInTowParamExpression], value: value };
+            this.alertRules.push(rule);
+          }
+        } else if (findIndexInOneParamExpression >= 0) {
+          let tmp = exprStr.substring(exprStr.indexOf('(') + 1, exprStr.length 
- 1);
+          if (tmp != '' && tmp != null) {
+            let metric = this.currentMetrics.find(item => item.value == 
tmp.trim());
+            let rule = { metric: metric, operator: 
oneParamExpressionArr[findIndexInOneParamExpression] };
+            this.alertRules.push(rule);
+          }
+        } else {
+          let values = exprStr.trim().split(' ');
+          if (values.length == 3 && values[2].trim() != '' && 
!Number.isNaN(parseFloat(values[2].trim()))) {
+            let metric = this.currentMetrics.find(item => item.value == 
values[0].trim());
+            let rule = { metric: metric, operator: values[1].trim(), value: 
values[2].trim() };
+            this.alertRules.push(rule);
+          }
+        }
+      }
+      if (this.alertRules.length != exprArr.length) {
+        this.alertRules = [{}];
+        this.isExpr = true;
+        return;
+      }
+    } catch (e) {
+      console.error(e);
+      this.isExpr = true;
+      this.alertRules = [{}];
+      return;
+    }
+    if (this.alertRules.length == 0) {
+      this.alertRules = [{}];
+      this.isExpr = true;
+    }
+  }
+
+  onManageModalCancel() {
+    this.isExpr = false;
+    this.isManageModalVisible = false;
+  }
+
+  resetManageModalData() {
+    this.cascadeValues = [];
+    this.alertRules = [{}];
+    this.isExpr = false;
+    this.isManageModalVisible = false;
+  }
+
+  onManageModalOk() {
+    this.isManageModalOkLoading = true;
+    this.define.app = this.cascadeValues[0];
+    this.define.metric = this.cascadeValues[1];
+    if (this.cascadeValues.length == 3) {
+      this.define.field = this.cascadeValues[2];
+      if (!this.isExpr) {
+        let expr = this.calculateAlertRuleExpr();
+        if (expr != '') {
+          this.define.expr = expr;
+        }
+      }
+    } else {
+      this.define.expr = '';
+      this.define.field = '';
+    }
+    if (this.isManageModalAdd) {
+      const modalOk$ = this.alertDefineSvc
+        .newAlertDefine(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.new-success'), '');
+              this.loadAlertDefineTable();
+              this.resetManageModalData();
+            } 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.alertDefineSvc
+        .editAlertDefine(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.loadAlertDefineTable();
+            } 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);
+          }
+        );
+    }
+  }
+
+  onRemoveTag(tag: TagItem) {
+    if (this.define != undefined && this.define.tags != undefined) {
+      this.define.tags = this.define.tags.filter(item => item !== tag);
+    }
+  }
+
+  sliceTagName(tag: TagItem): string {
+    if (tag.value != undefined && tag.value.trim() != '') {
+      return `${tag.name}:${tag.value}`;
+    } else {
+      return tag.name;
+    }
+  }
+
+  // end 新增修改告警定义model
+
+  // start Tag model
+  isTagManageModalVisible = false;
+  isTagManageModalOkLoading = false;
+  tagCheckedAll: boolean = false;
+  tagTableLoading = false;
+  tagSearch!: string;
+  tags!: Tag[];
+  checkedTags = new Set<Tag>();
+  loadTagsTable() {
+    this.tagTableLoading = true;
+    let tagsReq$ = this.tagSvc.loadTags(this.tagSearch, 1, 0, 1000).subscribe(
+      message => {
+        this.tagTableLoading = false;
+        this.tagCheckedAll = false;
+        this.checkedTags.clear();
+        if (message.code === 0) {
+          let page = message.data;
+          this.tags = page.content;
+        } else {
+          console.warn(message.msg);
+        }
+        tagsReq$.unsubscribe();
+      },
+      error => {
+        this.tagTableLoading = false;
+        tagsReq$.unsubscribe();
+      }
+    );
+  }
+  onShowTagsModal() {
+    this.isTagManageModalVisible = true;
+    this.loadTagsTable();
+  }
+  onTagManageModalCancel() {
+    this.isTagManageModalVisible = false;
+  }
+  onTagManageModalOk() {
+    this.isTagManageModalOkLoading = true;
+    this.checkedTags.forEach(item => {
+      if (this.define.tags.find(tag => tag.name == item.name && tag.value == 
item.tagValue) == undefined) {
+        this.define.tags.push({ name: item.name, value: item.tagValue });
+      }
+    });
+    this.isTagManageModalOkLoading = false;
+    this.isTagManageModalVisible = false;
+  }
+  onTagAllChecked(checked: boolean) {
+    if (checked) {
+      this.tags.forEach(tag => this.checkedTags.add(tag));
+    } else {
+      this.checkedTags.clear();
+    }
+  }
+  onTagItemChecked(tag: Tag, checked: boolean) {
+    if (checked) {
+      this.checkedTags.add(tag);
+    } else {
+      this.checkedTags.delete(tag);
+    }
+  }
+  // end tag model
+
+  // start 告警定义与监控关联model
+  isConnectModalVisible = false;
+  isConnectModalOkLoading = false;
+  transferData: TransferItem[] = [];
+  currentAlertDefineId!: number;
+  $asTransferItems = (data: unknown): TransferItem[] => data as TransferItem[];
+  onOpenConnectModal(alertDefineId: number, app: string) {
+    this.isConnectModalVisible = true;
+    this.currentAlertDefineId = alertDefineId;
+    zip(this.alertDefineSvc.getAlertDefineMonitorsBind(alertDefineId), 
this.monitorSvc.getMonitorsByApp(app))
+      .pipe(
+        map(([defineBindData, monitorData]: [Message<AlertDefineBind[]>, 
Message<Monitor[]>]) => {
+          let bindRecode: Record<number, string> = {};
+          if (defineBindData.data != undefined) {
+            defineBindData.data.forEach(bind => {
+              bindRecode[bind.monitorId] = bind.monitor.name;
+            });
+          }
+          let listTmp: any[] = [];
+          if (monitorData.data != undefined) {
+            monitorData.data.forEach(monitor => {
+              listTmp.push({
+                id: monitor.id,
+                name: monitor.name,
+                key: monitor.id,
+                direction: bindRecode[monitor.id] == undefined ? 'left' : 
'right'
+              });
+            });
+          }
+          return listTmp;
+        })
+      )
+      .subscribe(list => (this.transferData = list));
+  }
+  onConnectModalCancel() {
+    this.isConnectModalVisible = false;
+  }
+  onExportTypeModalCancel() {
+    this.isSwitchExportTypeModalVisible = false;
+  }
+  onConnectModalOk() {
+    this.isConnectModalOkLoading = true;
+    let defineBinds: AlertDefineBind[] = [];
+    this.transferData.forEach(item => {
+      if (item.direction == 'right') {
+        let bind = new AlertDefineBind();
+        bind.alertDefineId = this.currentAlertDefineId;
+        bind.monitorId = item.id;
+        defineBinds.push(bind);
+      }
+    });
+    const applyBind$ = this.alertDefineSvc
+      .applyAlertDefineMonitorsBind(this.currentAlertDefineId, defineBinds)
+      .pipe(
+        finalize(() => {
+          applyBind$.unsubscribe();
+          this.isConnectModalOkLoading = false;
+        })
+      )
+      .subscribe(
+        message => {
+          this.isConnectModalOkLoading = false;
+          if (message.code === 0) {
+            
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.apply-success'), '');
+            this.isConnectModalVisible = false;
+            this.loadAlertDefineTable();
+          } else {
+            
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.apply-fail'), 
message.msg);
+          }
+        },
+        error => {
+          this.isConnectModalOkLoading = false;
+          this.notifySvc.error(this.i18nSvc.fanyi('common.notify.apply-fail'), 
error.msg);
+        }
+      );
+  }
+  change(ret: TransferChange): void {
+    const listKeys = ret.list.map(l => l.key);
+    const hasOwnKey = (e: TransferItem): boolean => e.hasOwnProperty('key');
+    this.transferData = this.transferData.map(e => {
+      if (listKeys.includes(e.key) && hasOwnKey(e)) {
+        if (ret.to === 'left') {
+          delete e.hide;
+        } else if (ret.to === 'right') {
+          e.hide = false;
+        }
+      }
+      return e;
+    });
+  }
+  filterMetrics(currentMetrics: any[], cascadeValues: any): any[] {
+    if (cascadeValues.length !== 3) {
+      return currentMetrics;
+    }
+    // sort the cascadeValues[2] to first
+    return currentMetrics.sort((a, b) => {
+      if (a.value !== cascadeValues[2]) {
+        return 1;
+      } else {
+        return -1;
+      }
+    });
+  }
+  // end 告警定义与监控关联model
+  //查询告警阈值
+  onFilterSearchAlertDefinesByName() {
+    this.tableLoading = true;
+    let filter$ = this.alertDefineSvc.getAlertDefines(this.search, 
this.pageIndex - 1, this.pageSize).subscribe(
+      message => {
+        filter$.unsubscribe();
+        this.tableLoading = false;
+        this.checkedAll = false;
+        this.checkedDefineIds.clear();
+        if (message.code === 0) {
+          let page = message.data;
+          this.defines = page.content;
+          this.pageIndex = page.number + 1;
+          this.total = page.totalElements;
+        } else {
+          console.warn(message.msg);
+        }
+      },
+      error => {
+        this.tableLoading = false;
+        filter$.unsubscribe();
+        console.error(error.msg);
+      }
+    );
+  }
+}
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..89029ec6e 100644
--- a/web-app/src/app/routes/routes.module.ts
+++ b/web-app/src/app/routes/routes.module.ts
@@ -12,13 +12,20 @@ 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';
+import {CommonModule} from "@angular/common";
+import {NzRadioModule} from "ng-zorro-antd/radio";
+import {NzUploadModule} from "ng-zorro-antd/upload";
+import {NzCascaderModule} from "ng-zorro-antd/cascader";
+import {NzTransferModule} from "ng-zorro-antd/transfer";
+import {NzSwitchComponent} from "ng-zorro-antd/switch";
 
-const COMPONENTS: Array<Type<void>> = [DashboardComponent, UserLoginComponent, 
UserLockComponent, StatusPublicComponent];
+const COMPONENTS: Array<Type<void>> = [DashboardComponent, UserLoginComponent, 
UserLockComponent, StatusPublicComponent, BulletinComponent];
 
 @NgModule({
   imports: [
@@ -32,7 +39,13 @@ const COMPONENTS: Array<Type<void>> = [DashboardComponent, 
UserLoginComponent, U
     NzDividerModule,
     LayoutModule,
     NzCollapseModule,
-    NzListModule
+    NzListModule,
+    CommonModule,
+    NzRadioModule,
+    NzUploadModule,
+    NzCascaderModule,
+    NzTransferModule,
+    NzSwitchComponent
   ],
   declarations: COMPONENTS
 })
diff --git a/web-app/src/assets/app-data.json b/web-app/src/assets/app-data.json
index a7251fbcc..00da1f9d6 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": "Monitor-bulletin",
+          "i18n": "menu.monitor.center",
+          "icon": "anticon-laptop",
+          "link": "/bulletin"
+        },
         {
           "text": "Define",
           "i18n": "menu.advanced.define",


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

Reply via email to