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>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>${app} : {{ 'alert.setting.template.monitor-type' | i18n }}</code> + </nz-list-item> + <nz-list-item> + <code>${metrics} : {{ 'alert.setting.template.metrics-name' | i18n }}</code> + </nz-list-item> + <nz-list-item *ngIf="cascadeValues.length == 3"> + <code>${metric} : {{ 'alert.setting.template.metric-name' | i18n }}</code> + </nz-list-item> + <nz-list-item *ngFor="let item of filterMetrics(currentMetrics, cascadeValues)"> + <code + >${{ '{' + 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]
