chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -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
|
||||
@@ -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/**/*
|
||||
@@ -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'
|
||||
};
|
||||
@@ -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/**/*"
|
||||
]
|
||||
}
|
||||
@@ -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```
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "./node_modules/ng-alain/schema.json",
|
||||
"theme": {
|
||||
"list": [
|
||||
{
|
||||
"theme": "dark"
|
||||
},
|
||||
{
|
||||
"theme": "compact"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
Generated
+16592
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"/api/*": {
|
||||
"target": "http://localhost:1157",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"logLevel": "debug"
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 © {{ 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 © {{ 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 {}
|
||||
@@ -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 © {{ 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = {};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user