chore: import upstream snapshot with attribution
MCP Bash Server CI / Test MCP Bash Server (dev) (push) Waiting to run
MCP Bash Server CI / Test MCP Bash Server (release) (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:11:39 +08:00
commit c8cebdfeee
4654 changed files with 626756 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
# last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
+16
View File
@@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false
+34
View File
@@ -0,0 +1,34 @@
_cli-tpl/
dist/
coverage/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Dependency directories
node_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.cache/
# yarn v2
.yarn
+132
View File
@@ -0,0 +1,132 @@
const prettierConfig = require('./.prettierrc.js');
module.exports = {
root: true,
parserOptions: { ecmaVersion: 2021 },
overrides: [
{
files: ['*.ts'],
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['tsconfig.json'],
createDefaultProgram: true
},
plugins: ['@typescript-eslint', 'jsdoc', 'import'],
extends: [
'plugin:@angular-eslint/recommended',
'plugin:@angular-eslint/template/process-inline-templates',
'plugin:prettier/recommended'
],
rules: {
'prettier/prettier': ['error', prettierConfig],
'jsdoc/tag-lines': [
'error',
'any',
{
startLines: 1,
},
],
'@angular-eslint/component-class-suffix': [
'error',
{
suffixes: ['Directive', 'Component', 'Base', 'Widget']
}
],
'@angular-eslint/directive-class-suffix': [
'error',
{
suffixes: ['Directive', 'Component', 'Base', 'Widget']
}
],
'@angular-eslint/component-selector': [
'off',
{
type: ['element', 'attribute'],
prefix: ['app', 'test'],
style: 'kebab-case'
}
],
'@angular-eslint/directive-selector': [
'off',
{
type: 'attribute',
prefix: ['app']
}
],
'@angular-eslint/no-attribute-decorator': 'error',
'@angular-eslint/no-conflicting-lifecycle': 'off',
'@angular-eslint/no-forward-ref': 'off',
'@angular-eslint/no-host-metadata-property': 'off',
'@angular-eslint/no-lifecycle-call': 'off',
'@angular-eslint/no-pipe-impure': 'error',
'@angular-eslint/prefer-output-readonly': 'error',
'@angular-eslint/use-component-selector': 'off',
'@angular-eslint/use-component-view-encapsulation': 'off',
'@angular-eslint/no-input-rename': 'off',
'@angular-eslint/no-output-native': 'off',
'@typescript-eslint/array-type': [
'error',
{
default: 'array-simple'
}
],
'@typescript-eslint/ban-types': [
'off',
{
types: {
String: {
message: 'Use string instead.'
},
Number: {
message: 'Use number instead.'
},
Boolean: {
message: 'Use boolean instead.'
},
Function: {
message: 'Use specific callable interface instead.'
}
}
}
],
'import/no-duplicates': 'error',
'import/no-unused-modules': 'error',
'import/no-unassigned-import': 'error',
'import/order': [
'error',
{
alphabetize: { order: 'asc', caseInsensitive: false },
'newlines-between': 'always',
groups: ['external', 'internal', ['parent', 'sibling', 'index']],
pathGroups: [],
pathGroupsExcludedImportTypes: []
}
],
'@typescript-eslint/no-this-alias': 'error',
'@typescript-eslint/member-ordering': 'off',
'no-irregular-whitespace': 'error',
'no-multiple-empty-lines': 'error',
'no-sparse-arrays': 'error',
'prefer-object-spread': 'error',
'prefer-template': 'error',
'prefer-const': 'off',
'max-len': 'off'
}
},
{
files: ['*.html'],
extends: ['plugin:@angular-eslint/template/recommended'],
rules: {}
},
{
files: ['*.html'],
excludedFiles: ['*inline-template-*.component.html'],
extends: ['plugin:prettier/recommended'],
rules: {
'prettier/prettier': ['error', { parser: 'angular' }],
'@angular-eslint/template/eqeqeq': 'off'
}
}
]
};
+46
View File
@@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
.angular/*
# profiling files
chrome-profiler-events*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
+18
View File
@@ -0,0 +1,18 @@
# add files you wish to ignore here
**/*.md
**/*.svg
**/test.ts
.stylelintrc
.prettierrc
src/assets/*
src/index.html
node_modules/
.vscode/
coverage/
dist/
package.json
tslint.json
_cli-tpl/**/*
+13
View File
@@ -0,0 +1,13 @@
module.exports = {
singleQuote: true,
useTabs: false,
printWidth: 140,
tabWidth: 2,
semi: true,
htmlWhitespaceSensitivity: 'strict',
arrowParens: 'avoid',
bracketSpacing: true,
proseWrap: 'preserve',
trailingComma: 'none',
endOfLine: 'lf'
};
+37
View File
@@ -0,0 +1,37 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-rational-order",
"stylelint-config-prettier"
],
"plugins": [
"stylelint-order",
"stylelint-declaration-block-no-ignored-properties"
],
"rules": {
"no-descending-specificity": null,
"plugin/declaration-block-no-ignored-properties": true,
"selector-type-no-unknown": [
true,
{
"ignoreTypes": [
"/^g2-/",
"/^nz-/",
"/^app-/"
]
}
],
"selector-pseudo-element-no-unknown": [
true,
{
"ignorePseudoElements": [
"ng-deep"
]
}
],
"import-notation": "string"
},
"ignoreFiles": [
"src/assets/**/*"
]
}
+30
View File
@@ -0,0 +1,30 @@
## HertzBeat Web-App
> [!NOTE]
>
> HertzBeat Web-App is a fork to [ng-alain](https://github.com/ng-alain/ng-alain/). Check [LICENSE](/LICENSE) and [license-ng-alain.txt](/material/licenses/frontend/LICENSE-ng-alain.txt) for more details.
### Quickly Start
1. Need `Node Pnpm` Environment, Make sure `Node.js >= 18`
2. Install yarn if not existed `npm install -g pnpm`
3. Execute `pnpm install` or `pnpm install --registry=https://registry.npmmirror.com` in `web-app`
4. Start After Backend Server Available : `pnpm start`
### Build HertzBeat Install Package
1. Execute command in web-app
```pnpm package```
2. Execute command in root
```mvn clean install```
The HertzBeat install package will at `manager/target/hertzbeat-{version}.tar.gz`
3. Execute command in collector
```mvn clean package -Pcluster```
+181
View File
@@ -0,0 +1,181 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"hertzbeat-web-app": {
"projectType": "application",
"root": "",
"sourceRoot": "src",
"prefix": "app",
"schematics": {
"@schematics/angular:component": {
"style": "less"
},
"@schematics/angular:application": {
"strict": true
}
},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"preserveSymlinks": true,
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "src/assets/audio",
"output": "/assets/audio"
},
{
"glob": "**/*",
"input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
"output": "/assets/"
},
{
"glob": "**/*",
"input": "node_modules/monaco-editor/min/vs",
"output": "/assets/vs/"
}
],
"styles": [
"src/styles.less",
"node_modules/slick-carousel/slick/slick.scss",
"node_modules/slick-carousel/slick/slick-theme.scss"
],
"scripts": [
"node_modules/jquery/dist/jquery.min.js",
"node_modules/slick-carousel/slick/slick.min.js"
],
"allowedCommonJsDependencies": [
"ajv",
"ajv-formats",
"mockjs",
"date-fns",
"file-saver",
"extend"
],
"stylePreprocessorOptions": {
"includePaths": [
"node_modules/"
]
}
},
"configurations": {
"production": {
"extractLicenses": false,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all",
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "6mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "50kb",
"maximumError": "100kb"
}
]
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"proxyConfig": "proxy.conf.json",
"buildTarget": "hertzbeat-web-app:build"
},
"configurations": {
"production": {
"buildTarget": "hertzbeat-web-app:build:production"
},
"development": {
"buildTarget": "hertzbeat-web-app:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "hertzbeat-web-app:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"karmaConfig": "karma.conf.js",
"tsConfig": "tsconfig.spec.json",
"scripts": [],
"styles": [],
"assets": [
"src/assets"
]
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "hertzbeat-web-app:serve"
},
"configurations": {
"production": {
"devServerTarget": "hertzbeat-web-app:serve:production"
}
}
}
}
}
},
"cli": {
"packageManager": "yarn",
"schematicCollections": [
"@schematics/angular",
"hertzbeat-web-app"
],
"analytics": false
},
"schematics": {
"@angular-eslint/schematics:application": {
"setParserOptionsProject": true
},
"@angular-eslint/schematics:library": {
"setParserOptionsProject": true
}
}
}
+44
View File
@@ -0,0 +1,44 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/web-app'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};
+13
View File
@@ -0,0 +1,13 @@
{
"$schema": "./node_modules/ng-alain/schema.json",
"theme": {
"list": [
{
"theme": "dark"
},
{
"theme": "compact"
}
]
}
}
+147
View File
@@ -0,0 +1,147 @@
{
"name": "hertzbeat-web-app",
"version": "1.0.0",
"description": "A real-time monitoring tool with custom-monitor and agentless.",
"author": "hertzbeat community",
"repository": {
"type": "git",
"url": "git+https://github.com/apache/hertzbeat.git"
},
"bugs": {
"url": "https://github.com/apache/hertzbeat/issues"
},
"homepage": "https://hertzbeat.apache.org",
"license": "Apache2.0",
"keywords": [
"monitor",
"uptime",
"monitoring",
"angular"
],
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"build": "npm run ng-high-memory build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"ng-high-memory": "node --max_old_space_size=8000 ./node_modules/@angular/cli/bin/ng",
"hmr": "ng s -o --hmr",
"analyze": "npm run ng-high-memory build -- --source-map",
"analyze:view": "source-map-explorer dist/**/*.js",
"test-coverage": "ng test --code-coverage --watch=false",
"color-less": "ng-alain-plugin-theme -t=colorLess",
"theme": "ng-alain-plugin-theme -t=themeCss",
"icon": "ng g ng-alain:plugin icon",
"lint": "npm run lint:fix && npm run lint:style",
"lint:ts": "ng lint",
"lint:fix": "ng lint --fix",
"lint:style": "stylelint \"src/**/*.less\" --syntax less --fix",
"package": "ng build --configuration production"
},
"private": true,
"dependencies": {
"@angular/animations": "17.3.10",
"@angular/cdk": "17.3.10",
"@angular/common": "17.3.10",
"@angular/compiler": "17.3.10",
"@angular/core": "17.3.10",
"@angular/forms": "17.3.10",
"@angular/platform-browser": "17.3.10",
"@angular/platform-browser-dynamic": "17.3.10",
"@angular/router": "17.3.10",
"@ant-design/colors": "^7.2.1",
"@ant-design/fast-color": "^3.0.0",
"@ant-design/icons-angular": "19.0.0",
"@ctrl/tinycolor": "^4.2.0",
"@delon/abc": "17.0.5",
"@delon/acl": "17.3.1",
"@delon/auth": "17.3.1",
"@delon/cache": "17.3.1",
"@delon/form": "17.3.1",
"@delon/mock": "17.3.1",
"@delon/theme": "17.3.1",
"@delon/util": "17.3.1",
"@kerwin612/ngx-query-builder": "0.6.4",
"ajv": "8.12.0",
"ajv-formats": "2.1.1",
"angular-tag-cloud-module": "17.0.1",
"date-fns": "2.30.0",
"@babel/runtime": "7.28.4",
"echarts": "5.4.3",
"extend": "^3.0.2",
"fast-deep-equal": "^3.1.3",
"isutf8": "^4.0.1",
"jquery": "3.7.1",
"json-schema-traverse": "^1.0.0",
"lodash-es": "^4.17.21",
"marked": "15.0.6",
"monaco-editor": "0.36.1",
"ng-in-viewport": "^16.1.0",
"ng-zorro-antd": "17.4.1",
"ngx-color-picker": "16.0.0",
"ngx-echarts": "17.2.0",
"ngx-markdown": "19.0.0",
"ngx-slick-carousel": "17.0.0",
"rxjs": "7.8.1",
"screenfull": "6.0.2",
"slick-carousel": "1.8.1",
"tslib": "2.6.2",
"uri-js": "^4.4.1",
"zone.js": "0.14.5",
"zrender": "^6.0.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.3.8",
"@angular-eslint/builder": "17.5.2",
"@angular-eslint/eslint-plugin": "17.5.2",
"@angular-eslint/eslint-plugin-template": "17.5.2",
"@angular-eslint/schematics": "17.5.2",
"@angular-eslint/template-parser": "17.5.2",
"@angular/cli": "17.3.8",
"@angular/compiler-cli": "17.3.10",
"@angular/language-service": "17.3.10",
"@delon/testing": "17.3.1",
"@types/jasmine": "5.1.4",
"@types/jasminewd2": "2.0.13",
"@types/node": "20.12.11",
"@typescript-eslint/eslint-plugin": "7.8.0",
"@typescript-eslint/parser": "7.8.0",
"eslint": "8.56.0",
"eslint-config-prettier": "8.6.0",
"eslint-plugin-deprecation": "^3.0.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jsdoc": "48.2.5",
"eslint-plugin-prefer-arrow": "1.2.3",
"eslint-plugin-prettier": "4.2.1",
"husky": "7.0.4",
"jasmine-core": "4.3.0",
"jasmine-spec-reporter": "7.0.0",
"karma": "6.4.2",
"karma-chrome-launcher": "3.1.1",
"karma-coverage": "2.2.1",
"karma-jasmine": "5.1.0",
"karma-jasmine-html-reporter": "2.0.0",
"lint-staged": "13.3.0",
"ng-alain": "17.3.1",
"node-fetch": "2.7.0",
"prettier": "2.8.8",
"protractor": "7.0.0",
"source-map-explorer": "2.5.3",
"stylelint": "14.16.1",
"stylelint-config-prettier": "9.0.5",
"stylelint-config-rational-order": "0.1.2",
"stylelint-config-standard": "28.0.0",
"stylelint-declaration-block-no-ignored-properties": "2.7.0",
"stylelint-order": "5.0.0",
"ts-node": "10.9.1",
"typescript": "5.3.3"
},
"lint-staged": {
"(src)/**/*.{html,ts}": [
"eslint --fix"
],
"(src)/**/*.less": [
"npm run lint:style"
]
}
}
+16592
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
{
"/api/*": {
"target": "http://localhost:1157",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
}
}
+52
View File
@@ -0,0 +1,52 @@
import { Component, ElementRef, Inject, OnInit, Renderer2 } from '@angular/core';
import { NavigationEnd, NavigationError, RouteConfigLoadStart, Router } from '@angular/router';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN, TitleService, VERSION as VERSION_ALAIN } from '@delon/theme';
import { environment } from '@env/environment';
import { NzModalService } from 'ng-zorro-antd/modal';
import { VERSION as VERSION_ZORRO } from 'ng-zorro-antd/version';
import { ThemeService } from './service/theme.service';
@Component({
selector: 'app-root',
template: ` <router-outlet></router-outlet> `
})
export class AppComponent implements OnInit {
constructor(
el: ElementRef,
renderer: Renderer2,
private router: Router,
private titleSrv: TitleService,
private modalSrv: NzModalService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService
) {
renderer.setAttribute(el.nativeElement, 'ng-alain-version', VERSION_ALAIN.full);
renderer.setAttribute(el.nativeElement, 'ng-zorro-version', VERSION_ZORRO.full);
}
ngOnInit(): void {
let configLoad = false;
this.router.events.subscribe(ev => {
if (ev instanceof RouteConfigLoadStart) {
configLoad = true;
}
if (configLoad && ev instanceof NavigationError) {
this.modalSrv.confirm({
nzTitle: this.i18nSvc.fanyi('common.notice'),
nzContent: environment.production
? `New version may have been published, please click refresh.`
: `Can not resolve route${ev.url}`,
nzCancelDisabled: false,
nzOkText: this.i18nSvc.fanyi('common.refresh'),
nzCancelText: this.i18nSvc.fanyi('common.ignore'),
nzOnOk: () => location.reload()
});
}
if (ev instanceof NavigationEnd) {
this.titleSrv.setTitle();
this.modalSrv.closeAll();
}
});
}
}
+95
View File
@@ -0,0 +1,95 @@
/* eslint-disable import/order */
/* eslint-disable import/no-duplicates */
import { HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, LOCALE_ID, NgModule, Type } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NzMessageModule } from 'ng-zorro-antd/message';
import { NzNotificationModule } from 'ng-zorro-antd/notification';
import { Observable } from 'rxjs';
import { default as ngLang } from '@angular/common/locales/en';
import { DELON_LOCALE, en_US as delonLang } from '@delon/theme';
import { enUS as dateLang } from 'date-fns/locale';
import { NZ_DATE_LOCALE, NZ_I18N, en_US as zorroLang } from 'ng-zorro-antd/i18n';
const LANG = {
abbr: 'en-US',
ng: ngLang,
zorro: zorroLang,
date: dateLang,
delon: delonLang
};
import { registerLocaleData } from '@angular/common';
registerLocaleData(LANG.ng, LANG.abbr);
const LANG_PROVIDES = [
{ provide: LOCALE_ID, useValue: LANG.abbr },
{ provide: NZ_I18N, useValue: LANG.zorro },
{ provide: NZ_DATE_LOCALE, useValue: LANG.date },
{ provide: DELON_LOCALE, useValue: LANG.delon }
];
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { I18NService } from '@core';
const I18NSERVICE_PROVIDES = [{ provide: ALAIN_I18N_TOKEN, useClass: I18NService, multi: false }];
import { JsonSchemaModule } from '@shared';
const FORM_MODULES = [JsonSchemaModule];
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { DefaultInterceptor } from '@core';
const INTERCEPTOR_PROVIDES = [{ provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true }];
const GLOBAL_THIRD_MODULES: Array<Type<void>> = [SlickCarouselModule, TagCloudComponent];
import { StartupService } from '@core';
export function StartupServiceFactory(startupService: StartupService): () => Observable<void> {
return () => startupService.load();
}
const APP_INIT_PROVIDES = [
StartupService,
{
provide: APP_INITIALIZER,
useFactory: StartupServiceFactory,
deps: [StartupService],
multi: true
}
];
import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';
import { GlobalConfigModule } from './global-config.module';
import { LayoutModule } from './layout/layout.module';
import { RoutesModule } from './routes/routes.module';
import { SharedModule } from './shared/shared.module';
import { ReactiveFormsModule } from '@angular/forms';
import { NgxEchartsModule } from 'ngx-echarts';
import { SlickCarouselModule } from 'ngx-slick-carousel';
import { TagCloudComponent } from 'angular-tag-cloud-module';
import { MarkdownModule } from 'ngx-markdown';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
GlobalConfigModule.forRoot(),
CoreModule,
SharedModule,
LayoutModule,
RoutesModule,
NzMessageModule,
NzNotificationModule,
...FORM_MODULES,
...GLOBAL_THIRD_MODULES,
ReactiveFormsModule,
NgxEchartsModule.forRoot({
echarts: () => import('echarts')
}),
MarkdownModule.forRoot()
],
providers: [...LANG_PROVIDES, ...INTERCEPTOR_PROVIDES, ...I18NSERVICE_PROVIDES, ...APP_INIT_PROVIDES],
bootstrap: [AppComponent]
})
export class AppModule {}
+13
View File
@@ -0,0 +1,13 @@
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { I18NService } from './i18n/i18n.service';
import { throwIfAlreadyLoaded } from './module-import-guard';
@NgModule({
providers: [I18NService]
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
throwIfAlreadyLoaded(parentModule, 'CoreModule');
}
}
@@ -0,0 +1,34 @@
import { Inject, Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { NzNotificationService } from 'ng-zorro-antd/notification';
import { Observable } from 'rxjs';
import { LocalStorageService } from '../../service/local-storage.service';
@Injectable({
providedIn: 'root'
})
export class DetectAuthGuard implements CanActivate {
constructor(
private localStorageSvc: LocalStorageService,
private notifySvc: NzNotificationService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
private router: Router
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
let activate = this.localStorageSvc.hasAuthorizationToken();
if (!activate) {
setTimeout(() => {
this.notifySvc.warning(this.i18nSvc.fanyi('app.login.notify'), '');
this.router.navigateByUrl('/passport/login');
});
}
return activate;
}
}
@@ -0,0 +1,91 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { DelonLocaleService, SettingsService } from '@delon/theme';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import { NzI18nService } from 'ng-zorro-antd/i18n';
import { of } from 'rxjs';
import { I18NService } from './i18n.service';
describe('Service: I18n', () => {
let injector: TestBed;
let srv: I18NService;
const MockSettingsService: NzSafeAny = {
layout: {
lang: null
}
};
const MockNzI18nService = {
setLocale: () => {},
setDateLocale: () => {}
};
const MockDelonLocaleService = {
setLocale: () => {}
};
const MockTranslateService = {
getBrowserLang: jasmine.createSpy('getBrowserLang'),
addLangs: () => {},
setLocale: () => {},
getDefaultLang: () => '',
use: (lang: string) => of(lang),
instant: jasmine.createSpy('instant')
};
function genModule(): void {
injector = TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
I18NService,
{ provide: SettingsService, useValue: MockSettingsService },
{ provide: NzI18nService, useValue: MockNzI18nService },
{ provide: DelonLocaleService, useValue: MockDelonLocaleService }
]
});
srv = TestBed.inject(I18NService);
}
it('should working', () => {
spyOnProperty(navigator, 'languages').and.returnValue(['zh-CN']);
genModule();
expect(srv).toBeTruthy();
expect(srv.defaultLang).toBe('zh-CN');
srv.fanyi('a');
srv.fanyi('a', {});
});
it('should be used layout as default language', () => {
MockSettingsService.layout.lang = 'en-US';
const navSpy = spyOnProperty(navigator, 'languages');
genModule();
expect(navSpy).not.toHaveBeenCalled();
expect(srv.defaultLang).toBe('en-US');
MockSettingsService.layout.lang = null;
});
it('should be used browser as default language', () => {
spyOnProperty(navigator, 'languages').and.returnValue(['zh-TW']);
genModule();
expect(srv.defaultLang).toBe('zh-TW');
});
it('should be use default language when the browser language is not in the list', () => {
spyOnProperty(navigator, 'languages').and.returnValue(['es-419']);
genModule();
expect(srv.defaultLang).toBe('zh-CN');
});
it('should be trigger notify when changed language', () => {
genModule();
srv.use('en-US', {});
srv.change.subscribe(lang => {
expect(lang).toBe('en-US');
});
});
it('should be trigger notify when changed language', () => {
genModule();
srv.use('pt-BR', {});
srv.change.subscribe(lang => {
expect(lang).toBe('pt-BR');
});
});
});
+152
View File
@@ -0,0 +1,152 @@
import { Platform } from '@angular/cdk/platform';
import { registerLocaleData } from '@angular/common';
import { HttpHeaders } from '@angular/common/http';
import ngEn from '@angular/common/locales/en';
import ngJa from '@angular/common/locales/ja';
import ngPt from '@angular/common/locales/pt';
import ngZh from '@angular/common/locales/zh';
import ngZhTw from '@angular/common/locales/zh-Hant';
import { Injectable } from '@angular/core';
import {
_HttpClient,
AlainI18nBaseService,
DelonLocaleService,
en_US as delonEnUS,
SettingsService,
zh_CN as delonZhCn,
zh_TW as delonZhTw,
ja_JP as delonJaJP
} from '@delon/theme';
import { AlainConfigService } from '@delon/util/config';
import { enUS as dfEn, zhCN as dfZhCn, zhTW as dfZhTw, ja as dfJa, ptBR as dfPtBR } from 'date-fns/locale';
import { NzSafeAny } from 'ng-zorro-antd/core/types';
import {
en_US as zorroEnUS,
NzI18nService,
zh_CN as zorroZhCN,
zh_TW as zorroZhTW,
ja_JP as zorroJaJP,
pt_BR as zorroPtBR
} from 'ng-zorro-antd/i18n';
import { Observable, zip } from 'rxjs';
import { map } from 'rxjs/operators';
import { Message } from '../../pojo/Message';
interface LangConfigData {
abbr: string;
text: string;
ng: NzSafeAny;
zorro: NzSafeAny;
date: NzSafeAny;
delon: NzSafeAny;
}
const DEFAULT = 'en-US';
const LANGS: { [key: string]: LangConfigData } = {
'en-US': {
text: 'English',
ng: ngEn,
zorro: zorroEnUS,
date: dfEn,
delon: delonEnUS,
abbr: '🇬🇧'
},
'zh-CN': {
text: '简体中文',
ng: ngZh,
zorro: zorroZhCN,
date: dfZhCn,
delon: delonZhCn,
abbr: '🇨🇳'
},
'zh-TW': {
text: '繁体中文',
ng: ngZhTw,
zorro: zorroZhTW,
date: dfZhTw,
delon: delonZhTw,
abbr: '🇭🇰'
},
'ja-JP': {
text: '日本語',
ng: ngJa,
zorro: zorroJaJP,
date: dfJa,
delon: delonJaJP,
abbr: '🇯🇵'
},
'pt-BR': {
text: 'Português (Brasil)',
ng: ngPt,
zorro: zorroPtBR,
date: dfPtBR,
delon: delonEnUS, // Usando en-US como fallback (ou crie um locale personalizado)
abbr: '🇧🇷'
}
};
@Injectable({ providedIn: 'root' })
export class I18NService extends AlainI18nBaseService {
protected _defaultLang = DEFAULT;
private _langs = Object.keys(LANGS).map(code => {
const item = LANGS[code];
return { code, text: item.text, abbr: item.abbr };
});
constructor(
private http: _HttpClient,
private settings: SettingsService,
private nzI18nService: NzI18nService,
private delonLocaleService: DelonLocaleService,
private platform: Platform,
cogSrv: AlainConfigService
) {
super(cogSrv);
const defaultLang = this.getDefaultLang();
this._defaultLang = this._langs.findIndex(w => w.code === defaultLang) === -1 ? DEFAULT : defaultLang;
}
private getDefaultLang(): string {
if (!this.platform.isBrowser) {
return DEFAULT;
}
if (this.settings.layout.lang) {
return this.settings.layout.lang;
}
let res = (navigator.languages ? navigator.languages[0] : null) || navigator.language;
const arr = res.split('-');
return arr.length <= 1 ? res : `${arr[0]}-${arr[1].toUpperCase()}`;
}
loadLangData(lang: string): Observable<NzSafeAny> {
const headers = new HttpHeaders({ 'Cache-Control': 'no-cache' });
return zip(this.http.get(`./assets/i18n/${lang}.json`, null, { headers: headers }), this.http.get(`/i18n/${lang}`)).pipe(
map(([langLocalData, langRemoteData]: [Record<string, string>, Message<any>]) => {
let remote: Record<string, string> = langRemoteData.data;
Object.keys(remote).forEach(key => {
langLocalData[key] = remote[key];
});
return langLocalData;
})
);
}
use(lang: string, data: Record<string, unknown>): void {
this._data = this.flatData(data, []);
const item = LANGS[lang];
registerLocaleData(item.ng);
this.nzI18nService.setLocale(item.zorro);
this.nzI18nService.setDateLocale(item.date);
this.delonLocaleService.setLocale(item.delon);
this._currentLang = lang;
this._change$.next(lang);
}
getLangs(): Array<{ code: string; text: string; abbr: string }> {
return this._langs;
}
}
+4
View File
@@ -0,0 +1,4 @@
export * from './i18n/i18n.service';
export * from './module-import-guard';
export * from './interceptor/default.interceptor';
export * from './startup/startup.service';
@@ -0,0 +1,203 @@
import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpHeaders,
HttpInterceptor,
HttpRequest,
HttpResponse,
HttpResponseBase
} from '@angular/common/http';
import { Injectable, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { ALAIN_I18N_TOKEN, _HttpClient } from '@delon/theme';
import { environment } from '@env/environment';
import { NzNotificationService } from 'ng-zorro-antd/notification';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { catchError, filter, mergeMap, switchMap, take } from 'rxjs/operators';
import { Message } from '../../pojo/Message';
import { AuthService } from '../../service/auth.service';
import { LocalStorageService } from '../../service/local-storage.service';
const CODE_MESSAGE: { [key: number]: string } = {
400: 'Request Illegal Content, No Response.',
401: 'Auth Error.',
403: 'No Permission For This Request.',
404: 'Not Found.',
406: 'Request Illegal Content.',
409: 'Request Conflict.',
410: 'Request Resource Already Deleted.',
422: 'Validate Error.',
500: 'Server Error Happen.',
502: 'Gateway Error.',
503: 'Service Not Available, Try After.',
504: 'Gateway Timeout.'
};
@Injectable()
export class DefaultInterceptor implements HttpInterceptor {
private notified = false;
// Whether token is refreshing
private refreshToking = false;
private refreshToken$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(private injector: Injector, private authSvc: AuthService, private storageSvc: LocalStorageService) {}
private get notification(): NzNotificationService {
return this.injector.get(NzNotificationService);
}
private get http(): _HttpClient {
return this.injector.get(_HttpClient);
}
private goTo(url: string): void {
setTimeout(() => {
this.injector.get(Router).navigateByUrl(url);
this.notified = false;
});
}
private checkStatus(ev: HttpResponseBase): void {
const errorText = CODE_MESSAGE[ev.status] || ev.statusText;
console.warn(` ${ev.status}: ${ev.url}`, errorText);
if (ev.status == 403) {
this.notification.error(` ${ev.status}: ${errorText}`, '');
} else {
this.notification.error(` ${ev.status}: ${ev.url}`, errorText);
}
}
/**
* refresh Token request
*/
private refreshTokenRequest(): Observable<Message<any>> {
const refreshToken = this.storageSvc.getRefreshToken();
if (refreshToken == null) {
return throwError('refreshToken is null.');
}
return this.authSvc.refreshToken(refreshToken);
}
// #region 刷新Token方式一:使用 401 重新刷新 Token
private tryRefreshToken(ev: HttpResponseBase, req: HttpRequest<any>, next: HttpHandler): Observable<any> {
// 1, redirect to login page if this request is used for refreshing token
if ([`/account/auth/refresh`].some(url => req.url.includes(url))) {
this.toLogin();
return throwError(ev);
}
// 2, if `refreshToking` is true, means that the refreshing token request is in progress
// All requests will be suspended and wait for the refreshing token request to complete
if (this.refreshToking) {
return this.refreshToken$.pipe(
filter(v => !!v),
take(1),
switchMap(() => next.handle(this.reAttachToken(req)))
);
}
// 3、try refreshing Token
this.refreshToking = true;
this.refreshToken$.next(null);
return this.refreshTokenRequest().pipe(
switchMap(res => {
// Check whether the TOKEN is correct
this.refreshToking = false;
if (res.code === 0 && res.data != undefined) {
let token = res.data.token;
let refreshToken = res.data.refreshToken;
if (token != undefined) {
this.storageSvc.storageAuthorizationToken(token);
this.storageSvc.storageRefreshToken(refreshToken);
// notifies subsequent requests to continue
this.refreshToken$.next(token);
return next.handle(this.reAttachToken(req));
} else {
console.warn(`flush new token failed. ${res.msg}`);
return throwError('flush new token failed.');
}
} else {
console.warn(`flush new token failed. ${res.msg}`);
return throwError('flush new token failed.');
}
}),
catchError(err => {
// refreshing token is failed, redirect to login page
console.warn(`flush new token failed. ${err.msg}`);
this.refreshToking = false;
this.toLogin();
return throwError(err);
})
);
}
private reAttachToken(req: HttpRequest<any>): HttpRequest<any> {
let token = this.storageSvc.getAuthorizationToken();
return req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
private toLogin(): void {
if (!this.notified) {
this.notified = true;
this.goTo('/passport/login');
}
}
private fillHeaders(headers?: HttpHeaders): { [name: string]: string } {
const res: { [name: string]: string } = {};
const lang = this.injector.get(ALAIN_I18N_TOKEN).currentLang;
if (!headers?.has('Accept-Language') && lang) {
res['Accept-Language'] = lang;
}
let token = this.storageSvc.getAuthorizationToken();
if (token !== null) {
res['Authorization'] = `Bearer ${token}`;
}
return res;
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let url = req.url;
if (!url.startsWith('https://') && !url.startsWith('http://') && !url.startsWith('.')) {
const { baseUrl } = environment.api;
url = baseUrl + (baseUrl?.endsWith('/') && url.startsWith('/') ? url.substring(1) : url);
}
const newReq = req.clone({ url, setHeaders: this.fillHeaders(req.headers) });
return next.handle(newReq).pipe(
mergeMap(httpEvent => {
if (httpEvent instanceof HttpResponseBase) {
return of(httpEvent);
} else {
return of(httpEvent);
}
}),
catchError((err: HttpErrorResponse) => {
// handle failed response and token expired
switch (err.status) {
case 401:
return this.tryRefreshToken(err, newReq, next);
case 404:
case 500:
this.goTo(`/exception/${err.status}?url=${req.urlWithParams}`);
break;
case 400:
let resp = new HttpResponse({
body: err.error,
headers: err.headers,
status: err.status,
statusText: err.statusText
});
return of(resp);
default:
break;
}
this.checkStatus(err);
return throwError(err.error);
})
);
}
}
@@ -0,0 +1,6 @@
// https://angular.io/guide/styleguide#style-04-12
export function throwIfAlreadyLoaded(parentModule: any, moduleName: string): void {
if (parentModule) {
throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
}
}
@@ -0,0 +1,99 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable, Inject } from '@angular/core';
import { Router } from '@angular/router';
import { ACLService } from '@delon/acl';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
import { ALAIN_I18N_TOKEN, Menu, MenuService, SettingsService, TitleService } from '@delon/theme';
import type { NzSafeAny } from 'ng-zorro-antd/core/types';
import { NzIconService } from 'ng-zorro-antd/icon';
import { Observable, zip } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { ICONS } from '../../../style-icons';
import { ICONS_AUTO } from '../../../style-icons-auto';
import { MemoryStorageService } from '../../service/memory-storage.service';
import { ThemeService } from '../../service/theme.service';
import { I18NService } from '../i18n/i18n.service';
@Injectable({
providedIn: 'root'
})
export class StartupService {
constructor(
iconSrv: NzIconService,
private menuService: MenuService,
@Inject(ALAIN_I18N_TOKEN) private i18n: I18NService,
private settingService: SettingsService,
private aclService: ACLService,
private titleService: TitleService,
@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService,
private httpClient: HttpClient,
private router: Router,
private storageService: MemoryStorageService,
private themeService: ThemeService
) {
iconSrv.addIcon(...ICONS_AUTO, ...ICONS);
}
public loadConfigResourceViaHttp(): Observable<void> {
const defaultLang = this.i18n.defaultLang;
const headers = new HttpHeaders({ 'Cache-Control': 'no-cache' });
return zip(
this.i18n.loadLangData(defaultLang),
this.httpClient.get('./assets/app-data.json', { headers: headers }),
this.httpClient.get(`/apps/hierarchy?lang=${defaultLang}`)
).pipe(
catchError((res: NzSafeAny) => {
console.warn(`StartupService.load: Network request failed`, res);
setTimeout(() => this.router.navigateByUrl(`/exception/500`));
return [];
}),
map(([langData, appData, menuData]: [Record<string, string>, NzSafeAny, NzSafeAny]) => {
// setting language data
this.i18n.use(defaultLang, langData);
// Application information: including site name, description, year
this.settingService.setApp(appData.app);
// this.settingService.setLayout('collapsed', true);
this.aclService.setFull(true);
this.menuService.add(appData.menu);
menuData.data.forEach((item: { category: string; value: string; hide: boolean }) => {
if (item.hide) {
return;
}
let category = item.category;
let app = item.value;
let menu: Menu | null = this.menuService.getItem(category);
if (menu != null) {
menu.children?.push({
text: app,
link: `/monitors?app=${app}`,
i18n: `monitor.app.${app}`
});
} else {
if (app != 'prometheus' && app != 'push') {
this.menuService.getItem('monitoring')?.children?.push({
text: app,
link: `/monitors?app=${app}`,
i18n: `monitor.app.${app}`,
icon: 'anticon-project'
});
}
}
});
this.menuService.getItem('monitoring')?.children?.forEach(item => {
if (item.key != null && (item.children == null || item.children.length == 0)) {
item.hide = true;
}
});
this.storageService.putData('hierarchy', menuData.data);
this.menuService.resume();
this.titleService.suffix = appData.app.name;
this.themeService.changeTheme(null);
})
);
}
load(): Observable<void> {
return this.loadConfigResourceViaHttp();
}
}
+32
View File
@@ -0,0 +1,32 @@
/* eslint-disable import/order */
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { DelonACLModule } from '@delon/acl';
import { AlainThemeModule } from '@delon/theme';
import { AlainConfig, ALAIN_CONFIG } from '@delon/util/config';
import { throwIfAlreadyLoaded } from '@core';
import { environment } from '@env/environment';
const alainModules: any[] = [AlainThemeModule.forRoot(), DelonACLModule];
import { NzConfig, NZ_CONFIG } from 'ng-zorro-antd/core/config';
const ngZorroConfig: NzConfig = {};
const zorroProvides = [{ provide: NZ_CONFIG, useValue: ngZorroConfig }];
@NgModule({
imports: [...alainModules, ...(environment.modules || [])]
})
export class GlobalConfigModule {
constructor(@Optional() @SkipSelf() parentModule: GlobalConfigModule) {
throwIfAlreadyLoaded(parentModule, 'GlobalConfigModule');
}
static forRoot(): ModuleWithProviders<GlobalConfigModule> {
return {
ngModule: GlobalConfigModule,
providers: [...zorroProvides]
};
}
}
+12
View File
@@ -0,0 +1,12 @@
import { NgModule } from '@angular/core';
import { MenuFoldOutline, MenuUnfoldOutline, FormOutline, DashboardOutline } from '@ant-design/icons-angular/icons';
import { NZ_ICONS, NzIconModule } from 'ng-zorro-antd/icon';
const icons = [MenuFoldOutline, MenuUnfoldOutline, DashboardOutline, FormOutline];
@NgModule({
imports: [NzIconModule],
exports: [NzIconModule],
providers: [{ provide: NZ_ICONS, useValue: icons }]
})
export class IconsProviderModule {}
@@ -0,0 +1,393 @@
/* Component Styles */
/* Import system theme variables */
@import 'src/styles/theme.less';
/* CSS Variables for theme support */
:host {
/* Light theme variables */
--background-color: #f5f7fa;
--sidebar-background: #ffffff;
--content-background: #ffffff;
--border-color: rgba(255, 255, 255, 0.2);
--shadow-color: rgba(0, 0, 0, 0.06);
--primary-color: @primary-color;
--primary-color-light: lighten(@primary-color, 10%);
display: block;
/* Change to block layout to avoid extra whitespace */
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--background-color);
min-height: 100vh;
@media (max-width: 1200px) {
.page-header-item {
display: none;
}
}
@media (max-width: 992px) {
.page-header-item2 {
display: none;
}
}
}
/* Dark theme variables */
:host.dark {
--background-color: #141414;
--sidebar-background: #1f1f1f;
--content-background: #1f1f1f;
--border-color: rgba(255, 255, 255, 0.1);
--shadow-color: rgba(0, 0, 0, 0.3);
}
/* Dark theme support for ng-alain */
:host ::ng-deep .dark {
--background-color: #141414;
--sidebar-background: #1f1f1f;
--content-background: #1f1f1f;
--border-color: rgba(255, 255, 255, 0.1);
--shadow-color: rgba(0, 0, 0, 0.3);
}
/* Global dark theme support */
[data-theme='dark'] :host,
.dark :host {
--background-color: #141414;
--sidebar-background: #1f1f1f;
--content-background: #1f1f1f;
--border-color: rgba(255, 255, 255, 0.1);
--shadow-color: rgba(0, 0, 0, 0.3);
}
/* Modern Layout Styles with Rounded Separation - Complete Override */
:host ::ng-deep {
/* Reset all default ng-alain positioning - exclude header items */
.alain-default {
position: static !important;
transform: none !important;
left: auto !important;
right: auto !important;
top: auto !important;
bottom: auto !important;
}
.alain-default__aside,
.alain-default__content {
position: static !important;
transform: none !important;
left: auto !important;
right: auto !important;
top: auto !important;
bottom: auto !important;
}
/* Layout container styling with CSS Grid */
.alain-default {
background-color: var(--background-color, #f5f7fa) !important;
padding: 12px !important;
display: grid !important;
grid-template-columns: 200px 1fr !important;
grid-template-rows: 64px 1fr !important;
grid-template-areas:
"header header"
"sidebar content" !important;
gap: 12px !important;
min-height: 100vh !important;
box-sizing: border-box !important;
}
/* Collapsed sidebar layout */
.alain-default.alain-default__collapsed {
grid-template-columns: 64px 1fr !important;
}
/* Header modern styling */
.alain-default__header {
grid-area: header !important;
background: linear-gradient(135deg, var(--primary-color, @primary-color) 0%, var(--primary-color-light, lighten(@primary-color, 10%)) 100%) !important;
box-shadow: 0 4px 20px var(--shadow-color, rgba(0, 0, 0, 0.08)) !important;
border-radius: 6px !important;
backdrop-filter: blur(10px) !important;
transition: all 0.3s ease !important;
position: relative !important;
z-index: 10 !important;
margin: 0 !important;
width: 100% !important;
height: 64px !important;
display: flex !important;
align-items: center !important;
overflow: hidden !important;
}
/* Ensure header items are visible and properly positioned */
.alain-default__header-item,
.alain-default__header .alain-default__header-logo,
.alain-default__header .alain-default__header-trigger,
.alain-default__header .alain-default__header-action,
.alain-default__nav-item,
header-notify,
header-user,
header-search {
position: relative !important;
z-index: 11 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
/* Fix notification badge positioning */
.alain-default__nav-item .ant-badge,
.alain-default__nav-item nz-badge {
display: inline-flex !important;
align-items: center !important;
}
/* Sidebar modern styling */
.alain-default__aside {
grid-area: sidebar !important;
background: var(--sidebar-background, #ffffff) !important;
border-radius: 6px !important;
box-shadow: 0 8px 32px var(--shadow-color, rgba(0, 0, 0, 0.06)) !important;
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.2)) !important;
backdrop-filter: blur(20px) !important;
overflow: hidden !important;
transition: all 0.3s ease !important;
position: relative !important;
margin: 0 !important;
width: 100% !important;
height: 100% !important;
}
.alain-default__aside::after {
display: none !important;
/* Remove default border */
}
/* Content area modern styling */
.alain-default__content {
grid-area: content !important;
background: var(--content-background, #ffffff) !important;
border-radius: 6px !important;
box-shadow: 0 8px 32px var(--shadow-color, rgba(0, 0, 0, 0.06)) !important;
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.2)) !important;
backdrop-filter: blur(20px) !important;
overflow: auto !important;
transition: all 0.3s ease !important;
position: relative !important;
margin: 0 !important;
padding: 12px !important;
width: 100% !important;
height: 100% !important;
}
/* Sidebar navigation modern styling */
.sidebar-nav__item {
margin: 4px 12px;
border-radius: 6px;
border-left: none;
transition: all 0.3s ease;
overflow: hidden;
}
.sidebar-nav__item:hover {
background: linear-gradient(135deg, rgba(63, 81, 181, 0.08) 0%, rgba(63, 81, 181, 0.04) 100%);
transform: translateX(4px);
}
.sidebar-nav__selected {
background: linear-gradient(135deg, rgba(63, 81, 181, 0.12) 0%, rgba(63, 81, 181, 0.08) 100%);
border-left: none;
border-radius: 6px;
position: relative;
}
.sidebar-nav__selected::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 24px;
background: linear-gradient(135deg, @primary-color 0%, lighten(@primary-color, 20%) 100%);
border-radius: 0 3px 3px 0;
}
.sidebar-nav__item-link {
padding: 12px 16px;
border-radius: 6px;
transition: all 0.3s ease;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
font-weight: 500;
}
.sidebar-nav__selected > .sidebar-nav__item-link {
font-weight: 600;
}
/* Header items modern styling */
.alain-default__nav-item {
border-radius: 6px;
margin: 0 4px;
transition: all 0.3s ease;
}
.alain-default__nav-item {
display: flex;
align-items: center;
justify-content: center;
}
.alain-default__nav-item:hover {
background-color: rgba(255, 255, 255, 0.15) !important;
transform: translateY(-1px);
}
/* Search component modern styling */
.alain-default__search {
.ant-input-affix-wrapper {
border-radius: 6px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.ant-input-affix-wrapper:hover,
.ant-input-affix-wrapper:focus {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.4);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
/* Logo area styling */
.alain-default__header-logo {
border-radius: 6px;
margin: 8px;
transition: all 0.3s ease;
}
.alain-default__header-logo:hover {
transform: scale(1.02);
}
/* Responsive design */
@media (max-width: 768px) {
.alain-default {
padding: 4px;
}
.alain-default__header {
border-radius: 6px;
}
.alain-default__aside {
margin-top: 4px;
border-radius: 6px;
}
.alain-default__content {
margin-top: 4px;
margin-left: 4px;
border-radius: 6px;
}
}
/* Dark theme support */
[data-theme='dark'] & {
:host {
background-color: #141414;
}
.alain-default__aside {
background: #1f1f1f;
border-color: rgba(255, 255, 255, 0.1);
}
.alain-default__content {
background: #1f1f1f;
border-color: rgba(255, 255, 255, 0.1);
}
.sidebar-nav__item:hover {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%);
}
.sidebar-nav__selected {
background: linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.08) 100%);
}
}
}
/* Footer styles */
global-footer {
text-align: center;
padding: 16px;
border-top: 1px solid #e5e5e5;
min-height: 120px;
}
/* AI Chat Button styles */
.ai-chat-button-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.ai-chat-button {
width: 44px;
height: 44px;
border-radius: 6px;
background-color: @primary-color;
color: white;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transition: all 0.3s ease;
}
.ai-chat-button:hover {
transform: scale(1.05);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
}
.ai-chat-button:active {
transform: scale(0.95);
}
/* AI Chat Modal Styles */
:host ::ng-deep .ai-chat-modal {
.ant-modal {
border-radius: 6px !important;
overflow: hidden !important;
}
.ant-modal-content {
border-radius: 6px !important;
overflow: hidden !important;
}
.ant-modal-header {
border-radius: 6px 6px 0 0 !important;
margin: 0 !important;
padding: 16px !important;
}
.ant-modal-body {
border-radius: 0 0 6px 6px !important;
padding: 0 !important;
}
.ant-modal-close {
top: 8px !important;
right: 8px !important;
}
}
@@ -0,0 +1,134 @@
import { Component, Inject, OnInit, OnDestroy } from '@angular/core';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN, SettingsService, User } from '@delon/theme';
import { LayoutDefaultOptions } from '@delon/theme/layout-default';
import { environment } from '@env/environment';
import { Subject } from 'rxjs';
import { CONSTANTS } from '../../shared/constants';
import { AiChatModalService } from '../../shared/services/ai-chat-modal.service';
@Component({
selector: 'layout-basic',
template: `
<layout-default [options]="options" [nav]="navTpl" [content]="contentTpl" [customError]="null">
<layout-default-header-item direction="left">
<a layout-default-header-item-trigger href="//github.com/apache/hertzbeat" target="_blank">
<i nz-icon nzType="github"></i>
</a>
</layout-default-header-item>
<layout-default-header-item direction="middle">
<header-ai-chat class="alain-default__ai-chat"></header-ai-chat>
</layout-default-header-item>
<layout-default-header-item direction="right" hidden="mobile">
<header-notify></header-notify>
</layout-default-header-item>
<layout-default-header-item direction="right" hidden="mobile">
<a layout-default-header-item-trigger routerLink="/passport/lock">
<i nz-icon nzType="lock"></i>
</a>
</layout-default-header-item>
<layout-default-header-item direction="right" hidden="mobile">
<div layout-default-header-item-trigger nz-dropdown [nzDropdownMenu]="settingsMenu" nzTrigger="click" nzPlacement="bottomRight">
<i nz-icon nzType="setting"></i>
</div>
<nz-dropdown-menu #settingsMenu="nzDropdownMenu">
<div nz-menu style="width: 200px;">
<div nz-menu-item>
<header-fullscreen></header-fullscreen>
</div>
<div nz-menu-item routerLink="/setting/labels">
<i nz-icon nzType="tag" class="mr-sm"></i>
<span style="margin-left: 4px">{{ 'menu.advanced.labels' | i18n }}</span>
</div>
<div nz-menu-item>
<header-i18n></header-i18n>
</div>
</div>
</nz-dropdown-menu>
</layout-default-header-item>
<layout-default-header-item direction="right">
<header-user></header-user>
</layout-default-header-item>
<ng-template #navTpl>
<layout-default-nav class="d-block py-lg" openStrictly="true"></layout-default-nav>
</ng-template>
<ng-template #contentTpl>
<router-outlet></router-outlet>
</ng-template>
</layout-default>
<global-footer>
<div style="margin-top: 30px">
Apache HertzBeat™ {{ version }}<br />
Copyright &copy; {{ currentYear }}
<a href="https://hertzbeat.apache.org" target="_blank">Apache HertzBeat™</a>
<br />
Licensed under the Apache License, Version 2.0
</div>
</global-footer>
<setting-drawer *ngIf="showSettingDrawer" appSettingDrawerI18n></setting-drawer>
<!-- AI Chat Button -->
<div class="ai-chat-button-container">
<div class="ai-chat-button" (click)="openChatModal()">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="28"
height="28"
fill="white"
style="min-width:36px; min-height:36px;"
>
<path
d="M12 2a2 2 0 0 1 2 2c0 .74-.4 1.39-1 1.73V7h1a7 7 0 0 1 7 7h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1h-1v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1H2a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1a7 7 0 0 1 7-7h1V5.73c-.6-.34-1-.99-1-1.73a2 2 0 0 1 2-2M7.5 13A2.5 2.5 0 0 0 5 15.5A2.5 2.5 0 0 0 7.5 18a2.5 2.5 0 0 0 2.5-2.5A2.5 2.5 0 0 0 7.5 13m9 0a2.5 2.5 0 0 0-2.5 2.5a2.5 2.5 0 0 0 2.5 2.5a2.5 2.5 0 0 0 2.5-2.5a2.5 2.5 0 0 0-2.5-2.5z"
/>
</svg>
</div>
</div>
`,
styleUrls: ['./basic.component.less']
})
export class LayoutBasicComponent implements OnDestroy {
options: LayoutDefaultOptions = {
logoExpanded: `./assets/brand_white.svg`,
logoCollapsed: `./assets/logo.svg`
};
avatar: string = `./assets/img/avatar.svg`;
searchToggleStatus = false;
aiChatToggleStatus = false;
showSettingDrawer = !environment.production;
version = CONSTANTS.VERSION;
currentYear = new Date().getFullYear();
get user(): User {
return this.settings.user;
}
get role(): string {
let userTmp = this.settings.user;
if (userTmp == undefined || userTmp.role == undefined) {
return this.i18nSvc.fanyi('app.role.admin');
} else {
let roles: string[] = JSON.parse(userTmp.role);
return roles.length > 0 ? roles[0] : '';
}
}
private destroy$ = new Subject<void>();
constructor(
private settings: SettingsService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
private aiChatModalService: AiChatModalService
) {}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
openChatModal(): void {
this.aiChatModalService.openChatModal();
}
}
@@ -0,0 +1,159 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Component, NgZone } from '@angular/core';
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { of } from 'rxjs';
import { SettingDrawerI18nDirective } from './setting-drawer-i18n.directive';
@Component({
template: `<setting-drawer appSettingDrawerI18n>
<div class="setting-drawer__content">
<div>主题色</div>
<div>设置</div>
<div>配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改参数配置文件 src/styles/theme.less</div>
</div>
</setting-drawer>`
})
class TestComponent {}
describe('SettingDrawerI18nDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let directive: SettingDrawerI18nDirective;
let i18nService: jasmine.SpyObj<I18NService>;
let httpMock: HttpTestingController;
let mockTranslations: { [key: string]: string };
const mockI18nData = {
'zh-CN': {
'setting.drawer.theme.color': '主题色',
'setting.drawer.settings': '设置',
'setting.drawer.info.message': '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改参数配置文件 src/styles/theme.less'
},
'en-US': {
'setting.drawer.theme.color': 'Theme Color',
'setting.drawer.settings': 'Settings',
'setting.drawer.info.message':
'The configuration panel is only for preview in the development environment, it will not be displayed in the production environment. Please copy and manually modify the parameter configuration file src/styles/theme.less'
},
'ja-JP': {
'setting.drawer.theme.color': 'テーマカラー',
'setting.drawer.settings': '設定',
'setting.drawer.info.message':
'設定パネルは開発環境でのプレビューのみに使用され、本番環境では表示されません。コピーして、パラメータ設定ファイル src/styles/theme.less を手動で変更してください'
},
'pt-BR': {
'setting.drawer.theme.color': 'Cor do Tema',
'setting.drawer.settings': 'Configurações',
'setting.drawer.info.message':
'O painel de configuração é apenas para visualização no ambiente de desenvolvimento, não será exibido no ambiente de produção. Por favor, copie e modifique manualmente o arquivo de configuração de parâmetros src/styles/theme.less'
},
'zh-TW': {
'setting.drawer.theme.color': '主題色',
'setting.drawer.settings': '設置',
'setting.drawer.info.message': '配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改參數配置文件 src/styles/theme.less'
}
};
beforeEach(async () => {
mockTranslations = {
'setting.drawer.theme.color': 'Theme Color',
'setting.drawer.settings': 'Settings',
'setting.drawer.info.message':
'The configuration panel is only for preview in the development environment, it will not be displayed in the production environment. Please copy and manually modify the parameter configuration file src/styles/theme.less'
};
const i18nServiceSpy = jasmine.createSpyObj('I18NService', ['fanyi', 'change'], {
change: of('en-US')
});
i18nServiceSpy.fanyi.and.callFake((key: string) => {
return mockTranslations[key] || key;
});
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
declarations: [TestComponent, SettingDrawerI18nDirective],
providers: [{ provide: ALAIN_I18N_TOKEN, useValue: i18nServiceSpy }, NgZone]
}).compileComponents();
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
i18nService = TestBed.inject(ALAIN_I18N_TOKEN) as jasmine.SpyObj<I18NService>;
httpMock = TestBed.inject(HttpTestingController);
const directiveEl = fixture.debugElement.query(By.directive(SettingDrawerI18nDirective));
if (directiveEl) {
directive = directiveEl.injector.get(SettingDrawerI18nDirective);
}
});
afterEach(() => {
httpMock.verify();
});
it('should create', () => {
expect(directive).toBeTruthy();
});
it('should load mappings from i18n files', fakeAsync(() => {
fixture.detectChanges();
const languages = ['zh-CN', 'en-US', 'ja-JP', 'pt-BR', 'zh-TW'];
const requests = languages.map(lang => httpMock.expectOne(`./assets/i18n/${lang}.json`));
languages.forEach((lang, index) => {
requests[index].flush(mockI18nData[lang as keyof typeof mockI18nData]);
});
tick(100);
fixture.detectChanges();
expect(i18nService.fanyi).toHaveBeenCalled();
}));
it('should replace Chinese text with translations', fakeAsync(() => {
fixture.detectChanges();
const languages = ['zh-CN', 'en-US', 'ja-JP', 'pt-BR', 'zh-TW'];
const requests = languages.map(lang => httpMock.expectOne(`./assets/i18n/${lang}.json`));
languages.forEach((lang, index) => {
requests[index].flush(mockI18nData[lang as keyof typeof mockI18nData]);
});
tick(2000);
fixture.detectChanges();
tick(100);
const content = fixture.nativeElement.querySelector('.setting-drawer__content');
if (content) {
const themeColorDiv = content.querySelector('div:first-child');
if (themeColorDiv) {
expect(themeColorDiv.textContent).toContain('Theme Color');
}
}
}));
});
@@ -0,0 +1,377 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { HttpClient } from '@angular/common/http';
import { Directive, ElementRef, Inject, OnInit, OnDestroy, AfterViewInit, NgZone } from '@angular/core';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { Subject, forkJoin } from 'rxjs';
import { takeUntil, map } from 'rxjs/operators';
@Directive({
selector: 'setting-drawer[appSettingDrawerI18n]'
})
export class SettingDrawerI18nDirective implements OnInit, AfterViewInit, OnDestroy {
private destroy$ = new Subject<void>();
private mutationObserver?: MutationObserver;
private replaceInterval?: any;
private textMappings: { [key: string]: string } = {};
private mappingsReady = false;
private readonly settingDrawerKeys = [
'setting.drawer.theme.color',
'setting.drawer.settings',
'setting.drawer.top',
'setting.drawer.sidebar',
'setting.drawer.content',
'setting.drawer.other',
'setting.drawer.height',
'setting.drawer.background.color',
'setting.drawer.background.color.default',
'setting.drawer.top.padding',
'setting.drawer.fixed.header.sidebar',
'setting.drawer.colorblind.mode',
'setting.drawer.preview',
'setting.drawer.reset',
'setting.drawer.copy',
'setting.drawer.info.message'
];
private readonly languages = ['zh-CN', 'en-US', 'ja-JP', 'pt-BR', 'zh-TW'];
constructor(
private el: ElementRef<HTMLElement>,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
private ngZone: NgZone,
private http: HttpClient
) {
this.loadMappingsFromI18nFiles();
}
private loadMappingsFromI18nFiles(): void {
const requests = this.languages.map(lang =>
this.http.get<{ [key: string]: string }>(`./assets/i18n/${lang}.json`).pipe(map(data => ({ lang, data })))
);
forkJoin(requests)
.pipe(takeUntil(this.destroy$))
.subscribe(results => {
this.textMappings = {};
for (const key of this.settingDrawerKeys) {
for (const result of results) {
const translation = result.data[key];
if (translation && translation.trim()) {
this.textMappings[translation] = key;
}
}
}
const sortedEntries = Object.entries(this.textMappings).sort((a, b) => b[0].length - a[0].length);
this.textMappings = Object.fromEntries(sortedEntries);
this.mappingsReady = true;
setTimeout(() => this.replaceText(), 0);
});
}
ngOnInit(): void {
setTimeout(() => this.replaceText(), 0);
setTimeout(() => this.replaceText(), 100);
setTimeout(() => this.replaceText(), 500);
setTimeout(() => this.replaceText(), 1000);
}
ngAfterViewInit(): void {
setTimeout(() => this.replaceText(), 0);
setTimeout(() => this.replaceText(), 100);
setTimeout(() => this.replaceText(), 200);
setTimeout(() => this.replaceText(), 500);
setTimeout(() => this.replaceText(), 1000);
setTimeout(() => this.replaceText(), 2000);
this.ngZone.runOutsideAngular(() => {
let timeoutId: any;
this.mutationObserver = new MutationObserver(mutations => {
if (mutations.length > 0) {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
this.ngZone.run(() => {
this.replaceText();
});
}, 50);
}
});
this.mutationObserver.observe(this.el.nativeElement, {
childList: true,
subtree: true,
characterData: true,
attributes: false
});
});
this.replaceInterval = setInterval(() => {
this.replaceText();
}, 500);
this.i18nSvc.change.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.replaceText();
});
}
ngOnDestroy(): void {
if (this.mutationObserver) {
this.mutationObserver.disconnect();
}
if (this.replaceInterval) {
clearInterval(this.replaceInterval);
}
this.destroy$.next();
this.destroy$.complete();
}
private replaceText(): void {
if (!this.mappingsReady || Object.keys(this.textMappings).length === 0) {
return;
}
requestAnimationFrame(() => {
const drawerContent =
document.querySelector('.setting-drawer__content') || document.querySelector('setting-drawer') || this.el.nativeElement;
if (!drawerContent) {
return;
}
const currentTranslations = new Map<string, string>();
for (const key of this.settingDrawerKeys) {
const translation = this.i18nSvc.fanyi(key);
if (translation && translation !== key) {
currentTranslations.set(key, translation);
}
}
const currentTranslationValues = Array.from(currentTranslations.values());
const processedNodes = new Set<Node>();
const walker = document.createTreeWalker(drawerContent as Node, NodeFilter.SHOW_TEXT, {
acceptNode: (node: Node) => {
const parent = node.parentElement;
if (parent && (parent.tagName === 'SCRIPT' || parent.tagName === 'STYLE')) {
return NodeFilter.FILTER_REJECT;
}
if (parent && (parent.tagName === 'INPUT' || parent.tagName === 'TEXTAREA' || parent.tagName === 'SELECT')) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
});
let textNode;
while ((textNode = walker.nextNode())) {
if (processedNodes.has(textNode)) continue;
processedNodes.add(textNode);
const textContent = textNode.textContent || '';
if (!textContent.trim()) continue;
let newText = textContent;
let hasChanges = false;
const alreadyTranslated = currentTranslationValues.some(trans => textContent.includes(trans));
if (alreadyTranslated) {
continue;
}
for (const [knownText, translationKey] of Object.entries(this.textMappings)) {
const currentTranslation = currentTranslations.get(translationKey);
if (!currentTranslation) continue;
if (newText.includes(knownText) && knownText !== currentTranslation && !newText.includes(currentTranslation)) {
newText = newText.replace(new RegExp(this.escapeRegExp(knownText), 'g'), currentTranslation);
hasChanges = true;
}
}
if (hasChanges && newText !== textContent) {
textNode.textContent = newText;
}
}
const allElements = (drawerContent as HTMLElement).querySelectorAll('*');
for (let i = 0; i < allElements.length; i++) {
const el = allElements[i];
const htmlElement = el as HTMLElement;
if (['SCRIPT', 'STYLE', 'INPUT', 'TEXTAREA', 'SELECT'].includes(htmlElement.tagName)) {
continue;
}
if (htmlElement.children.length === 0 && htmlElement.textContent) {
const textContent = htmlElement.textContent;
if (!textContent.trim()) continue;
if (htmlElement.childNodes.length === 1 && htmlElement.childNodes[0].nodeType === Node.TEXT_NODE) {
if (processedNodes.has(htmlElement.childNodes[0])) {
continue;
}
}
const alreadyTranslated = currentTranslationValues.some(trans => textContent.includes(trans));
if (alreadyTranslated) {
continue;
}
let newText = textContent;
let hasChanges = false;
for (const [knownText, translationKey] of Object.entries(this.textMappings)) {
const currentTranslation = currentTranslations.get(translationKey);
if (!currentTranslation) continue;
if (newText.includes(knownText) && knownText !== currentTranslation && !newText.includes(currentTranslation)) {
newText = newText.replace(new RegExp(this.escapeRegExp(knownText), 'g'), currentTranslation);
hasChanges = true;
}
}
if (hasChanges && newText !== textContent) {
htmlElement.textContent = newText;
}
}
}
const infoMessageKey = 'setting.drawer.info.message';
const currentInfoMessage = currentTranslations.get(infoMessageKey);
if (currentInfoMessage) {
const allInfoMessages: string[] = [];
for (const [text, key] of Object.entries(this.textMappings)) {
if (key === infoMessageKey && text !== currentInfoMessage) {
allInfoMessages.push(text);
}
}
const chinesePhrases = [
'配置栏只在开发环境用于',
'配置栏只在開發環境用於',
'生产环境不会展现',
'生產環境不會展現',
'请拷贝后手动修改',
'請拷貝後手動修改',
'参数配置文件',
'參數配置文件',
'src/styles/theme.less'
];
const containers = (drawerContent as HTMLElement).querySelectorAll('div, span, p, li, td, section, article');
for (let i = 0; i < containers.length; i++) {
const container = containers[i] as HTMLElement;
const containerText = container.textContent || '';
if (containerText.includes(currentInfoMessage)) {
continue;
}
let containsInfoMessage = false;
for (const knownInfoMsg of allInfoMessages) {
if (containerText.includes(knownInfoMsg)) {
containsInfoMessage = true;
break;
}
}
if (!containsInfoMessage) {
containsInfoMessage = chinesePhrases.some(phrase => containerText.includes(phrase));
}
const hasChineseChars = /[\u4e00-\u9fa5]/.test(containerText);
const isLongEnough = containerText.length > 50;
if (containsInfoMessage || (hasChineseChars && isLongEnough && containerText.includes('src/styles/theme.less'))) {
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
let textNode;
const textNodes: Node[] = [];
while ((textNode = walker.nextNode())) {
if (!processedNodes.has(textNode)) {
textNodes.push(textNode);
}
}
if (textNodes.length > 0) {
textNodes[0].textContent = currentInfoMessage;
processedNodes.add(textNodes[0]);
for (let j = 1; j < textNodes.length; j++) {
textNodes[j].textContent = '';
processedNodes.add(textNodes[j]);
}
} else {
container.textContent = currentInfoMessage;
}
break;
}
}
}
for (let i = 0; i < allElements.length; i++) {
const el = allElements[i];
const htmlElement = el as HTMLElement;
const attributesToCheck = ['placeholder', 'title', 'aria-label', 'alt'];
for (let j = 0; j < attributesToCheck.length; j++) {
const attr = attributesToCheck[j];
if (htmlElement.hasAttribute(attr)) {
const attrValue = htmlElement.getAttribute(attr);
if (attrValue) {
let newValue = attrValue;
let hasChanges = false;
for (const [knownText, translationKey] of Object.entries(this.textMappings)) {
const currentTranslation = currentTranslations.get(translationKey);
if (!currentTranslation) continue;
if (newValue.includes(knownText) && knownText !== currentTranslation && !newValue.includes(currentTranslation)) {
newValue = newValue.replace(new RegExp(this.escapeRegExp(knownText), 'g'), currentTranslation);
hasChanges = true;
}
}
if (hasChanges && newValue !== attrValue) {
htmlElement.setAttribute(attr, newValue);
}
}
}
}
}
});
}
private escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
}
@@ -0,0 +1,89 @@
import { Component, Input, Output, EventEmitter, Inject } from '@angular/core';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { AiChatModalService } from '../../../shared/services/ai-chat-modal.service';
@Component({
selector: 'header-ai-chat',
template: `
<div class="ai-chat-input-container">
<nz-input-group [nzSuffix]="iconTpl" nzCompact class="ai-chat-input-group">
<ng-template #iconTpl>
<i nz-icon [hidden]="inputMessage.trim()" [nzType]="'message'"></i>
<i nz-icon (click)="onSubmit()" [hidden]="!inputMessage.trim()" [nzType]="'send'"></i>
</ng-template>
<input
nz-input
type="text"
class="ai-chat-input"
[(ngModel)]="inputMessage"
[placeholder]="'ai.chat.input.placeholder' | i18n"
(keydown.enter)="onSubmit()"
(focus)="onFocus()"
(blur)="onBlur()"
/>
</nz-input-group>
</div>
`,
styles: [
`
.ai-chat-input-container {
display: flex;
align-items: center;
width: 100%;
max-width: 500px;
}
.ai-chat-input-group {
width: 100%;
display: flex;
}
.ai-chat-input {
flex: 1;
min-width: 300px;
border-radius: 8px;
border-right: none !important;
}
.ai-chat-input:focus {
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
border-color: #1890ff;
}
[data-theme='dark'] .ai-chat-input {
background-color: #141414;
border-color: #434343;
color: #fff;
}
[data-theme='dark'] .ai-chat-input::placeholder {
color: #8c8c8c;
}
`
]
})
export class HeaderAiChatComponent {
@Input() disabled = false;
@Output() readonly search = new EventEmitter<string>();
inputMessage = '';
constructor(private aiChatModalService: AiChatModalService, @Inject(ALAIN_I18N_TOKEN) private i18n: I18NService) {}
onSubmit(): void {
if (this.inputMessage.trim()) {
this.aiChatModalService.openChatModal(this.inputMessage.trim());
this.inputMessage = '';
}
}
onFocus(): void {
// 可以添加聚焦时的逻辑
}
onBlur(): void {
// 可以添加失焦时的逻辑
}
}
@@ -0,0 +1,29 @@
import { ChangeDetectionStrategy, Component, HostListener } from '@angular/core';
import screenfull from 'screenfull';
@Component({
selector: 'header-fullscreen',
template: `
<i nz-icon class="mr-sm" [nzType]="status ? 'fullscreen-exit' : 'fullscreen'"></i>
{{ (status ? 'menu.fullscreen.exit' : 'menu.fullscreen') | i18n }}
`,
host: {
'[class.d-block]': 'true'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderFullScreenComponent {
status = false;
@HostListener('window:resize')
_resize(): void {
this.status = screenfull.isFullscreen;
}
@HostListener('click')
_click(): void {
if (screenfull.isEnabled) {
screenfull.toggle();
}
}
}
@@ -0,0 +1,62 @@
import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN, SettingsService } from '@delon/theme';
import { BooleanInput, InputBoolean } from '@delon/util/decorator';
@Component({
selector: 'header-i18n',
template: `
<div *ngIf="showLangText" nz-dropdown [nzDropdownMenu]="langMenu" nzPlacement="bottomRight">
<i nz-icon class="mr-sm" nzType="global"></i>
{{ 'menu.lang' | i18n }}
<i nz-icon nzType="down"></i>
</div>
<i
style="font-size: 25px; color: #b421cc"
*ngIf="!showLangText"
nz-dropdown
[nzDropdownMenu]="langMenu"
nzPlacement="bottomRight"
nz-icon
nzType="global"
></i>
<nz-dropdown-menu #langMenu="nzDropdownMenu">
<ul nz-menu>
<li nz-menu-item class="mr-sm" *ngFor="let item of langs" [nzSelected]="item.code === curLangCode" (click)="change(item.code)">
<span role="img" [attr.aria-label]="item.text" class="pr-xs">{{ item.abbr }}</span>
{{ item.text }}
</li>
</ul>
</nz-dropdown-menu>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderI18nComponent {
static ngAcceptInputType_showLangText: BooleanInput;
/** Whether to display language text */
@Input() @InputBoolean() showLangText = true;
get langs(): Array<{ code: string; text: string; abbr: string }> {
return this.i18n.getLangs();
}
get curLangCode(): string {
return this.settings.layout.lang;
}
constructor(private settings: SettingsService, @Inject(ALAIN_I18N_TOKEN) private i18n: I18NService, @Inject(DOCUMENT) private doc: any) {}
change(lang: string): void {
const spinEl = this.doc.createElement('div');
spinEl.setAttribute('class', `page-loading ant-spin ant-spin-lg ant-spin-spinning`);
spinEl.innerHTML = `<span class="ant-spin-dot ant-spin-dot-spin"><i></i><i></i><i></i><i></i></span>`;
this.doc.body.appendChild(spinEl);
this.i18n.loadLangData(lang).subscribe(res => {
this.i18n.use(lang, res);
this.settings.setLayout('lang', lang);
setTimeout(() => this.doc.location.reload());
});
}
}
@@ -0,0 +1,358 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnInit, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzNotificationService } from 'ng-zorro-antd/notification';
import { finalize } from 'rxjs/operators';
import { Mute } from '../../../pojo/Mute';
import { SingleAlert } from '../../../pojo/SingleAlert';
import { AlertSoundService } from '../../../service/alert-sound.service';
import { AlertService } from '../../../service/alert.service';
import { GeneralConfigService } from '../../../service/general-config.service';
@Component({
selector: 'header-notify',
template: `
<div style="display: flex; align-items: center;">
<ng-template #badgeTpl>
<nz-badge [nzCount]="count" ngClass="alain-default__nav-item" [nzStyle]="{ 'box-shadow': 'none' }">
<i nz-icon nzType="bell" ngClass="alain-default__nav-item-icon"></i>
</nz-badge>
</ng-template>
@if (data!.length <= 0) {
<ng-template [ngTemplateOutlet]="badgeTpl" />
} @else {
<div
nz-dropdown
(nzVisibleChange)="onPopoverVisibleChange($event)"
[(nzVisible)]="popoverVisible"
nzTrigger="click"
nzPlacement="bottomRight"
nzOverlayClassName="header-dropdown notice-icon"
[nzDropdownMenu]="noticeMenu"
>
<ng-template [ngTemplateOutlet]="badgeTpl" />
</div>
}
<nz-badge ngClass="alain-default__nav-item" [nzStyle]="{ 'box-shadow': 'none' }">
<i
nz-icon
[nzType]="mute.mute ? 'muted' : 'sound'"
ngClass="alain-default__nav-item-icon"
(click)="toggleMute($event)"
nz-tooltip
nzTooltipPlacement="bottom"
[nzTooltipTitle]="(mute.mute ? 'common.unmute' : 'common.mute') | i18n"
></i>
</nz-badge>
</div>
<nz-dropdown-menu #noticeMenu="nzDropdownMenu">
@if (data[0].title) {
<div class="ant-modal-title" style="line-height: 44px; text-align: center;">{{ data[0].title }}</div>
}
<nz-spin [nzSpinning]="loading" [nzDelay]="0">
@if (data[0].list && data[0].list.length > 0) {
<ng-template [ngTemplateOutlet]="listTpl" />
} @else {
<div class="notice-icon__notfound">
@if (data[0].emptyImage) {
<img class="notice-icon__notfound-img" [attr.src]="data[0].emptyImage" alt="not found" />
}
<p>
<ng-container *nzStringTemplateOutlet="data[0].emptyText">
{{ data[0].emptyText }}
</ng-container>
</p>
</div>
}
</nz-spin>
<div style="display: flex; align-items: center; border-top: 1px solid #f0f0f0;">
<div class="notice-icon__clear" style="flex: 1; border-top: none;" (click)="gotoAlertCenter()">{{ data[0].enterText }}</div>
</div>
</nz-dropdown-menu>
<ng-template #listTpl>
<nz-list [nzDataSource]="data[0].list" [nzRenderItem]="item">
<ng-template #item let-item>
<nz-list-item [class.notice-icon__item-read]="item.read">
<nz-list-item-meta [nzTitle]="nzTitle" [nzDescription]="nzDescription" [nzAvatar]="item.avatar">
<ng-template #nzTitle>
<ng-container *nzStringTemplateOutlet="item.title; context: { $implicit: item }">
<a (click)="gotoDetail(item.id)">{{ item.title }}</a>
</ng-container>
@if (item.extra) {
<div class="notice-icon__item-extra">
<nz-tag [nzColor]="item.color">{{ item.extra }}</nz-tag>
</div>
}
</ng-template>
<ng-template #nzDescription>
@if (item.description) {
<div class="notice-icon__item-desc">
<ng-container *nzStringTemplateOutlet="item.description; context: { $implicit: item }">
{{ item.description }}
</ng-container>
</div>
} @if (item.datetime) {
<div class="notice-icon__item-time">{{ item.datetime }}</div>
}
</ng-template>
</nz-list-item-meta>
</nz-list-item>
</ng-template>
</nz-list>
</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderNotifyComponent implements OnInit, OnDestroy {
data: any[] = [
{
title: this.i18nSvc.fanyi('dashboard.alerts.title-no'),
list: [],
emptyText: this.i18nSvc.fanyi('dashboard.alerts.no'),
emptyImage: '/assets/img/empty-full.svg',
enterText: this.i18nSvc.fanyi('dashboard.alerts.enter'),
clearText: this.i18nSvc.fanyi('alert.center.clear')
}
];
notifiedAlert: any[] = [];
count = 0;
loading = false;
popoverVisible = false;
refreshInterval: any;
private previousCount = 0;
// default to mute status
mute: Mute = { mute: true };
private eventSource!: EventSource;
constructor(
private router: Router,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService,
private notifySvc: NzNotificationService,
private configSvc: GeneralConfigService,
private alertSvc: AlertService,
private modal: NzModalService,
private cdr: ChangeDetectorRef,
private alertSound: AlertSoundService
) {}
ngOnInit(): void {
let muteInit$ = this.configSvc
.getGeneralConfig('mute')
.pipe(
finalize(() => {
muteInit$.unsubscribe();
})
)
.subscribe(
message => {
if (message.code === 0) {
this.mute = message.data;
} else {
console.warn(message.msg);
}
},
error => {
console.error(error);
}
);
this.loadData();
this.initAlertSSEConnection();
this.initManagerSSEConnection();
}
ngOnDestroy() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
if (this.eventSource) {
this.eventSource.close();
}
}
onPopoverVisibleChange(visible: boolean): void {
if (visible) {
this.loadData();
}
}
updateNoticeData(notices: any[]): any[] {
const data = this.data.slice();
data.forEach(i => (i.list = []));
notices.forEach(item => {
data[0].list.push({ ...item });
});
return data;
}
loadData(): void {
if (this.loading) {
return;
}
this.loading = true;
let loadAlerts$ = this.alertSvc
.loadAlerts('firing', undefined, 0, 5)
.pipe(
finalize(() => {
loadAlerts$.unsubscribe();
this.loading = false;
})
)
.subscribe(
message => {
if (message.code === 0) {
let page = message.data;
let alerts = page.content;
if (alerts == undefined) {
return;
}
let list: any[] = [];
alerts.forEach(alert => {
let item = {
id: alert.id,
avatar: '/assets/img/notification.svg',
title: alert.content,
datetime: new Date(alert.activeAt).toLocaleString(),
color: 'blue',
status: alert.status,
type: this.i18nSvc.fanyi('dashboard.alerts.title-no')
};
list.push(item);
});
this.data = this.updateNoticeData(list);
this.count = page.totalElements;
} else {
console.warn(message.msg);
}
this.cdr.detectChanges();
},
error => {
console.error(error);
}
);
}
gotoAlertCenter(): void {
this.popoverVisible = false;
this.router.navigateByUrl(`/alert/center`);
}
gotoDetail(id: number): void {
this.popoverVisible = false;
// todo goto this alert detail pop page
// this.router.navigateByUrl(`/alarm/center/detail/${id}`);
}
toggleMute(event: MouseEvent): void {
event.stopPropagation();
const updatedMuteState = !this.mute.mute;
const updatedMuteConfig = { ...this.mute, mute: updatedMuteState };
// request notification permission
debugger;
if (!updatedMuteState) {
Notification.requestPermission().then(permission => {
if (permission !== 'granted') {
console.log('Notification permission denied.');
}
});
}
let saveConfig$ = this.configSvc
.saveGeneralConfig(updatedMuteConfig, 'mute')
.pipe(
finalize(() => {
saveConfig$.unsubscribe();
})
)
.subscribe({
next: response => {
if (response.code === 0) {
this.mute = updatedMuteConfig;
console.log('Config saved successfully', response);
} else {
console.warn('Error saving config', response.msg);
}
},
error: error => {
console.error('Error saving config', error);
},
complete: () => {
this.cdr.markForCheck();
}
});
}
private initAlertSSEConnection(): void {
const sseUrl = '/api/alert/sse/subscribe';
this.eventSource = new EventSource(sseUrl);
this.eventSource.addEventListener('ALERT_EVENT', (evt: MessageEvent) => {
let list: any[] = [];
let alert: SingleAlert = JSON.parse(evt.data);
let item = {
id: alert.id,
avatar: '/assets/img/notification.svg',
// title: `${alert.tags?.monitorName}--${this.i18nSvc.fanyi(`alert.severity.${alert.severity}`)}`,
title: alert.content,
datetime: new Date(alert.activeAt).toLocaleString(),
color: 'blue',
status: alert.status,
type: this.i18nSvc.fanyi('dashboard.alerts.title-no')
};
console.log('alert:', alert);
list.push(item);
this.data = this.updateNoticeData(list);
if (!this.mute.mute && !this.notifiedAlert.includes(alert.id)) {
this.notifiedAlert.push(alert.id);
this.alertSound.playAlertSound(this.i18nSvc.currentLang);
const notification = new Notification(this.i18nSvc.fanyi('alert.notify.title'), {
body: this.i18nSvc.fanyi('alert.notify.body'),
icon: 'assets/logo.svg'
});
notification.onclick = () => {
window.focus();
this.router.navigateByUrl(`/alert/center`);
notification.close();
};
}
this.cdr.detectChanges();
});
this.eventSource.onerror = error => {
console.error('SSE connection error:', error);
this.eventSource.close();
};
}
private initManagerSSEConnection(): void {
const sseUrl = '/api/manager/sse/subscribe';
this.eventSource = new EventSource(sseUrl);
this.eventSource.addEventListener('IMPORT_TASK_EVENT', (evt: MessageEvent) => {
let msg = JSON.parse(evt.data);
if (msg.notifyLevel === 'SUCCESS') {
this.notifySvc.success(
this.i18nSvc.fanyi('common.notice'),
this.i18nSvc.fanyi('common.notify.import-success-detail', { taskName: msg.taskName })
);
} else if (msg.notifyLevel === 'ERROR') {
this.notifySvc.error(
this.i18nSvc.fanyi('common.notice'),
this.i18nSvc.fanyi('common.notify.import-fail-detail', { taskName: msg.taskName, errMsg: msg.errMsg })
);
} else if (msg.notifyLevel === 'INFO') {
this.notifySvc.info(
this.i18nSvc.fanyi('common.notice'),
this.i18nSvc.fanyi('common.notify.import-progress', { taskName: msg.taskName, progress: msg.progress })
);
} else {
console.error('Parse message error, msg:', evt.data);
}
});
this.eventSource.onerror = error => {
console.error('Manager SSE connection error:', error);
this.eventSource.close();
};
}
}
@@ -0,0 +1,151 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Router } from '@angular/router';
import { SettingsService, User } from '@delon/theme';
import { LocalStorageService } from '../../../service/local-storage.service';
import { CONSTANTS } from '../../../shared/constants';
@Component({
selector: 'header-user',
template: `
<div class="alain-default__nav-item d-flex align-items-center px-sm" nz-dropdown nzPlacement="bottomRight" [nzDropdownMenu]="userMenu">
<nz-avatar [nzSrc]="user.avatar" nzSize="small" class="mr-sm"></nz-avatar>
{{ user.name }}
</div>
<nz-dropdown-menu #userMenu="nzDropdownMenu">
<div nz-menu class="width-sm">
<div nz-menu-item routerLink="/setting/settings/config">
<i nz-icon nzType="tool" class="mr-sm"></i>
{{ 'menu.extras.setting' | i18n }}
</div>
<div nz-menu-item (click)="logout()">
<i nz-icon nzType="logout" class="mr-sm"></i>
{{ 'menu.account.logout' | i18n }}
</div>
<div nz-menu-item (click)="showAboutModal()">
<i nz-icon nzType="environment" class="mr-sm"></i>
{{ 'menu.extras.about' | i18n }}
</div>
</div>
</nz-dropdown-menu>
<nz-modal
[(nzVisible)]="isAboutModalVisible"
[nzTitle]=""
[nzFooter]="null"
(nzOnCancel)="disAboutModal()"
nzClosable="false"
nzWidth="48%"
>
<div *nzModalContent class="-inner-content">
<div style="text-align: center">
<img style="height: 33px" src="./assets/brand.svg" alt="" />
</div>
<div style="margin-top: 10px; font-weight: bolder; font-size: small;">
{{ 'about.title' | i18n }}
</div>
<div style="margin-top: 10px; font-weight: normal; font-size: small;">
<nz-badge nzStatus="processing" [nzText]="'about.point.1' | i18n"></nz-badge>
<br />
<nz-badge nzStatus="processing" [nzText]="'about.point.2' | i18n"></nz-badge>
<br />
<nz-badge nzStatus="processing" [nzText]="'about.point.3' | i18n"></nz-badge>
<br />
<nz-badge nzStatus="processing" [nzText]="'about.point.4' | i18n"></nz-badge>
<br />
<nz-badge nzStatus="processing" [nzText]="'about.point.5' | i18n"></nz-badge>
<br />
<nz-badge nzStatus="processing" [nzText]="'about.point.6' | i18n"></nz-badge>
<br />
</div>
<div style="margin-top: 10px; font-weight: normal; font-size: small;">
{{ 'about.help' | i18n }}
</div>
<div style="margin-top: 10px; font-weight: bolder; font-size: medium;">
<a [href]="'https://github.com/apache/hertzbeat/releases/tag/' + version" target="_blank"> Apache HertzBeat™ {{ version }} </a>
</div>
<div style="margin-top: 10px; font-weight: normal; font-size: small;">
Copyright &copy; {{ currentYear }}
<nz-divider nzType="vertical"></nz-divider>
<a href="https://hertzbeat.apache.org" target="_blank">Apache HertzBeat™</a>
</div>
<label
style="margin-top: 16px;color:gray;font-size:13px"
(ngModelChange)="onNotShowAgainChange($event)"
nz-checkbox
[(ngModel)]="notShowAgain"
>{{ 'about.not-show-next-login' | i18n }}
</label>
<nz-divider></nz-divider>
<div style="margin-top: 10px; font-weight: bolder">
<span nz-icon nzType="github"></span>
<a href="//github.com/apache/hertzbeat" target="_blank"> {{ 'about.github' | i18n }} </a>
<nz-divider nzType="vertical"></nz-divider>
<span nz-icon nzType="bug"></span>
<a href="//github.com/apache/hertzbeat/issues" target="_blank"> {{ 'about.issue' | i18n }} </a>
<nz-divider nzType="vertical"></nz-divider>
<span nz-icon nzType="fork"></span>
<a href="//github.com/apache/hertzbeat/pulls" target="_blank"> {{ 'about.pr' | i18n }} </a>
<nz-divider nzType="vertical"></nz-divider>
<span nz-icon nzType="message"></span>
<a href="//discord.com/invite/Fb6M73htGr" target="_blank"> {{ 'about.discuss' | i18n }} </a>
<nz-divider nzType="vertical"></nz-divider>
<span nz-icon nzType="read"></span>
<a href="https://hertzbeat.apache.org/docs/" target="_blank"> {{ 'about.doc' | i18n }} </a>
<nz-divider nzType="vertical"></nz-divider>
<span nz-icon nzType="cloud-download"></span>
<a href="https://hertzbeat.apache.org/docs/start/upgrade" target="_blank"> {{ 'about.upgrade' | i18n }} </a>
<div style="float: right">
<nz-divider nzType="vertical"></nz-divider>
<span nz-icon nzType="star" nzTheme="twotone"></span>
<a href="//github.com/apache/hertzbeat" target="_blank"> {{ 'about.star' | i18n }} </a>
</div>
</div>
</div>
</nz-modal>
`,
changeDetection: ChangeDetectionStrategy.Default
})
export class HeaderUserComponent {
isAboutModalVisible = false;
notShowAgain = false;
version = CONSTANTS.VERSION;
currentYear = new Date().getFullYear();
get user(): User {
return this.settings.user;
}
private readonly notShowAgainKey = 'NOT_SHOW_ABOUT_NEXT_LOGIN';
constructor(private settings: SettingsService, private router: Router, private localStorageSvc: LocalStorageService) {
this.notShowAgain =
this.localStorageSvc.getData(this.notShowAgainKey) !== null
? JSON.parse(<string>this.localStorageSvc.getData(this.notShowAgainKey))
: false;
// @ts-ignore
if (router.getCurrentNavigation()?.previousNavigation?.finalUrl.toString() === '/passport/login' && !this.notShowAgain) {
this.showAndCloseAboutModal();
}
}
logout(): void {
this.localStorageSvc.clearAuthorization();
this.router.navigateByUrl('/passport/login');
}
showAboutModal() {
this.isAboutModalVisible = true;
}
disAboutModal() {
this.isAboutModalVisible = false;
}
showAndCloseAboutModal() {
this.isAboutModalVisible = true;
setTimeout(() => (this.isAboutModalVisible = false), 20000);
}
onNotShowAgainChange(value: boolean): void {
this.notShowAgain = value;
this.localStorageSvc.putData(this.notShowAgainKey, JSON.stringify(value));
}
}
@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'layout-blank',
template: `<router-outlet></router-outlet> `,
host: {
'[class.alain-blank]': 'true'
}
})
export class LayoutBlankComponent {}
+87
View File
@@ -0,0 +1,87 @@
/* eslint-disable import/order */
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { GlobalFooterModule } from '@delon/abc/global-footer';
import { AlainThemeModule, I18nPipe } from '@delon/theme';
import { LayoutDefaultModule } from '@delon/theme/layout-default';
import { SettingDrawerModule } from '@delon/theme/setting-drawer';
import { ThemeBtnModule } from '@delon/theme/theme-btn';
import { NzAutocompleteModule } from 'ng-zorro-antd/auto-complete';
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
import { NzBadgeModule } from 'ng-zorro-antd/badge';
import { NzDropDownModule } from 'ng-zorro-antd/dropdown';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzGridModule } from 'ng-zorro-antd/grid';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzSpinModule } from 'ng-zorro-antd/spin';
import { LayoutBasicComponent } from './basic/basic.component';
import { HeaderFullScreenComponent } from './basic/widgets/fullscreen.component';
import { HeaderI18nComponent } from './basic/widgets/i18n.component';
import { HeaderAiChatComponent } from './basic/widgets/chat-input.component';
import { HeaderUserComponent } from './basic/widgets/user.component';
import { HeaderNotifyComponent } from './basic/widgets/notify.component';
import { LayoutBlankComponent } from './blank/blank.component';
import { SettingDrawerI18nDirective } from './basic/directives/setting-drawer-i18n.directive';
const COMPONENTS = [LayoutBasicComponent, LayoutBlankComponent, HeaderI18nComponent];
const DIRECTIVES = [SettingDrawerI18nDirective];
const HEADER_COMPONENTS = [
HeaderAiChatComponent,
HeaderFullScreenComponent,
HeaderI18nComponent,
HeaderUserComponent,
HeaderNotifyComponent
];
import { LayoutPassportComponent } from './passport/passport.component';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzModalModule } from 'ng-zorro-antd/modal';
import { NzTagModule } from 'ng-zorro-antd/tag';
import { NzDividerModule } from 'ng-zorro-antd/divider';
import { NzListComponent, NzListItemActionComponent, NzListItemComponent, NzListItemMetaComponent } from 'ng-zorro-antd/list';
import { NzStringTemplateOutletDirective } from 'ng-zorro-antd/core/outlet';
import { NzTooltipDirective } from 'ng-zorro-antd/tooltip';
import { NzCheckboxComponent } from 'ng-zorro-antd/checkbox';
const PASSPORT = [LayoutPassportComponent];
@NgModule({
imports: [
CommonModule,
FormsModule,
RouterModule,
AlainThemeModule.forChild(),
ThemeBtnModule,
SettingDrawerModule,
LayoutDefaultModule,
GlobalFooterModule,
NzDropDownModule,
NzInputModule,
NzAutocompleteModule,
NzGridModule,
NzFormModule,
NzSpinModule,
NzBadgeModule,
NzAvatarModule,
NzIconModule,
NzButtonModule,
NzModalModule,
NzTagModule,
NzDividerModule,
NzListComponent,
NzListItemComponent,
NzListItemMetaComponent,
NzStringTemplateOutletDirective,
NzListItemActionComponent,
NzTooltipDirective,
NzCheckboxComponent,
I18nPipe
],
declarations: [...COMPONENTS, ...HEADER_COMPONENTS, ...PASSPORT, ...DIRECTIVES],
exports: [...COMPONENTS, ...PASSPORT]
})
export class LayoutModule {}
@@ -0,0 +1,58 @@
<div class="container">
<header-i18n showLangText="false" class="langs"></header-i18n>
<div class="wrap" style="display: flex; flex-direction: column">
<div class="top">
<div class="head">
<img class="logo" src="./assets/brand.svg" alt="" />
</div>
<div class="desc">{{ 'app.passport.desc' | i18n }}</div>
</div>
<div nz-row [nzGutter]="16">
<div nz-col [nzXs]="{ span: 24 }" [nzLg]="{ span: 10, offset: 2 }">
<div class="mobile-hide passport-left-content">
<div class="passport-intro">{{ 'app.passport.intro-1' | i18n }}<br />{{ 'app.passport.intro-2' | i18n }}</div>
<nz-divider></nz-divider>
<div class="passport-points">
<div class="point-item">
<nz-badge nzColor="white" nzStatus="processing"></nz-badge>
<span>{{ 'about.point.1' | i18n }}</span>
</div>
<div class="point-item">
<nz-badge nzColor="white" nzStatus="processing"></nz-badge>
<span>{{ 'about.point.2' | i18n }}</span>
</div>
<div class="point-item">
<nz-badge nzColor="white" nzStatus="processing"></nz-badge>
<span>{{ 'about.point.3' | i18n }}</span>
</div>
<div class="point-item">
<nz-badge nzColor="white" nzStatus="processing"></nz-badge>
<span>{{ 'about.point.4' | i18n }}</span>
</div>
<div class="point-item">
<nz-badge nzColor="white" nzStatus="processing"></nz-badge>
<span>{{ 'about.point.5' | i18n }}</span>
</div>
<div class="point-item">
<nz-badge nzColor="white" nzStatus="processing"></nz-badge>
<span>{{ 'about.point.6' | i18n }}</span>
</div>
</div>
<nz-divider></nz-divider>
</div>
</div>
<div nz-col [nzXs]="{ span: 24 }" [nzLg]="{ span: 12 }">
<router-outlet></router-outlet>
</div>
</div>
<global-footer style="border-top: 1px solid #d692e3; margin-top: auto">
<div style="margin-top: 16px">
Apache HertzBeat™ {{ version }}<br />
Copyright &copy; {{ currentYear }}
<a href="https://hertzbeat.apache.org" target="_blank">Apache HertzBeat™</a>
<br />
Licensed under the Apache License, Version 2.0
</div>
</global-footer>
</div>
</div>
@@ -0,0 +1,172 @@
@import '@delon/theme/index';
:host ::ng-deep {
.container {
display: flex;
flex-direction: column;
min-height: 100%;
background: #f0f2f5;
}
.langs {
width: 100%;
height: 40px;
line-height: 44px;
text-align: right;
.anticon {
margin-top: 24px;
margin-right: 24px;
font-size: 14px;
vertical-align: top;
cursor: pointer;
}
}
.wrap {
flex: 1;
padding: 12px 0;
}
.ant-form-item {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
@media (min-width: @screen-md-min) {
.container {
background-image: url("src/assets/bg.png");
background-repeat: no-repeat;
background-position: center;
background-attachment: fixed;
background-size: cover;
}
.wrap {
padding: 12px 0 8px;
}
}
.top {
text-align: center;
}
.header {
height: 44px;
line-height: 44px;
a {
text-decoration: none;
}
}
.logo {
height: 55px;
margin-right: 16px;
}
.title {
position: relative;
color: @heading-color;
font-weight: 600;
font-size: 33px;
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
vertical-align: middle;
}
.desc {
margin-top: 12px;
margin-bottom: 20px;
color: @text-color-secondary;
font-size: @font-size-base;
}
.passport-left-content {
margin-left: 10%;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
&.long-lang {
.passport-intro {
font-size: clamp(1.4rem, 2.5vw, 2.2rem);
}
.passport-points {
font-size: 12px;
}
}
nz-divider {
margin: 10px 0;
background: rgba(255, 255, 255, 0.2);
}
}
.passport-intro {
margin-top: 0;
font-weight: bolder;
font-size: clamp(1.6rem, 3vw, 2.4rem);
color: white;
line-height: 1.2;
word-break: break-word;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.passport-points {
margin-top: 8px;
font-weight: normal;
font-size: 13px;
color: white;
display: grid;
grid-template-columns: 1fr;
gap: 4px 24px;
@media (min-width: @screen-lg-min) {
.long-lang & {
grid-template-columns: 1fr 1fr;
}
@media (max-height: 750px) {
grid-template-columns: 1fr 1fr;
}
}
.point-item {
display: flex;
align-items: flex-start;
line-height: 1.4;
margin-bottom: 4px;
nz-badge {
margin-top: 5px;
margin-right: 8px;
flex-shrink: 0;
}
span {
flex: 1;
opacity: 0.9;
}
}
}
}
[data-theme='dark'] {
:host ::ng-deep {
.container {
background: #141414;
}
.title {
color: fade(@white, 85%);
}
.desc {
color: fade(@white, 45%);
}
.global-footer__copyright {
color: fade(@white, 45%);
}
.global-footer__links-item {
color: fade(@white, 45%);
}
@media (min-width: @screen-md-min) {
.container {
background-image: none;
}
}
}
}
[data-theme='compact'] {
:host ::ng-deep {
.ant-form-item {
margin-bottom: 16px;
}
}
}
@@ -0,0 +1,23 @@
import { Component, Inject, OnInit } from '@angular/core';
import { I18NService } from '@core';
import { DA_SERVICE_TOKEN, ITokenService } from '@delon/auth';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { CONSTANTS } from '../../shared/constants';
@Component({
selector: 'layout-passport',
templateUrl: './passport.component.html',
styleUrls: ['./passport.component.less']
})
export class LayoutPassportComponent implements OnInit {
version = CONSTANTS.VERSION;
currentYear = new Date().getFullYear();
constructor(@Inject(DA_SERVICE_TOKEN) private tokenService: ITokenService, @Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService) {}
ngOnInit(): void {
this.tokenService.clear();
}
}
+41
View File
@@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class AlertDefine {
id!: number;
name!: string;
// realtime_metric, periodic_metric, realtime_log, periodic_log
type: string = 'realtime_metric';
// datasource when type is periodic, promql | sql
datasource: string = 'promql';
expr!: string;
// unit second
period: number = 300;
times: number = 3;
// severity: info, warning, critical, emergency, fatal
labels!: Record<string, string>;
annotations!: Record<string, string>;
enable: boolean = true;
template!: string;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
priority: number = 2;
}
+29
View File
@@ -0,0 +1,29 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Monitor } from './Monitor';
export class AlertDefineBind {
id!: number;
alertDefineId!: number;
monitorId!: number;
monitor!: Monitor;
gmtCreate!: number;
gmtUpdate!: number;
}
@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class AlertGroupConverge {
id!: number;
name!: string;
enable: boolean = true;
groupLabels!: string[];
// 30 seconds
groupWait: number = 30;
// 5 minutes
groupInterval: number = 300;
// 4 hours
repeatInterval: number = 14400;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
+31
View File
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class AlertInhibit {
id!: number;
name!: string;
enable: boolean = true;
sourceLabels!: Record<string, string>;
targetLabels!: Record<string, string>;
equalLabels!: string[];
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
+35
View File
@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class AlertSilence {
id!: number;
name!: string;
enable: boolean = true;
matchAll: boolean = true;
type: number = 0;
times!: number;
labels!: Record<string, string>;
days!: number[];
periodStart: Date = new Date();
periodEnd: Date = new Date();
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
+25
View File
@@ -0,0 +1,25 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class AlibabaSmsConfig {
accessKeyId!: string;
accessKeySecret!: string;
signName!: string;
templateCode!: string;
}
+27
View File
@@ -0,0 +1,27 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class AppCount {
category!: string;
app!: string;
size: number = 0;
availableSize: number = 0;
unManageSize: number = 0;
unAvailableSize: number = 0;
}
+24
View File
@@ -0,0 +1,24 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class AwsSmsConfig {
accessKeyId!: string;
accessKeySecret!: string;
region!: string;
}
+28
View File
@@ -0,0 +1,28 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Fields } from './Fields';
export class BulletinDefine {
id!: number;
name!: string;
monitorIds!: number[];
app!: string;
fields: Fields = {};
}
+32
View File
@@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class Collector {
id!: number;
name!: string;
ip!: string;
version!: string;
// public or private
mode!: string;
// collector status: 0-online 1-offline
status!: number;
gmtCreate!: number;
gmtUpdate!: number;
tmp!: any;
}
+26
View File
@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Collector } from './Collector';
export class CollectorSummary {
collector!: Collector;
pinMonitorNum!: number;
dispatchMonitorNum!: number;
}
+33
View File
@@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class EmailNoticeSender {
id!: number;
emailHost!: string;
emailPort!: number;
emailUsername!: string;
emailPassword!: string;
emailSsl: boolean = true;
emailStarttls: boolean = false;
enable!: boolean;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
+22
View File
@@ -0,0 +1,22 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export interface Fields {
[key: string]: string[];
}
+26
View File
@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Grafana pojo
export class GrafanaDashboard {
// is enabled
enabled: boolean = false;
// grafana template json
template!: string;
// dashboard url
url!: string;
}
+35
View File
@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SingleAlert } from './SingleAlert';
export class GroupAlert {
id!: number;
groupKey!: string;
status!: string;
groupLabels!: Record<string, string>;
commonLabels!: Record<string, string>;
commonAnnotations!: Record<string, string>;
alertFingerprints!: string[];
alerts!: SingleAlert[];
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
+31
View File
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class Label {
id!: number;
name!: string;
tagValue!: string;
description!: string;
// type -- 0:auto-generated by system 1: user generate 2: system preset
type!: number;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
+37
View File
@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class LogEntry {
timeUnixNano?: number;
observedTimeUnixNano?: number;
severityNumber?: number;
severityText?: string;
body?: any;
attributes?: { [key: string]: any };
droppedAttributesCount?: number;
traceId?: string;
spanId?: string;
traceFlags?: number;
resource?: { [key: string]: any };
instrumentationScope?: {
name?: string;
version?: string;
attributes?: { [key: string]: any };
};
}
+24
View File
@@ -0,0 +1,24 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class Message<T> {
data!: T;
msg!: string;
code: number = 0;
}
@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class ModelProviderConfig {
error!: string;
type!: string;
code: string = 'openai';
baseUrl: string = '';
model: string = '';
apiKey!: string;
participationModel: string = 'PROTECTED';
}
export interface ProviderOption {
value: string;
label: string;
defaultBaseUrl: string;
defaultModel: string;
}
export const PROVIDER_OPTIONS: ProviderOption[] = [
{
value: 'openai',
label: 'OpenAI',
defaultBaseUrl: 'https://api.openai.com/v1',
defaultModel: 'gpt-4'
},
{
value: 'zai',
label: 'ZAI',
defaultBaseUrl: 'https://api.z.ai/api/paas/v4',
defaultModel: 'glm-4.6'
},
{
value: 'zhipu',
label: 'ZhiPu',
defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4',
defaultModel: 'glm-4.6'
}
];
+46
View File
@@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class Monitor {
id!: number;
name!: string;
app!: string;
scrape!: string;
instance!: string;
intervals: number = 60;
// Schedule type: interval | cron
scheduleType: string = 'interval';
// Cron expression when scheduleType is cron
cronExpression?: string;
// Monitoring status 0: Paused, 1: Up, 2: Down
status!: number;
// Task type 0: Normal, 1: push auto create, 2: discovery auto create
type!: number;
description!: string;
labels!: Record<string, string>;
annotations!: Record<string, string>;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
_displayStatus?: 'ACTIVE' | 'DISAPPEARED' | 'GRACE_PERIOD';
_graceTimer?: any;
_disappearTime?: number;
}
+22
View File
@@ -0,0 +1,22 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class Mute {
mute!: boolean;
}
+63
View File
@@ -0,0 +1,63 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class NoticeReceiver {
id!: number;
name!: string;
// notification mode: 0-sms 1-email 2-webhook 3-wechat public account 4-work wechat robot 5-Dingding robot 6-Feishu robot
// 7-Telegram robot 8-SlackWebHook 9-Discord robot 10-work wechat app message 11-Huawei cloud SMN 12-ServerChan 13-Gotify
// 14-FeiShu app message 15-Ntfy
type: number = 1;
phone!: string;
email!: string;
tgBotToken!: string;
tgUserId!: string;
tgMessageThreadId!: string;
userId!: string;
slackWebHookUrl!: string;
discordChannelId!: string;
discordBotToken!: string;
hookUrl!: string;
hookAuthType!: string;
hookAuthToken!: string;
wechatId!: string;
accessToken!: string;
corpId!: string;
agentId!: number;
appSecret!: string;
partyId!: string;
tagId!: string;
smnAk!: string;
smnSk!: string;
smnProjectId!: string;
smnRegion!: string;
smnTopicUrn!: string;
serverChanToken!: string;
gotifyToken!: string;
ntfyServerUrl!: string;
ntfyTopic!: string;
ntfyToken!: string;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
appId!: string;
larkReceiveType!: number;
chatId!: string;
}
+38
View File
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class NoticeRule {
id!: number;
name!: string;
receiverId!: number[];
receiverName!: string[];
templateId!: number | null;
templateName!: string | null;
enable: boolean = true;
// forward all or not
filterAll: boolean = true;
labels!: Record<string, string>;
days!: number[];
periodStart!: Date;
periodEnd!: Date;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
+32
View File
@@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class NoticeTemplate {
id!: number;
name!: string;
// notification mode: 0-sms 1-email 2-webhook 3-wechat public account 4-work wechat robot 5-Dingding robot 6-Feishu robot
// 7-Telegram robot 8-SlackWebHook 9-Discord robot 10-work wechat app message 11-Huawei cloud SMN 12-ServerChan 13-Gotify
type!: number;
preset!: boolean;
creator!: string;
modifier!: string;
content!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
+49
View File
@@ -0,0 +1,49 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class ObjectStore<T> {
type: ObjectStoreType = ObjectStoreType.DATABASE;
config!: T;
appDefineStoreType!: ObjectStoreType;
}
export enum ObjectStoreType {
/**
* Local file
*/
FILE = 'FILE',
/**
* Local database
*/
DATABASE = 'DATABASE',
/**
* <a href="https://support.huaweicloud.com/obs/index.html">Huawei cloud OBS</a>
*/
OBS = 'OBS'
}
export class ObsConfig {
accessKey!: string;
secretKey!: string;
bucketName!: string;
endpoint!: string;
savePath: string = 'hertzbeat';
}
+30
View File
@@ -0,0 +1,30 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class Page<T> {
content!: T[];
totalPages!: number;
totalElements!: number;
// page size
size!: number;
// page index, start from 0
number!: number;
// the number of elements in this page
numberOfElements!: number;
}
+26
View File
@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class Param {
id!: number;
field!: string;
type: number | undefined;
paramValue: any;
display: boolean = true;
}
+39
View File
@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { List } from 'echarts';
export class ParamDefine {
name!: string;
field!: string;
type!: string;
required: boolean = false;
defaultValue: string | undefined;
placeholder!: string;
range: string | undefined;
limit: number | undefined;
options!: any[];
// key alias. This param is valid when the type is key-value
keyAlias!: string;
valueAlias!: string;
// whether the param is hidden, default is false
hide: boolean = false;
// Map of dependent params
depend: Map<string, List<any>> | undefined;
}
+28
View File
@@ -0,0 +1,28 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PluginItem } from './PluginItem';
export class Plugin {
id!: number;
name!: string;
enableStatus!: boolean;
items!: PluginItem[];
paramCount!: number;
}
+22
View File
@@ -0,0 +1,22 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class PluginItem {
type!: string;
}
+35
View File
@@ -0,0 +1,35 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class SingleAlert {
id!: number;
fingerprint!: string;
labels!: Record<string, string>;
annotations!: Record<string, string>;
content!: string;
status!: string;
startAt!: number;
endAt!: number;
activeAt!: number;
triggerTimes!: number;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
+42
View File
@@ -0,0 +1,42 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { AlibabaSmsConfig } from './AlibabaSmsConfig';
import { AwsSmsConfig } from './AwsSmsConfig';
import { SmslocalSmsConfig } from './SmslocalSmsConfig';
import { TencentSmsConfig } from './TencentSmsConfig';
import { TwilioSmsConfig } from './TwilioSmsConfig';
import { UniSmsConfig } from './UniSmsConfig';
import { SmsType } from './enums/sms-type.enum';
export class SmsNoticeSender {
id!: number;
type: SmsType = SmsType.TENCENT;
tencent: TencentSmsConfig = new TencentSmsConfig();
alibaba: AlibabaSmsConfig = new AlibabaSmsConfig();
unisms: UniSmsConfig = new UniSmsConfig();
smslocal: SmslocalSmsConfig = new SmslocalSmsConfig();
aws: AwsSmsConfig = new AwsSmsConfig();
twilio: TwilioSmsConfig = new TwilioSmsConfig();
enable: boolean = false;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
+22
View File
@@ -0,0 +1,22 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class SmslocalSmsConfig {
apiKey!: string;
}
@@ -0,0 +1,36 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class StatusPageComponent {
id!: number;
orgId!: number;
name!: string;
description!: string;
labels!: Record<string, string>;
// calculate status method: 0-auto 1-manual
method: number = 0;
// config state when use manual method: 0-Normal 1-Abnormal 2-unknown
configState: number = 0;
// component status when use auto method: 0-Normal 1-Abnormal 2-unknown
state: number = 0;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { StatusPageComponent } from './StatusPageComponent';
import { StatusPageHistory } from './StatusPageHistory';
export class StatusPageComponentStatus {
info!: StatusPageComponent;
history!: StatusPageHistory[];
}
+33
View File
@@ -0,0 +1,33 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class StatusPageHistory {
id!: number;
componentId!: number;
state!: number;
timestamp!: number;
uptime!: number;
abnormal!: number;
unknowing!: number;
normal!: number;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
@@ -0,0 +1,40 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { StatusPageComponent } from './StatusPageComponent';
import { StatusPageIncidentContent } from './StatusPageIncidentContent';
export class StatusPageIncident {
id!: number;
orgId!: number;
name!: string;
// incident current state: 0-Investigating 1-Identified 2-Monitoring 3-Resolved
state: number = 0;
// incident start Investigating timestamp
startTime!: number;
// incident end Resolved timestamp
endTime!: number;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
components!: StatusPageComponent[];
contents!: StatusPageIncidentContent[];
}
@@ -0,0 +1,30 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class StatusPageIncidentContent {
id!: number;
incidentId!: number;
message!: string;
state: number = 0;
timestamp!: number;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
+34
View File
@@ -0,0 +1,34 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class StatusPageOrg {
id!: number;
name!: string;
// org current state: 0-All Systems Operational 1-Some Systems Abnormal 2-All Systems Abnormal
state!: number;
description!: string;
home!: string;
logo!: string;
feedback!: string;
color!: string;
creator!: string;
modifier!: string;
gmtCreate!: number;
gmtUpdate!: number;
}
+24
View File
@@ -0,0 +1,24 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class SystemConfig {
timeZoneId!: string;
locale!: string;
theme!: string;
}
+22
View File
@@ -0,0 +1,22 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class TemplateConfig {
apps!: Record<string, any>;
}
+26
View File
@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class TencentSmsConfig {
secretId!: string;
secretKey!: string;
signName!: string;
appId!: string;
templateId!: string;
}
+24
View File
@@ -0,0 +1,24 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class TwilioSmsConfig {
accountSid!: string;
authToken!: string;
twilioPhoneNumber!: string;
}
+26
View File
@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export class UniSmsConfig {
accessKeyId!: string;
accessKeySecret!: string;
signature!: string;
authMode!: string;
templateId!: string;
}
@@ -0,0 +1,32 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export enum SmsType {
TENCENT = 'tencent',
ALIBABA = 'alibaba',
UNISMS = 'unisms',
SMSLOCAL = 'smslocal',
AWS = 'aws',
TWILIO = 'twilio'
}
export enum UniSmsAuthMode {
HMAC = 'hmac',
SIMPLE = 'simple'
}
+31
View File
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Pipe({
standalone: true,
name: 'safe'
})
export class SafePipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}
transform(url: string, type: string): SafeResourceUrl {
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
}
@@ -0,0 +1,209 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<app-help-message-show
[help_message_content]="'alert.help.center' | i18n"
[guild_link]="'alert.help.center.link' | i18n"
[module_name]="'menu.alert.center'"
[icon_name]="'alert'"
></app-help-message-show>
<nz-divider></nz-divider>
<app-toolbar>
<ng-template #center>
<div class="center-content">
<div class="search-wrapper">
<nz-input-group [nzPrefix]="prefixTemplate" class="search-input">
<input
type="text"
nz-input
[(ngModel)]="filterContent"
(keydown.enter)="loadAlertsTable()"
[placeholder]="'alert.center.search' | i18n"
/>
</nz-input-group>
<ng-template #prefixTemplate>
<i nz-icon nzType="search"></i>
</ng-template>
</div>
<nz-select
class="mobile-hide"
nzAllowClear
[nzPlaceHolder]="'alert.center.filter-status' | i18n"
[(ngModel)]="filterStatus"
(ngModelChange)="loadAlertsTable()"
>
<nz-option [nzLabel]="'alert.status.firing' | i18n" [nzValue]="'firing'"></nz-option>
<nz-option [nzLabel]="'alert.status.resolved' | i18n" [nzValue]="'resolved'"></nz-option>
</nz-select>
</div>
</ng-template>
</app-toolbar>
<div class="alert-cards">
<nz-card
*ngFor="let group of groupAlerts"
class="alert-card"
[class.new-alert]="group.isNew"
[class]="'status-' + group.status"
[nzBordered]="false"
>
<!-- Alert Group Header -->
<div class="alert-header">
<div class="alert-info">
<div class="alert-labels">
<nz-tag *ngFor="let item of group.groupLabels | keyvalue">{{ item.key }}:{{ item.value }}</nz-tag>
</div>
<div class="alert-meta-info">
<span class="alert-time">
<i nz-icon nzType="clock-circle" nzTheme="outline"></i>
{{ group.gmtUpdate | date : 'yyyy-MM-dd HH:mm:ss' }}
</span>
</div>
</div>
<div class="alert-actions">
<!-- <button-->
<!-- *ngIf="group.status != 'firing'"-->
<!-- nz-button-->
<!-- (click)="onMarkReadOneAlert(group.id)"-->
<!-- nz-tooltip-->
<!-- [nzTooltipTitle]="'alert.center.deal' | i18n"-->
<!-- >-->
<!-- <i nz-icon nzType="down-circle" nzTheme="outline"></i>-->
<!-- </button>-->
<!-- <button-->
<!-- *ngIf="group.status == 'firing'"-->
<!-- nz-button-->
<!-- (click)="onMarkUnReadOneAlert(group.id)"-->
<!-- nz-tooltip-->
<!-- [nzTooltipTitle]="'alert.center.no-deal' | i18n"-->
<!-- >-->
<!-- <i nz-icon nzType="up-circle" nzTheme="outline"></i>-->
<!-- </button>-->
<button nz-button nzDanger (click)="onDeleteOneAlert(group.id)" nz-tooltip [nzTooltipTitle]="'alert.center.delete' | i18n">
<i nz-icon nzType="delete" nzTheme="outline"></i>
</button>
</div>
</div>
<!-- Alert Details -->
<div class="alert-details">
<nz-collapse nzGhost>
<nz-collapse-panel
#panel
*ngFor="let item of group.alerts"
[nzHeader]="alertHeader"
[nzExtra]="alertExtra"
[nzActive]="false"
[style]="'border-left-color:' + (item.status == 'firing' ? '#ff4d4f' : '#52c41a')"
[nzExpandedIcon]="expandedIcon"
>
<ng-template #alertHeader>
<div class="alert-content">
<nz-tag *ngIf="item.labels.alertname" style="font-size: 14px; margin-right: 8px">{{ item.labels.alertname }}</nz-tag>
<span>{{ item.content }}</span>
</div>
</ng-template>
<ng-template #alertExtra>
<div class="alert-time">{{
item.endAt ? (item.endAt | date : 'yyyy-MM-dd HH:mm:ss') : (item.activeAt | date : 'yyyy-MM-dd HH:mm:ss')
}}</div>
</ng-template>
<ng-template #expandedIcon>
<i nz-icon [nzType]="panel.nzActive ? 'caret-down' : 'caret-right'"></i>
</ng-template>
<!-- Trigger Times -->
<div class="detail-section" *ngIf="item.triggerTimes">
<span class="alert-count" *ngIf="group?.alerts">
<i style="margin-right: 4px" nz-icon nzType="alert" nzTheme="outline"></i>
{{ 'alert.center.time.tip' | i18n : { times: item.triggerTimes } }}
</span>
</div>
<!-- Status -->
<div class="detail-section">
<div class="section-title">{{ 'alert.center.status' | i18n }}</div>
<div class="alert-status">
<nz-tag [nzColor]="item.status === 'firing' ? 'error' : 'success'">
{{ item.status === 'firing' ? ('alert.status.firing' | i18n) : ('alert.status.resolved' | i18n) }}
</nz-tag>
</div>
</div>
<!-- Labels -->
<div class="detail-section" *ngIf="item.labels">
<div class="section-title">{{ 'alert.center.labels' | i18n }}</div>
<div class="alert-labels">
<nz-tag *ngFor="let label of item.labels | keyvalue">{{ label.key }}:{{ label.value }}</nz-tag>
</div>
</div>
<!-- Annotations -->
<div class="detail-section" *ngIf="item.annotations">
<div class="section-title">{{ 'common.annotation' | i18n }}</div>
<div class="alert-annotations">
<div *ngFor="let anno of item.annotations | keyvalue" class="annotation-item">
<span class="annotation-key">{{ anno.key }}:</span>
<span class="annotation-value">
<markdown [data]="anno.value"></markdown>
</span>
</div>
</div>
</div>
<!-- Time Info -->
<div class="detail-section">
<div class="section-title">{{ 'alert.center.time' | i18n }}</div>
<div class="time-info">
<div class="time-item">
<span class="time-label">{{ 'alert.center.first-time' | i18n }}:</span>
<span class="time-value">{{ item.startAt | date : 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
<div *ngIf="item.status === 'firing' && item.activeAt" class="time-item">
<span class="time-label">{{ 'alert.center.last-time' | i18n }}:</span>
<span class="time-value">{{ item.activeAt | date : 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
<div *ngIf="item.status === 'resolved' && item.endAt" class="time-item">
<span class="time-label">{{ 'alert.center.end-time' | i18n }}:</span>
<span class="time-value">{{ item.endAt | date : 'yyyy-MM-dd HH:mm:ss' }}</span>
</div>
</div>
</div>
</nz-collapse-panel>
</nz-collapse>
</div>
</nz-card>
</div>
<nz-pagination
class="pagination"
[nzTotal]="total"
[(nzPageIndex)]="pageIndex"
[(nzPageSize)]="pageSize"
[nzShowSizeChanger]="true"
[nzShowQuickJumper]="true"
[nzShowTotal]="rangeTemplate"
(nzPageIndexChange)="loadAlertsTable()"
(nzPageSizeChange)="loadAlertsTable()"
></nz-pagination>
<ng-template #rangeTemplate let-range="range" let-total> {{ 'common.total' | i18n }} {{ total }} </ng-template>
@@ -0,0 +1,432 @@
@import "~src/styles/theme";
:host ::ng-deep app-toolbar {
.center-content {
display: flex;
align-items: center;
gap: 16px;
width: 100%;
padding: 0 16px;
button {
flex-shrink: 0;
}
nz-select {
min-width: 120px;
flex-shrink: 0;
}
}
}
.search-wrapper {
display: flex;
justify-content: center;
width: 100%;
max-width: 600px;
margin: 0 auto;
padding: 0;
}
.search-input {
width: 100%;
:global {
.ant-input {
height: 40px;
font-size: 14px;
border-radius: 6px;
padding: 4px 11px 4px 40px;
}
}
}
@media screen and (max-width: 768px) {
.search-wrapper {
max-width: 100%;
padding: 12px 16px;
}
}
.alert-cards {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
transform-style: preserve-3d;
perspective: 1200px;
}
.alert-card {
position: relative;
background: @common-background-color;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
z-index: 1;
&.expanded {
z-index: 2;
}
&.status-firing {
border-left: 4px solid #ff4d4f;
opacity: 0.9;
}
&.status-resolved {
border-left: 4px solid #52c41a;
opacity: 0.7;
}
&.status-pending {
border-left: 4px solid #faad14;
opacity: 0.8;
}
&:hover {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
z-index: 3;
}
::ng-deep .ant-card-body {
padding: 12px 16px;
}
.alert-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding-bottom: 12px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
.alert-info {
flex: 1;
.alert-labels {
display: flex;
flex-wrap: wrap;
gap: 6px;
nz-tag {
margin: 0;
border-radius: 2px;
}
}
.alert-meta-info {
display: flex;
gap: 16px;
margin-top: 8px;
color: rgba(0, 0, 0, 0.5);
font-size: 12px;
i {
margin-right: 4px;
}
.alert-time {
display: flex;
align-items: center;
}
}
}
.alert-actions {
display: flex;
gap: 8px;
margin-left: 16px;
flex-shrink: 0;
button {
padding: 4px 8px;
height: 28px;
i {
font-size: 14px;
}
}
}
}
.alert-details {
margin-top: 12px;
position: relative;
z-index: 4;
::ng-deep {
.ant-collapse {
background: transparent;
border: none;
position: relative;
z-index: 5;
.ant-collapse-item {
border-radius: 2px;
border: 1px solid rgba(0, 0, 0, 0.1);
margin-bottom: 8px;
position: relative;
z-index: 6;
&:last-child {
margin-bottom: 0;
}
.ant-collapse-header {
padding: 8px 12px;
align-items: center;
cursor: pointer;
position: relative;
z-index: 7;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.ant-collapse-header-text {
flex: 1;
}
.ant-collapse-extra {
margin: 0;
color: rgba(0, 0, 0, 0.5);
font-size: 12px;
}
.alert-content {
font-size: 13px;
line-height: 1.5;
}
}
.ant-collapse-content {
border-top: 1px solid rgba(0, 0, 0, 0.1);
position: relative;
z-index: 6;
.ant-collapse-content-box {
padding: 12px;
}
}
}
}
}
.detail-section {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 13px;
color: #595959;
margin-bottom: 8px;
font-weight: 500;
}
.alert-count {
display: flex;
align-items: center;
color: #8c8c8c;
font-size: 12px;
}
}
.alert-annotations {
.annotation-item {
display: flex;
margin-bottom: 4px;
font-size: 13px;
&:last-child {
margin-bottom: 0;
}
.annotation-key {
color: #8c8c8c;
margin-right: 8px;
min-width: 80px;
}
.annotation-value {
color: #262626;
flex: 1;
}
}
}
.time-info {
.time-item {
display: flex;
margin-bottom: 4px;
font-size: 13px;
&:last-child {
margin-bottom: 0;
}
.time-label {
color: #8c8c8c;
margin-right: 8px;
min-width: 80px;
}
.time-value {
color: #262626;
}
}
}
}
}
.pagination {
margin: 16px;
text-align: right;
}
@keyframes slideInFromRight {
0% {
transform: translate3d(120%, 0, 0) scale(0.95) rotate(3deg);
opacity: 0;
filter: blur(2px);
box-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.18);
}
50% {
opacity: 1;
filter: blur(0);
transform: translate3d(-5%, 0, 0) scale(1) rotate(-1deg);
}
75% {
transform: translate3d(2%, 0, 0) scale(1) rotate(0.5deg);
}
100% {
transform: translate3d(0, 0, 0) scale(1) rotate(0deg);
box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.1);
}
}
.alert-card {
transition:
transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94),
opacity 0.5s ease-out,
box-shadow 0.3s ease;
will-change: transform, opacity;
&:hover {
transform: translateY(-2px) scale(1.005);
box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.15);
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.3) 50%,
rgba(255, 255, 255, 0) 100%
);
opacity: 0;
animation: slideGlow 1s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
&.new-alert {
animation:
slideInFromRight 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards,
cardLanding 0.6s 0.3s ease-out forwards;
}
}
@keyframes slideGlow {
0% {
transform: translateX(-100%);
opacity: 0;
}
50% {
opacity: 0.6;
}
100% {
transform: translateX(100%);
opacity: 0;
}
}
@keyframes cardLanding {
0% { transform: translateY(0); }
50% { transform: translateY(-8px); }
80% { transform: translateY(2px); }
100% { transform: translateY(0); }
}
.alert-card:nth-child(1) { animation-delay: 0.1s; }
.alert-card:nth-child(2) { animation-delay: 0.15s; }
.alert-card:nth-child(3) { animation-delay: 0.2s; }
.alert-card:nth-child(n+4) { animation-delay: 0.25s; }
[data-theme='dark'] {
:host {
.alert-card {
background-color: @common-background-color-dark;
.alert-header {
.alert-info {
.alert-meta-info {
color: rgb(214, 214, 214);
}
}
}
.alert-details {
::ng-deep {
.ant-collapse {
.ant-collapse-item {
.ant-collapse-header {
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.ant-collapse-extra {
color: rgb(236, 236, 236);
}
}
}
}
}
.detail-section {
.section-title {
color: #cdcdcd;
}
.alert-count {
color: #ececec;
}
}
.alert-annotations {
.annotation-item {
.annotation-key {
color: #dfdede;
}
.annotation-value {
color: #e3e3e3;
}
}
}
.time-info {
.time-item {
.time-label {
color: #dfdede;
}
.time-value {
color: #e3e3e3;
}
}
}
}
}
}
}
@@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AlertCenterComponent } from './alert-center.component';
describe('AlertCenterComponent', () => {
let component: AlertCenterComponent;
let fixture: ComponentFixture<AlertCenterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AlertCenterComponent]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AlertCenterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,301 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzNotificationService } from 'ng-zorro-antd/notification';
import { GroupAlert } from '../../../pojo/GroupAlert';
import { AlertService } from '../../../service/alert.service';
interface ExtendedGroupAlert extends GroupAlert {
isNew?: boolean;
}
@Component({
selector: 'app-alert-center',
templateUrl: './alert-center.component.html',
styleUrl: './alert-center.component.less'
})
export class AlertCenterComponent implements OnInit, OnDestroy {
constructor(
private notifySvc: NzNotificationService,
private modal: NzModalService,
private alertSvc: AlertService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService
) {}
pageIndex: number = 1;
pageSize: number = 8;
total: number = 0;
groupAlerts: ExtendedGroupAlert[] = [];
tableLoading: boolean = false;
checkedAlertIds = new Set<number>();
filterStatus!: string;
filterContent: string | undefined;
private eventSource!: EventSource;
ngOnInit(): void {
this.loadAlertsTable();
this.initSSESubscription();
}
ngOnDestroy(): void {
if (this.eventSource) {
this.eventSource.close();
}
}
// Initialize SSE subscription for real-time alerts
private initSSESubscription(): void {
this.eventSource = new EventSource('/api/alert/sse/subscribe');
this.eventSource.addEventListener('ALERT_EVENT', (evt: MessageEvent) => {
try {
const newAlert: GroupAlert = JSON.parse(evt.data);
this.updateAlertList(newAlert);
} catch (error) {
console.error('Error parsing SSE data:', error);
}
});
// Handle SSE errors
this.eventSource.onerror = error => {
console.error('SSE connection error:', error);
this.eventSource.close();
};
}
private updateAlertList(newAlert: GroupAlert): void {
const extendedAlert: ExtendedGroupAlert = {
...newAlert,
isNew: true
};
if (!extendedAlert.alerts) {
extendedAlert.alerts = [];
}
const matchesFilter = this.checkAlertMatchesFilter(extendedAlert);
if (!matchesFilter) {
return;
}
const existingIndex = this.groupAlerts.findIndex(a => a.id === extendedAlert.id);
if (existingIndex === -1) {
this.groupAlerts = [extendedAlert, ...this.groupAlerts];
this.total += 1;
setTimeout(() => {
const index = this.groupAlerts.findIndex(a => a.id === extendedAlert.id);
if (index !== -1) {
this.groupAlerts[index].isNew = false;
// 触发变更检测
this.groupAlerts = [...this.groupAlerts];
}
}, 1000);
} else {
this.groupAlerts[existingIndex] = {
...extendedAlert,
isNew: true
};
setTimeout(() => {
if (this.groupAlerts[existingIndex]) {
this.groupAlerts[existingIndex].isNew = false;
this.groupAlerts = [...this.groupAlerts];
}
}, 1000);
this.groupAlerts = [...this.groupAlerts];
}
}
private checkAlertMatchesFilter(alert: ExtendedGroupAlert): boolean {
if (this.filterStatus && alert.status !== this.filterStatus) {
return false;
}
if (this.filterContent) {
const searchContent = this.filterContent.toLowerCase();
const hasMatchingContent = alert.alerts?.some(singleAlert => singleAlert.content?.toLowerCase().includes(searchContent));
const hasMatchingLabels = Object.entries(alert.groupLabels || {}).some(
([key, value]) => key.toLowerCase().includes(searchContent) || value.toLowerCase().includes(searchContent)
);
if (!hasMatchingContent && !hasMatchingLabels) {
return false;
}
}
return true;
}
loadAlertsTable() {
this.tableLoading = true;
let alertsInit$ = this.alertSvc.loadGroupAlerts(this.filterStatus, this.filterContent, this.pageIndex - 1, this.pageSize).subscribe(
message => {
if (message.code === 0) {
let page = message.data;
this.groupAlerts = page.content;
this.groupAlerts.forEach(alert => {
if (alert.alerts == undefined) {
alert.alerts = [];
}
});
this.pageIndex = page.number + 1;
this.total = page.totalElements;
} else {
console.warn(message.msg);
}
this.tableLoading = false;
alertsInit$.unsubscribe();
},
error => {
this.tableLoading = false;
alertsInit$.unsubscribe();
console.error(error.msg);
}
);
}
onDeleteAlerts() {
if (this.checkedAlertIds == null || this.checkedAlertIds.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.deleteAlerts(this.checkedAlertIds)
});
}
onMarkReadAlerts() {
if (this.checkedAlertIds == null || this.checkedAlertIds.size === 0) {
this.notifySvc.warning(this.i18nSvc.fanyi('alert.center.notify.no-mark'), '');
return;
}
this.modal.confirm({
nzTitle: this.i18nSvc.fanyi('alert.center.confirm.mark-done-batch'),
nzOkText: this.i18nSvc.fanyi('common.button.ok'),
nzCancelText: this.i18nSvc.fanyi('common.button.cancel'),
nzOkDanger: true,
nzOkType: 'primary',
nzClosable: false,
nzOnOk: () => this.updateAlertsStatus(this.checkedAlertIds, 'resolved')
});
}
onMarkUnReadAlerts() {
if (this.checkedAlertIds == null || this.checkedAlertIds.size === 0) {
this.notifySvc.warning(this.i18nSvc.fanyi('alert.center.notify.no-mark'), '');
return;
}
this.modal.confirm({
nzTitle: this.i18nSvc.fanyi('alert.center.confirm.mark-no-batch'),
nzOkText: this.i18nSvc.fanyi('common.button.ok'),
nzCancelText: this.i18nSvc.fanyi('common.button.cancel'),
nzOkDanger: true,
nzOkType: 'primary',
nzClosable: false,
nzOnOk: () => this.updateAlertsStatus(this.checkedAlertIds, 'firing')
});
}
onDeleteOneAlert(alertId: number) {
let alerts = new Set<number>();
alerts.add(alertId);
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.deleteAlerts(alerts)
});
}
onMarkReadOneAlert(alertId: number) {
let alerts = new Set<number>();
alerts.add(alertId);
this.updateAlertsStatus(alerts, 'resolved');
}
onMarkUnReadOneAlert(alertId: number) {
let alerts = new Set<number>();
alerts.add(alertId);
this.updateAlertsStatus(alerts, 'firing');
}
deleteAlerts(alertIds: Set<number>) {
this.tableLoading = true;
const deleteAlerts$ = this.alertSvc.deleteGroupAlerts(alertIds).subscribe(
message => {
deleteAlerts$.unsubscribe();
if (message.code === 0) {
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.delete-success'), '');
this.updatePageIndex(alertIds.size);
this.loadAlertsTable();
} else {
this.tableLoading = false;
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), message.msg);
}
},
error => {
this.tableLoading = false;
deleteAlerts$.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;
}
updateAlertsStatus(alertIds: Set<number>, status: string) {
this.tableLoading = true;
const markAlertsStatus$ = this.alertSvc.applyGroupAlertsStatus(alertIds, status).subscribe(
message => {
markAlertsStatus$.unsubscribe();
if (message.code === 0) {
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.mark-success'), '');
this.loadAlertsTable();
} else {
this.tableLoading = false;
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.mark-fail'), message.msg);
}
},
error => {
this.tableLoading = false;
markAlertsStatus$.unsubscribe();
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.mark-fail'), error.msg);
}
);
}
}
@@ -0,0 +1,265 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<app-help-message-show
[help_message_content]="'alert.help.group' | i18n"
[guild_link]="'alert.help.group.link' | i18n"
[module_name]="'menu.alert.group'"
[icon_name]="'filter'"
></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)="onNewGroupConverge()">
<i nz-icon nzType="appstore-add" nzTheme="outline"></i>
{{ 'common.button.new' | i18n }}
</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 nzDanger nz-button (click)="onDeleteGroupConverges()">
<i nz-icon nzType="delete" nzTheme="outline"></i>
{{ 'common.button.delete' | i18n }}
</button>
</li>
</ul>
</nz-dropdown-menu>
</ng-template>
<ng-template #right>
<app-multi-func-input
groupStyle="width: 250px;"
[placeholder]="'alert.group-converge.name' | i18n"
[(value)]="search"
(keydown.enter)="onFilterChange()"
(cleared)="onFilterChange()"
/>
<button nz-button nzType="primary" (click)="onFilterChange()" class="mobile-hide">
{{ 'common.search' | i18n }}
</button>
</ng-template>
</app-toolbar>
<nz-table
#fixedTable
[nzData]="groupConverges"
[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="14%">{{ 'alert.group-converge.name' | i18n }}</th>
<th nzAlign="center" nzWidth="12%">{{ 'alert.group-converge.group-labels' | i18n }}</th>
<th nzAlign="center" nzWidth="10%">{{ 'alert.group-converge.group-wait' | i18n }}</th>
<th nzAlign="center" nzWidth="10%">{{ 'alert.group-converge.group-interval' | i18n }}</th>
<th nzAlign="center" nzWidth="10%">{{ 'alert.group-converge.repeat-interval' | i18n }}</th>
<th nzAlign="center" nzWidth="10%" nzRight>{{ 'common.enable' | i18n }}</th>
<th nzAlign="center" nzWidth="14%">{{ 'common.edit-time' | i18n }}</th>
<th nzAlign="center" nzWidth="12%" nzRight>{{ 'common.edit' | i18n }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let data of fixedTable.data">
<td nzAlign="center" nzLeft [nzChecked]="checkedConvergeIds.has(data.id)" (nzCheckedChange)="onItemChecked(data.id, $event)"></td>
<td nzAlign="center">
<span>{{ data.name }}</span>
</td>
<td nzAlign="center">
<nz-tag *ngFor="let label of data.groupLabels" nz-tooltip [nzTooltipTitle]="label.length > 15 ? label : null">
{{ label.length > 15 ? (label | slice : 0 : 15) + '...' : label }}
</nz-tag>
</td>
<td nzAlign="center"> {{ data.groupWait }}{{ 'alert.group-converge.seconds' | i18n }} </td>
<td nzAlign="center"> {{ data.groupInterval }}{{ 'alert.group-converge.seconds' | i18n }} </td>
<td nzAlign="center"> {{ data.repeatInterval }}{{ 'alert.group-converge.seconds' | i18n }} </td>
<td nzAlign="center" nzRight>
<nz-switch [(ngModel)]="data.enable" (ngModelChange)="updateGroupConverge(data)" name="enable"></nz-switch>
</td>
<td nzAlign="center">{{ (data.gmtUpdate ? data.gmtUpdate : data.gmtCreate) | date : 'YYYY-MM-dd HH:mm:ss' }}</td>
<td nzAlign="center" nzRight>
<div class="actions">
<button
nz-button
nzType="primary"
(click)="editGroupConverge(data.id)"
nz-tooltip
[nzTooltipTitle]="'alert.group-converge.edit' | i18n"
>
<i nz-icon nzType="edit" 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
nzDanger
(click)="onDeleteOneGroupConverge(data.id)"
nz-tooltip
[nzTooltipTitle]="'alert.group-converge.delete' | i18n"
>
<i nz-icon nzType="delete" nzTheme="outline"></i>
</button>
</li>
</ul>
</nz-dropdown-menu>
</div>
</td>
</tr>
</tbody>
</nz-table>
<ng-template #rangeTemplate> {{ 'common.total' | i18n }} {{ total }} </ng-template>
<!-- new or update alert-converge pop-up box -->
<nz-modal
[(nzVisible)]="isManageModalVisible"
[nzTitle]="isManageModalAdd ? ('alert.group-converge.new' | i18n) : ('alert.group-converge.edit' | i18n)"
(nzOnCancel)="onManageModalCancel()"
(nzOnOk)="onManageModalOk()"
nzMaskClosable="false"
nzWidth="40%"
[nzOkLoading]="isManageModalOkLoading"
>
<div *nzModalContent class="-inner-content">
<form nz-form #ruleForm="ngForm">
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="rule_name" nzRequired="true" [nzTooltipTitle]="'alert.group-converge.name.tip' | i18n">
{{ 'alert.group-converge.name' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | i18n">
<input [(ngModel)]="groupConverge.name" nz-input required name="rule_name" type="text" id="rule_name" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="groupLabels" nzRequired="true" [nzTooltipTitle]="'alert.group-converge.group-labels.tip' | i18n">
{{ 'alert.group-converge.group-labels' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | i18n">
<nz-select
[(ngModel)]="groupConverge.groupLabels"
name="groupLabels"
nzMode="tags"
[nzTokenSeparators]="[',']"
[nzMaxMultipleCount]="10"
nzAllowClear
required
nzPlaceHolder="{{ 'alert.inhibit.equal_labels.placeholder' | i18n }}"
>
<nz-option *ngFor="let label of commonLabels" [nzLabel]="label" [nzValue]="label"></nz-option>
</nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="groupWait" nzRequired="true" [nzTooltipTitle]="'alert.group-converge.group-wait.tip' | i18n">
{{ 'alert.group-converge.group-wait' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12">
<nz-input-number
[(ngModel)]="groupConverge.groupWait"
[nzMin]="0"
[nzStep]="30"
[nzPlaceHolder]="'30'"
name="groupWait"
id="groupWait"
required
>
</nz-input-number>
<span class="ant-form-text">{{ 'common.time.unit.second' | i18n }}</span>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label
[nzSpan]="7"
nzFor="groupInterval"
nzRequired="true"
[nzTooltipTitle]="'alert.group-converge.group-interval.tip' | i18n"
>
{{ 'alert.group-converge.group-interval' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12">
<nz-input-number
[(ngModel)]="groupConverge.groupInterval"
[nzMin]="0"
[nzStep]="300"
[nzPlaceHolder]="'300'"
name="groupInterval"
id="groupInterval"
required
>
</nz-input-number>
<span class="ant-form-text">{{ 'alert.group-converge.seconds' | i18n }}</span>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label
[nzSpan]="7"
nzFor="reapInterval"
nzRequired="true"
[nzTooltipTitle]="'alert.group-converge.repeat-interval.tip' | i18n"
>
{{ 'alert.group-converge.repeat-interval' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12">
<nz-input-number
[(ngModel)]="groupConverge.repeatInterval"
[nzMin]="0"
[nzStep]="3600"
[nzPlaceHolder]="'14400'"
name="reapInterval"
id="reapInterval"
required
>
</nz-input-number>
<span class="ant-form-text">{{ 'alert.group-converge.seconds' | i18n }}</span>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label nzSpan="7" nzRequired="true" nzFor="enable">{{ 'common.enable' | i18n }}</nz-form-label>
<nz-form-control nzSpan="12">
<nz-switch [(ngModel)]="groupConverge.enable" name="enable" id="enable"></nz-switch>
</nz-form-control>
</nz-form-item>
</form>
</div>
</nz-modal>
@@ -0,0 +1,62 @@
.labels-container {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
.label-item {
display: flex;
align-items: center;
gap: 8px;
input {
flex: 1;
}
.button-group {
display: flex;
align-items: center;
gap: 4px;
.action-btn {
padding: 4px 8px;
height: 32px;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
i {
font-size: 16px;
}
}
.add-btn {
color: #1890ff;
}
}
}
}
// 让所有输入框宽度一致
:host ::ng-deep {
.ant-form-item-control-input {
width: 100%;
}
.ant-input-number {
width: 100%;
}
// 优化输入框样式
.ant-input {
border-radius: 4px;
}
// 优化按钮hover效果
.action-btn {
&:hover {
background: rgba(0, 0, 0, 0.018);
}
}
}
@@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AlertGroupConvergeComponent } from './alert-group-converge.component';
describe('AlertConvergeComponent', () => {
let component: AlertGroupConvergeComponent;
let fixture: ComponentFixture<AlertGroupConvergeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AlertGroupConvergeComponent]
}).compileComponents();
fixture = TestBed.createComponent(AlertGroupConvergeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,314 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzNotificationService } from 'ng-zorro-antd/notification';
import { NzTableQueryParams } from 'ng-zorro-antd/table';
import { finalize } from 'rxjs/operators';
import { AlertGroupConverge } from '../../../pojo/AlertGroupConverge';
import { AlertGroupService } from '../../../service/alert-group.service';
@Component({
selector: 'app-alert-converge',
templateUrl: './alert-group-converge.component.html',
styleUrls: ['./alert-group-converge.component.less']
})
export class AlertGroupConvergeComponent implements OnInit {
constructor(
private modal: NzModalService,
private notifySvc: NzNotificationService,
private alertConvergeService: AlertGroupService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService
) {}
@ViewChild('ruleForm', { static: false }) ruleForm: NgForm | undefined;
pageIndex: number = 1;
pageSize: number = 8;
total: number = 0;
search!: string;
groupConverges!: AlertGroupConverge[];
tableLoading: boolean = true;
checkedConvergeIds = new Set<number>();
commonLabels: string[] = ['alertname', 'instance', 'job', 'severity', 'service', 'host', 'env'];
ngOnInit(): void {
this.loadGroupConvergeTable();
}
sync() {
this.loadGroupConvergeTable();
}
loadGroupConvergeTable() {
this.tableLoading = true;
let alertDefineInit$ = this.alertConvergeService.getAlertGroupConverges(this.search, this.pageIndex - 1, this.pageSize).subscribe(
message => {
this.tableLoading = false;
this.checkedAll = false;
this.checkedConvergeIds.clear();
if (message.code === 0) {
let page = message.data;
this.groupConverges = page.content;
this.pageIndex = page.number + 1;
this.total = page.totalElements;
} else {
console.warn(message.msg);
}
alertDefineInit$.unsubscribe();
},
error => {
this.tableLoading = false;
alertDefineInit$.unsubscribe();
}
);
}
updateGroupConverge(alertConverge: AlertGroupConverge) {
this.tableLoading = true;
const updateDefine$ = this.alertConvergeService
.editAlertGroupConverge(alertConverge)
.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.loadGroupConvergeTable();
this.tableLoading = false;
},
error => {
this.tableLoading = false;
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), error.msg);
}
);
}
onDeleteGroupConverges() {
if (this.checkedConvergeIds == null || this.checkedConvergeIds.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.deleteGroupConverges(this.checkedConvergeIds)
});
}
onDeleteOneGroupConverge(id: number) {
let ids = new Set<number>();
ids.add(id);
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.deleteGroupConverges(ids)
});
}
deleteGroupConverges(convergeIds: Set<number>) {
if (convergeIds == null || convergeIds.size == 0) {
this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), '');
return;
}
this.tableLoading = true;
const deleteDefines$ = this.alertConvergeService.deleteAlertGroupConverges(convergeIds).subscribe(
message => {
deleteDefines$.unsubscribe();
if (message.code === 0) {
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.delete-success'), '');
this.updatePageIndex(convergeIds.size);
this.loadGroupConvergeTable();
} 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;
}
// begin: List multiple choice paging
checkedAll: boolean = false;
onAllChecked(checked: boolean) {
if (checked) {
this.groupConverges.forEach(item => this.checkedConvergeIds.add(item.id));
} else {
this.checkedConvergeIds.clear();
}
}
onItemChecked(id: number, checked: boolean) {
if (checked) {
this.checkedConvergeIds.add(id);
} else {
this.checkedConvergeIds.delete(id);
}
}
/**
* Paging callback
*
* @param params page info
*/
onTablePageChange(params: NzTableQueryParams) {
const { pageSize, pageIndex, sort, filter } = params;
this.pageIndex = pageIndex;
this.pageSize = pageSize;
this.loadGroupConvergeTable();
}
// end: List multiple choice paging
// start -- new or update alert-converge model
isManageModalVisible = false;
isManageModalOkLoading = false;
isManageModalAdd = true;
groupConverge: AlertGroupConverge = new AlertGroupConverge();
onNewGroupConverge() {
this.groupConverge = new AlertGroupConverge();
this.groupConverge.groupLabels = [];
this.isManageModalAdd = true;
this.isManageModalVisible = true;
this.isManageModalOkLoading = false;
}
onManageModalCancel() {
this.isManageModalVisible = false;
}
editGroupConverge(convergeId: number) {
this.isManageModalAdd = false;
this.isManageModalOkLoading = false;
const getConverge$ = this.alertConvergeService
.getAlertGroupConverge(convergeId)
.pipe(
finalize(() => {
getConverge$.unsubscribe();
})
)
.subscribe(
message => {
if (message.code === 0) {
this.groupConverge = message.data;
if (!Array.isArray(this.groupConverge.groupLabels) || this.groupConverge.groupLabels.length === 0) {
this.groupConverge.groupLabels = [];
}
this.isManageModalVisible = true;
} 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);
}
);
}
onManageModalOk() {
// Validate required form fields
if (this.ruleForm?.invalid) {
Object.values(this.ruleForm.controls).forEach(control => {
if (control.invalid) {
control.markAsDirty();
control.updateValueAndValidity({ onlySelf: true });
}
});
return;
}
this.isManageModalOkLoading = true;
if (this.isManageModalAdd) {
const modalOk$ = this.alertConvergeService
.newAlertGroupConverge(this.groupConverge)
.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.loadGroupConvergeTable();
} 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.alertConvergeService
.editAlertGroupConverge(this.groupConverge)
.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.loadGroupConvergeTable();
} 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);
}
);
}
}
onFilterChange(): void {
this.pageIndex = 1;
this.loadGroupConvergeTable();
}
}
@@ -0,0 +1,220 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<app-help-message-show
[help_message_content]="'alert.help.inhibit' | i18n"
[guild_link]="'alert.help.inhibit.link' | i18n"
[module_name]="'menu.alert.inhibit'"
[icon_name]="'delete-column'"
></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)="onNewInhibit()">
<i nz-icon nzType="appstore-add" nzTheme="outline"></i>
{{ 'common.button.new' | i18n }}
</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 nzDanger nz-button (click)="onDeleteInhibits()">
<i nz-icon nzType="delete" nzTheme="outline"></i>
{{ 'common.button.delete' | i18n }}
</button>
</li>
</ul>
</nz-dropdown-menu>
</ng-template>
<ng-template #right>
<app-multi-func-input
groupStyle="width: 250px;"
[placeholder]="'alert.inhibit.name' | i18n"
[(value)]="search"
(keydown.enter)="onFilterChange()"
(cleared)="onFilterChange()"
/>
<button nz-button nzType="primary" (click)="onFilterChange()" class="mobile-hide">
{{ 'common.search' | i18n }}
</button>
</ng-template>
</app-toolbar>
<nz-table
#fixedTable
[nzData]="inhibits"
[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="15%">{{ 'alert.inhibit.name' | i18n }}</th>
<th nzAlign="center" nzWidth="18%">{{ 'alert.inhibit.source_labels' | i18n }}</th>
<th nzAlign="center" nzWidth="18%">{{ 'alert.inhibit.target_labels' | i18n }}</th>
<th nzAlign="center" nzWidth="15%">{{ 'alert.inhibit.equal_labels' | i18n }}</th>
<th nzAlign="center" nzWidth="12%" nzRight>{{ 'common.enable' | i18n }}</th>
<th nzAlign="center" nzWidth="14%">{{ 'common.edit-time' | i18n }}</th>
<th nzAlign="center" nzWidth="12%" nzRight>{{ 'common.edit' | i18n }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let data of fixedTable.data">
<td nzAlign="center" nzLeft [nzChecked]="checkedConvergeIds.has(data.id)" (nzCheckedChange)="onItemChecked(data.id, $event)"></td>
<td nzAlign="center">{{ data.name }}</td>
<td nzAlign="center">
<nz-tag *ngFor="let item of data.sourceLabels | keyvalue">{{ item.key }}:{{ item.value }}</nz-tag>
</td>
<td nzAlign="center">
<nz-tag *ngFor="let item of data.targetLabels | keyvalue">{{ item.key }}:{{ item.value }}</nz-tag>
</td>
<td nzAlign="center">
<nz-tag *ngFor="let label of data.equalLabels">{{ label }}</nz-tag>
</td>
<td nzAlign="center" nzRight>
<nz-switch [(ngModel)]="data.enable" (ngModelChange)="updateInhibit(data)" name="enable"></nz-switch>
</td>
<td nzAlign="center">{{ (data.gmtUpdate ? data.gmtUpdate : data.gmtCreate) | date : 'YYYY-MM-dd HH:mm:ss' }}</td>
<td nzAlign="center" nzRight>
<div class="actions">
<button nz-button nzType="primary" (click)="editInhibit(data.id)" nz-tooltip [nzTooltipTitle]="'alert.inhibit.edit' | i18n">
<i nz-icon nzType="edit" 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
nzDanger
(click)="onDeleteOneInhibit(data.id)"
nz-tooltip
[nzTooltipTitle]="'alert.inhibit.delete' | i18n"
>
<i nz-icon nzType="delete" nzTheme="outline"></i>
</button>
</li>
</ul>
</nz-dropdown-menu>
</div>
</td>
</tr>
</tbody>
</nz-table>
<ng-template #rangeTemplate> {{ 'common.total' | i18n }} {{ total }} </ng-template>
<!-- new or update alert-converge pop-up box -->
<nz-modal
[(nzVisible)]="isManageModalVisible"
[nzTitle]="isManageModalAdd ? ('alert.inhibit.new' | i18n) : ('alert.inhibit.edit' | i18n)"
(nzOnCancel)="onManageModalCancel()"
(nzOnOk)="onManageModalOk()"
nzMaskClosable="false"
nzWidth="40%"
[nzOkLoading]="isManageModalOkLoading"
>
<div *nzModalContent class="-inner-content">
<form nz-form #ruleForm="ngForm">
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="rule_name" nzRequired="true" [nzTooltipTitle]="'alert.inhibit.name.tip' | i18n">
{{ 'alert.inhibit.name' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12" [nzErrorTip]="'validation.required' | i18n">
<input [(ngModel)]="alertInhibit.name" nz-input required name="rule_name" type="text" id="rule_name" />
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="sourceLabels" nzRequired="true" [nzTooltipTitle]="'alert.inhibit.source_labels.tip' | i18n">
{{ 'alert.inhibit.source_labels' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12">
<app-form-field
[item]="{
field: 'labels',
type: 'label-selector',
required: true
}"
[name]="'sourceLabels'"
[(ngModel)]="alertInhibit.sourceLabels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzFor="targetLabels" nzRequired="true" [nzTooltipTitle]="'alert.inhibit.target_labels.tip' | i18n">
{{ 'alert.inhibit.target_labels' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12">
<app-form-field
[item]="{
field: 'labels',
type: 'label-selector',
required: true
}"
[name]="'targetLabels'"
[(ngModel)]="alertInhibit.targetLabels"
/>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzRequired="true" [nzTooltipTitle]="'alert.inhibit.equal_labels.tip' | i18n">
{{ 'alert.inhibit.equal_labels' | i18n }}
</nz-form-label>
<nz-form-control [nzSpan]="12">
<nz-select
[(ngModel)]="alertInhibit.equalLabels"
name="equalLabels"
nzMode="tags"
[nzTokenSeparators]="[',']"
[nzMaxMultipleCount]="10"
nzAllowClear
required
nzPlaceHolder="{{ 'alert.inhibit.equal_labels.placeholder' | i18n }}"
>
<nz-option *ngFor="let label of commonLabels" [nzLabel]="label" [nzValue]="label"></nz-option>
</nz-select>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-label [nzSpan]="7" nzRequired="true" nzFor="enable">{{ 'common.enable' | i18n }}</nz-form-label>
<nz-form-control [nzSpan]="12">
<nz-switch [(ngModel)]="alertInhibit.enable" name="enable" id="enable"></nz-switch>
</nz-form-control>
</nz-form-item>
</form>
</div>
</nz-modal>
@@ -0,0 +1,19 @@
.actions {
button {
margin: 0 4px;
}
}
nz-tag {
margin: 2px;
}
:host {
app-key-value-input {
width: 100%;
}
nz-select {
width: 100%;
}
}
@@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AlertInhibitComponent } from './alert-inhibit.component';
describe('AlertInhibitComponent', () => {
let component: AlertInhibitComponent;
let fixture: ComponentFixture<AlertInhibitComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AlertInhibitComponent]
}).compileComponents();
fixture = TestBed.createComponent(AlertInhibitComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,309 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN } from '@delon/theme';
import { NzModalService } from 'ng-zorro-antd/modal';
import { NzNotificationService } from 'ng-zorro-antd/notification';
import { NzTableQueryParams } from 'ng-zorro-antd/table';
import { finalize } from 'rxjs/operators';
import { AlertInhibit } from '../../../pojo/AlertInhibit';
import { AlertInhibitService } from '../../../service/alert-inhibit.service';
@Component({
selector: 'app-alert-inhibit',
templateUrl: './alert-inhibit.component.html',
styleUrl: './alert-inhibit.component.less'
})
export class AlertInhibitComponent implements OnInit {
constructor(
private modal: NzModalService,
private notifySvc: NzNotificationService,
private alertInhibitService: AlertInhibitService,
@Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService
) {}
@ViewChild('ruleForm', { static: false }) ruleForm: NgForm | undefined;
pageIndex: number = 1;
pageSize: number = 8;
total: number = 0;
search!: string;
inhibits!: AlertInhibit[];
tableLoading: boolean = true;
checkedConvergeIds = new Set<number>();
commonLabels: string[] = ['alertname', 'instance', 'job', 'severity', 'service', 'host', 'env'];
ngOnInit(): void {
this.loadInhibitTable();
}
sync() {
this.loadInhibitTable();
}
loadInhibitTable() {
this.tableLoading = true;
let alertDefineInit$ = this.alertInhibitService.getAlertInhibits(this.search, this.pageIndex - 1, this.pageSize).subscribe(
message => {
this.tableLoading = false;
this.checkedAll = false;
this.checkedConvergeIds.clear();
if (message.code === 0) {
let page = message.data;
this.inhibits = page.content;
this.pageIndex = page.number + 1;
this.total = page.totalElements;
} else {
console.warn(message.msg);
}
alertDefineInit$.unsubscribe();
},
error => {
this.tableLoading = false;
alertDefineInit$.unsubscribe();
}
);
}
updateInhibit(alertConverge: AlertInhibit) {
this.tableLoading = true;
const updateDefine$ = this.alertInhibitService
.editAlertInhibit(alertConverge)
.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.loadInhibitTable();
this.tableLoading = false;
},
error => {
this.tableLoading = false;
this.notifySvc.error(this.i18nSvc.fanyi('common.notify.edit-fail'), error.msg);
}
);
}
onDeleteInhibits() {
if (this.checkedConvergeIds == null || this.checkedConvergeIds.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.deleteInhibits(this.checkedConvergeIds)
});
}
onDeleteOneInhibit(id: number) {
let ids = new Set<number>();
ids.add(id);
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.deleteInhibits(ids)
});
}
deleteInhibits(convergeIds: Set<number>) {
if (convergeIds == null || convergeIds.size == 0) {
this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), '');
return;
}
this.tableLoading = true;
const deleteDefines$ = this.alertInhibitService.deleteAlertInhibits(convergeIds).subscribe(
message => {
deleteDefines$.unsubscribe();
if (message.code === 0) {
this.notifySvc.success(this.i18nSvc.fanyi('common.notify.delete-success'), '');
this.updatePageIndex(convergeIds.size);
this.loadInhibitTable();
} 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;
}
// begin: List multiple choice paging
checkedAll: boolean = false;
onAllChecked(checked: boolean) {
if (checked) {
this.inhibits.forEach(item => this.checkedConvergeIds.add(item.id));
} else {
this.checkedConvergeIds.clear();
}
}
onItemChecked(id: number, checked: boolean) {
if (checked) {
this.checkedConvergeIds.add(id);
} else {
this.checkedConvergeIds.delete(id);
}
}
/**
* Paging callback
*
* @param params page info
*/
onTablePageChange(params: NzTableQueryParams) {
const { pageSize, pageIndex, sort, filter } = params;
this.pageIndex = pageIndex;
this.pageSize = pageSize;
this.loadInhibitTable();
}
// end: List multiple choice paging
// start -- new or update alert-converge model
isManageModalVisible = false;
isManageModalOkLoading = false;
isManageModalAdd = true;
alertInhibit: AlertInhibit = new AlertInhibit();
onNewInhibit() {
this.alertInhibit = new AlertInhibit();
this.isManageModalAdd = true;
this.isManageModalVisible = true;
this.isManageModalOkLoading = false;
}
onManageModalCancel() {
this.isManageModalVisible = false;
}
editInhibit(convergeId: number) {
this.isManageModalAdd = false;
this.isManageModalOkLoading = false;
const getConverge$ = this.alertInhibitService
.getAlertInhibit(convergeId)
.pipe(
finalize(() => {
getConverge$.unsubscribe();
})
)
.subscribe(
message => {
if (message.code === 0) {
this.alertInhibit = message.data;
this.isManageModalVisible = true;
} 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);
}
);
}
onManageModalOk() {
// Validate required form fields
if (this.ruleForm?.invalid) {
Object.values(this.ruleForm.controls).forEach(control => {
if (control.invalid) {
control.markAsDirty();
control.updateValueAndValidity({ onlySelf: true });
}
});
return;
}
this.isManageModalOkLoading = true;
if (this.isManageModalAdd) {
const modalOk$ = this.alertInhibitService
.newAlertInhibit(this.alertInhibit)
.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.loadInhibitTable();
} 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.alertInhibitService
.editAlertInhibit(this.alertInhibit)
.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.loadInhibitTable();
} 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);
}
);
}
}
onFilterChange(): void {
this.pageIndex = 1;
this.loadInhibitTable();
}
}
@@ -0,0 +1,56 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<app-help-message-show
[help_message_content]="'alert.help.integration' | i18n"
[guild_link]="'alert.help.integration.link' | i18n"
[module_name]="'menu.alert.integration'"
[icon_name]="'api'"
></app-help-message-show>
<nz-divider></nz-divider>
<div class="alert-integration-container">
<div class="data-sources">
<h2>{{ 'alert.integration.source' | i18n }}</h2>
<div class="source-list">
<div
class="source-item"
*ngFor="let source of dataSources"
(click)="selectSource(source)"
[class.active]="selectedSource?.id === source.id"
>
<img [src]="source.icon" [alt]="source.name" />
<span>{{ source.name }}</span>
</div>
</div>
</div>
<div class="doc-content">
<ng-container *ngIf="selectedSource">
<div style="display: flex; justify-content: space-between; align-items: center">
<h2>{{ selectedSource.name }}</h2>
<button nz-button nzType="default" (click)="goToTokenManagement()" style="margin-right: 16px">
<span nz-icon nzType="setting" nzTheme="outline"></span>
{{ 'alert.integration.token.manage' | i18n }}
</button>
</div>
<markdown [data]="markdownContent"></markdown>
</ng-container>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More