fix: merge
20
README.md
@ -1,5 +1,4 @@
|
|||||||
Apache SkyWalking Booster UI
|
# Apache SkyWalking Booster UI
|
||||||
===============
|
|
||||||
|
|
||||||
<img src="http://skywalking.apache.org/assets/logo.svg" alt="Sky Walking logo" height="90px" align="right" />
|
<img src="http://skywalking.apache.org/assets/logo.svg" alt="Sky Walking logo" height="90px" align="right" />
|
||||||
|
|
||||||
@ -10,11 +9,12 @@ Apache SkyWalking Booster UI
|
|||||||
This UI starts from SkyWalking OAP v9 core.
|
This UI starts from SkyWalking OAP v9 core.
|
||||||
|
|
||||||
## Release
|
## Release
|
||||||
|
|
||||||
This repo wouldn't release separately. All source codes have been included in the main repo release. The tags match the [main repo](https://github.com/apache/skywalking) tags.
|
This repo wouldn't release separately. All source codes have been included in the main repo release. The tags match the [main repo](https://github.com/apache/skywalking) tags.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
The app was built with [Vue3.x + Typescript](https://github.com/vuejs/vue).
|
The app was built with [Vue3.x + Typescript](https://github.com/vuejs/vue).
|
||||||
|
|
||||||
### Prepare
|
### Prepare
|
||||||
|
|
||||||
@ -28,19 +28,21 @@ npm install
|
|||||||
### Build
|
### Build
|
||||||
|
|
||||||
**All following builds are for dev.**
|
**All following builds are for dev.**
|
||||||
|
|
||||||
```
|
```
|
||||||
npm install
|
npm install
|
||||||
npm run serve
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The default UI address is `http://localhost:8080`.
|
The default UI address is `http://localhost:8080`.
|
||||||
|
|
||||||
|
|
||||||
# Contact Us
|
# Contact Us
|
||||||
* Submit an [issue](https://github.com/apache/skywalking/issues) if you face some issues. Submit a [discussion](https://github.com/apache/skywalking/discussions) if you want to propose new feature or have any question.
|
|
||||||
* Mailing list: **dev@skywalking.apache.org**. Mail to `dev-subscribe@skywalking.apache.org`, follow the reply to subscribe the mailing list.
|
- Submit an [issue](https://github.com/apache/skywalking/issues) if you face some issues. Submit a [discussion](https://github.com/apache/skywalking/discussions) if you want to propose new feature or have any question.
|
||||||
* Join Slack. Send `Request to join SkyWalking slack` mail to the mail list(`dev@skywalking.apache.org`), we will invite you in.
|
- Mailing list: **dev@skywalking.apache.org**. Mail to `dev-subscribe@skywalking.apache.org`, follow the reply to subscribe the mailing list.
|
||||||
* QQ Group: 392443393, 901167865
|
- Join Slack. Send `Request to join SkyWalking slack` mail to the mail list(`dev@skywalking.apache.org`), we will invite you in.
|
||||||
|
- QQ Group: 392443393, 901167865
|
||||||
|
|
||||||
# License
|
# License
|
||||||
|
|
||||||
[Apache 2.0 License.](/LICENSE)
|
[Apache 2.0 License.](/LICENSE)
|
||||||
|
86
package-lock.json
generated
@ -49,6 +49,7 @@
|
|||||||
"husky": "^8.0.2",
|
"husky": "^8.0.2",
|
||||||
"jsdom": "^20.0.3",
|
"jsdom": "^20.0.3",
|
||||||
"lint-staged": "^12.1.3",
|
"lint-staged": "^12.1.3",
|
||||||
|
"mockjs": "^1.1.0",
|
||||||
"node-sass": "^8.0.0",
|
"node-sass": "^8.0.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss-html": "^1.3.0",
|
"postcss-html": "^1.3.0",
|
||||||
@ -2147,9 +2148,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sideway/formula": {
|
"node_modules/@sideway/formula": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
||||||
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==",
|
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@sideway/pinpoint": {
|
"node_modules/@sideway/pinpoint": {
|
||||||
@ -8113,9 +8114,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/http-cache-semantics": {
|
"node_modules/http-cache-semantics": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||||
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
|
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/http-proxy-agent": {
|
"node_modules/http-proxy-agent": {
|
||||||
@ -8970,9 +8971,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
@ -9424,9 +9425,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/loader-utils/node_modules/json5": {
|
"node_modules/loader-utils/node_modules/json5": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimist": "^1.2.0"
|
"minimist": "^1.2.0"
|
||||||
@ -10168,6 +10169,18 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mockjs": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mockjs/-/mockjs-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "*"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"random": "bin/random"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/moment": {
|
"node_modules/moment": {
|
||||||
"version": "2.29.4",
|
"version": "2.29.4",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||||
@ -13626,7 +13639,7 @@
|
|||||||
"loader-utils": "^1.1.0",
|
"loader-utils": "^1.1.0",
|
||||||
"merge-options": "1.0.1",
|
"merge-options": "1.0.1",
|
||||||
"micromatch": "3.1.0",
|
"micromatch": "3.1.0",
|
||||||
"postcss": "^5.2.17",
|
"postcss": "^7.0.36",
|
||||||
"postcss-prefix-selector": "^1.6.0",
|
"postcss-prefix-selector": "^1.6.0",
|
||||||
"posthtml-rename-id": "^1.0",
|
"posthtml-rename-id": "^1.0",
|
||||||
"posthtml-svg-mode": "^1.0.3",
|
"posthtml-svg-mode": "^1.0.3",
|
||||||
@ -13789,9 +13802,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svg-baker/node_modules/postcss": {
|
"node_modules/svg-baker/node_modules/postcss": {
|
||||||
"version": "5.2.18",
|
"version": "7.0.36",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz",
|
||||||
"integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
|
"integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^1.1.3",
|
"chalk": "^1.1.3",
|
||||||
@ -17604,9 +17617,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@sideway/formula": {
|
"@sideway/formula": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
||||||
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==",
|
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@sideway/pinpoint": {
|
"@sideway/pinpoint": {
|
||||||
@ -22042,9 +22055,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"http-cache-semantics": {
|
"http-cache-semantics": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||||
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
|
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"http-proxy-agent": {
|
"http-proxy-agent": {
|
||||||
@ -22665,9 +22678,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"jsonfile": {
|
"jsonfile": {
|
||||||
@ -22989,9 +23002,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "^1.2.0"
|
"minimist": "^1.2.0"
|
||||||
@ -23559,6 +23572,15 @@
|
|||||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"mockjs": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mockjs/-/mockjs-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"commander": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"moment": {
|
"moment": {
|
||||||
"version": "2.29.4",
|
"version": "2.29.4",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||||
@ -26176,7 +26198,7 @@
|
|||||||
"loader-utils": "^1.1.0",
|
"loader-utils": "^1.1.0",
|
||||||
"merge-options": "1.0.1",
|
"merge-options": "1.0.1",
|
||||||
"micromatch": "3.1.0",
|
"micromatch": "3.1.0",
|
||||||
"postcss": "^5.2.17",
|
"postcss": "^7.0.36",
|
||||||
"postcss-prefix-selector": "^1.6.0",
|
"postcss-prefix-selector": "^1.6.0",
|
||||||
"posthtml-rename-id": "^1.0",
|
"posthtml-rename-id": "^1.0",
|
||||||
"posthtml-svg-mode": "^1.0.3",
|
"posthtml-svg-mode": "^1.0.3",
|
||||||
@ -26307,9 +26329,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"version": "5.2.18",
|
"version": "7.0.36",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz",
|
||||||
"integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==",
|
"integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"chalk": "^1.1.3",
|
"chalk": "^1.1.3",
|
||||||
|
11
package.json
@ -58,6 +58,7 @@
|
|||||||
"husky": "^8.0.2",
|
"husky": "^8.0.2",
|
||||||
"jsdom": "^20.0.3",
|
"jsdom": "^20.0.3",
|
||||||
"lint-staged": "^12.1.3",
|
"lint-staged": "^12.1.3",
|
||||||
|
"mockjs": "^1.1.0",
|
||||||
"node-sass": "^8.0.0",
|
"node-sass": "^8.0.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss-html": "^1.3.0",
|
"postcss-html": "^1.3.0",
|
||||||
@ -85,10 +86,14 @@
|
|||||||
"not dead"
|
"not dead"
|
||||||
],
|
],
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,jsx,ts,tsx,vue,scss,less}": [
|
"*.{js,jsx,ts,tsx,vue}": [
|
||||||
"eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
"eslint . --ext .vue,.js,.jsx,.ts,.tsx --fix --ignore-path .gitignore",
|
||||||
"prettier --write \"src/**/*.{js,tsx,css,less,scss,vue,html,md}\"",
|
"prettier --write \"src/**/*.{js,tsx,css,less,scss,vue,html,md}\"",
|
||||||
"stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/"
|
"stylelint --cache --fix \"**/*.{vue}\" --cache --cache-location node_modules/.cache/stylelint/"
|
||||||
|
],
|
||||||
|
"*.{scss,less}": [
|
||||||
|
"prettier --write \"src/**/*.{js,tsx,css,less,scss,vue,html,md}\"",
|
||||||
|
"stylelint --cache --fix \"**/*.{less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/"
|
||||||
],
|
],
|
||||||
"{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [
|
"{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": [
|
||||||
"prettier --write"
|
"prettier --write"
|
||||||
|
13
src/App.vue
@ -15,11 +15,22 @@ limitations under the License. -->
|
|||||||
<template>
|
<template>
|
||||||
<router-view />
|
<router-view />
|
||||||
</template>
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (route.name === "ViewWidget") {
|
||||||
|
(document.querySelector("#app") as any).style.minWidth = "120px";
|
||||||
|
} else {
|
||||||
|
(document.querySelector("#app") as any).style.minWidth = "1024px";
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
</script>
|
||||||
<style>
|
<style>
|
||||||
#app {
|
#app {
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 1024px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
15
src/assets/icons/add_iframe.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!-- 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. -->
|
||||||
|
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M856.32 428.064a32 32 0 0 0-32 32v163.328H372.48c-0.896 0-1.664 0.448-2.56 0.512v-177.696h244.48a32 32 0 1 0 0-64H130.56c-0.896 0-1.664 0.448-2.56 0.512V231.68h488.16a32 32 0 1 0 0-64H96a32 32 0 0 0-32 32v701.824a32 32 0 0 0 32 32h760.32a32 32 0 0 0 32-32V460.064a32 32 0 0 0-32-32zM128 445.728c0.896 0.064 1.664 0.512 2.56 0.512h175.36v423.264H128V445.728z m241.92 423.776v-182.624c0.896 0.064 1.664 0.512 2.56 0.512h451.84v182.08h-454.4zM960 174.656h-61.376V113.28a32 32 0 1 0-64 0v61.344H752.64a32 32 0 1 0 0 64h81.984v81.984a32 32 0 1 0 64 0V238.656H960a32 32 0 1 0 0-64z" fill="#2c2c2c"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
15
src/assets/icons/cross.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!-- 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. -->
|
||||||
|
<svg t="1680101648371" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="15649" width="48" height="48"><path d="M832 272c0-62.4-51-112.9-113.6-112-60.7 0.9-110 50.6-110.4 111.3-0.3 52.6 35.6 96.8 84.2 109.2 14 3.6 23.8 16 24.1 30.4 0.5 27.3-4.4 57.4-22.3 82.5-28.7 40.3-80.7 54.9-126.6 67.8-29 8.1-50.1 10.2-68.7 12-26.4 2.6-51.4 5.1-82.6 23-6.6 3.8-13.1 8-19.2 12.6-5.3 4-12.8 0.2-12.8-6.4V241.3c0-12.2 6.8-23.5 17.7-28.9 37.1-18.4 62.6-56.8 62.3-101.1-0.5-62.8-53.2-113.4-116-111.2C288.1 2.1 240 51.4 240 112c0 44 25.4 82.1 62.3 100.4 10.9 5.4 17.7 16.5 17.7 28.6v541.7c0 12.2-6.8 23.5-17.7 28.9-37.1 18.4-62.6 56.8-62.3 101.1 0.4 62.8 53.1 113.3 115.9 111.2C416 1021.9 464 972.5 464 912c0-44-25.4-82.1-62.3-100.4-10.9-5.4-17.7-16.5-17.7-28.6v-19.2c0-42 19.9-81.8 54.3-105.9 3.1-2.2 6.4-4.3 9.7-6.2 19.3-11.1 33.5-12.5 57-14.8 20.2-2 45.3-4.5 79.7-14.1 50.5-14.2 119.6-33.5 161.4-92.3 24-33.7 35.4-75 34.1-123-0.2-6.9-0.7-13.8-1.4-20.9-1.1-10.7 3.5-21 11.8-27.8 25.3-20.4 41.4-51.7 41.4-86.8zM304 112c0-26.5 21.5-48 48-48s48 21.5 48 48-21.5 48-48 48-48-21.5-48-48z m96 800c0 26.5-21.5 48-48 48s-48-21.5-48-48 21.5-48 48-48 48 21.5 48 48z m320-592c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48z" p-id="15650"></path></svg>
|
After Width: | Height: | Size: 2.0 KiB |
15
src/assets/icons/entry.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!-- 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. -->
|
||||||
|
<svg t="1680083488716" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1096" width="48" height="48"><path d="M853.333333 512a42.666667 42.666667 0 0 0-42.666666-42.666667h-323.84l98.133333-97.706666a42.666667 42.666667 0 1 0-60.586667-60.586667l-170.666666 170.666667a42.666667 42.666667 0 0 0-8.96 14.08 42.666667 42.666667 0 0 0 0 32.426666 42.666667 42.666667 0 0 0 8.96 14.08l170.666666 170.666667a42.666667 42.666667 0 0 0 60.586667 0 42.666667 42.666667 0 0 0 0-60.586667L486.826667 554.666667H810.666667a42.666667 42.666667 0 0 0 42.666666-42.666667zM725.333333 85.333333H298.666667a128 128 0 0 0-128 128v597.333334a128 128 0 0 0 128 128h426.666666a128 128 0 0 0 128-128v-128a42.666667 42.666667 0 0 0-85.333333 0v128a42.666667 42.666667 0 0 1-42.666667 42.666666H298.666667a42.666667 42.666667 0 0 1-42.666667-42.666666V213.333333a42.666667 42.666667 0 0 1 42.666667-42.666666h426.666666a42.666667 42.666667 0 0 1 42.666667 42.666666v128a42.666667 42.666667 0 0 0 85.333333 0V213.333333a128 128 0 0 0-128-128z" p-id="1097"></path></svg>
|
After Width: | Height: | Size: 1.8 KiB |
15
src/assets/icons/exit.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!-- 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. -->
|
||||||
|
<svg t="1680104481890" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7991" width="16" height="16"><path d="M918.4 489.6l-160-160c-12.8-12.8-32-12.8-44.8 0-12.8 12.8-12.8 32 0 44.8l105.6 105.6L512 480c-19.2 0-32 12.8-32 32s12.8 32 32 32l307.2 0-105.6 105.6c-12.8 12.8-12.8 32 0 44.8 6.4 6.4 12.8 9.6 22.4 9.6 9.6 0 16-3.2 22.4-9.6l160-163.2c0 0 0-3.2 3.2-3.2C931.2 518.4 931.2 499.2 918.4 489.6zM832 736c-19.2 0-32 12.8-32 32l0 64c0 19.2-12.8 32-32 32L224 864c-19.2 0-32-12.8-32-32L192 192c0-19.2 12.8-32 32-32l544 0c19.2 0 32 12.8 32 32l0 64c0 19.2 12.8 32 32 32s32-12.8 32-32L864 192c0-54.4-41.6-96-96-96L224 96C169.6 96 128 137.6 128 192l0 640c0 54.4 41.6 96 96 96l544 0c54.4 0 96-41.6 96-96l0-64C864 748.8 851.2 736 832 736z" p-id="7992"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/img/technologies/FASTAPI.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/img/technologies/HTTPX.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
src/assets/img/technologies/WEBSOCKETS.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/img/tools/ENTRY.png
Normal file
After Width: | Height: | Size: 262 B |
BIN
src/assets/img/tools/EXIT.png
Normal file
After Width: | Height: | Size: 269 B |
BIN
src/assets/img/tools/STREAM.png
Normal file
After Width: | Height: | Size: 373 B |
@ -15,7 +15,15 @@ limitations under the License. -->
|
|||||||
<template>
|
<template>
|
||||||
<div class="chart" ref="chartRef" :style="`height:${height};width:${width};`">
|
<div class="chart" ref="chartRef" :style="`height:${height};width:${width};`">
|
||||||
<div v-if="!available" class="no-data">No Data</div>
|
<div v-if="!available" class="no-data">No Data</div>
|
||||||
<div class="menus" v-show="visMenus" ref="menus">
|
<div
|
||||||
|
class="menus"
|
||||||
|
v-show="visMenus"
|
||||||
|
:style="{
|
||||||
|
top: menuPos.y + 'px',
|
||||||
|
left: menuPos.x + 'px',
|
||||||
|
}"
|
||||||
|
@mouseenter="hideTooltips"
|
||||||
|
>
|
||||||
<div class="tools" @click="associateMetrics" v-if="associate.length">
|
<div class="tools" @click="associateMetrics" v-if="associate.length">
|
||||||
{{ t("associateMetrics") }}
|
{{ t("associateMetrics") }}
|
||||||
</div>
|
</div>
|
||||||
@ -36,7 +44,7 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { watch, ref, onMounted, onBeforeUnmount, unref, computed } from "vue";
|
import { watch, ref, onMounted, onBeforeUnmount, unref, computed, reactive } from "vue";
|
||||||
import type { PropType, Ref } from "vue";
|
import type { PropType, Ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import type { EventParams } from "@/types/app";
|
import type { EventParams } from "@/types/app";
|
||||||
@ -50,7 +58,6 @@ limitations under the License. -->
|
|||||||
const emits = defineEmits(["select"]);
|
const emits = defineEmits(["select"]);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const chartRef = ref<Nullable<HTMLDivElement>>(null);
|
const chartRef = ref<Nullable<HTMLDivElement>>(null);
|
||||||
const menus = ref<Nullable<HTMLDivElement>>(null);
|
|
||||||
const visMenus = ref<boolean>(false);
|
const visMenus = ref<boolean>(false);
|
||||||
const { setOptions, resize, getInstance } = useECharts(chartRef as Ref<HTMLDivElement>);
|
const { setOptions, resize, getInstance } = useECharts(chartRef as Ref<HTMLDivElement>);
|
||||||
const currentParams = ref<Nullable<EventParams>>(null);
|
const currentParams = ref<Nullable<EventParams>>(null);
|
||||||
@ -58,6 +65,7 @@ limitations under the License. -->
|
|||||||
const traceOptions = ref<{ type: string; filters?: unknown }>({
|
const traceOptions = ref<{ type: string; filters?: unknown }>({
|
||||||
type: "Trace",
|
type: "Trace",
|
||||||
});
|
});
|
||||||
|
const menuPos = reactive<{ x: number; y: number }>({ x: NaN, y: NaN });
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
height: { type: String, default: "100%" },
|
height: { type: String, default: "100%" },
|
||||||
width: { type: String, default: "100%" },
|
width: { type: String, default: "100%" },
|
||||||
@ -100,26 +108,37 @@ limitations under the License. -->
|
|||||||
emits("select", currentParams.value);
|
emits("select", currentParams.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!menus.value || !chartRef.value) {
|
instance.dispatchAction({
|
||||||
|
type: "hideTip",
|
||||||
|
});
|
||||||
|
visMenus.value = true;
|
||||||
|
if (!chartRef.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
visMenus.value = true;
|
|
||||||
const w = chartRef.value.getBoundingClientRect().width || 0;
|
const w = chartRef.value.getBoundingClientRect().width || 0;
|
||||||
const h = chartRef.value.getBoundingClientRect().height || 0;
|
const h = chartRef.value.getBoundingClientRect().height || 0;
|
||||||
if (w - params.event.offsetX > 120) {
|
if (w - params.event.offsetX > 120) {
|
||||||
menus.value.style.left = params.event.offsetX + "px";
|
menuPos.x = params.event.offsetX;
|
||||||
} else {
|
} else {
|
||||||
menus.value.style.left = params.event.offsetX - 120 + "px";
|
menuPos.x = params.event.offsetX - 120;
|
||||||
}
|
}
|
||||||
if (h - params.event.offsetY < 50) {
|
if (h - params.event.offsetY < 50) {
|
||||||
menus.value.style.top = params.event.offsetY - 40 + "px";
|
menuPos.y = params.event.offsetY - 40;
|
||||||
} else {
|
} else {
|
||||||
menus.value.style.top = params.event.offsetY + 2 + "px";
|
menuPos.y = params.event.offsetY;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (props.option.series.type === "sankey") {
|
if (props.option.series.type === "sankey") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
instance.on("mouseover", () => {
|
||||||
|
visMenus.value = false;
|
||||||
|
});
|
||||||
|
instance.on("mouseout", () => {
|
||||||
|
instance.dispatchAction({
|
||||||
|
type: "hideTip",
|
||||||
|
});
|
||||||
|
});
|
||||||
document.addEventListener(
|
document.addEventListener(
|
||||||
"click",
|
"click",
|
||||||
() => {
|
() => {
|
||||||
@ -127,6 +146,9 @@ limitations under the License. -->
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
visMenus.value = false;
|
visMenus.value = false;
|
||||||
|
instance.dispatchAction({
|
||||||
|
type: "hideTip",
|
||||||
|
});
|
||||||
instance.dispatchAction({
|
instance.dispatchAction({
|
||||||
type: "updateAxisPointer",
|
type: "updateAxisPointer",
|
||||||
currTrigger: "leave",
|
currTrigger: "leave",
|
||||||
@ -139,14 +161,10 @@ limitations under the License. -->
|
|||||||
|
|
||||||
function associateMetrics() {
|
function associateMetrics() {
|
||||||
emits("select", currentParams.value);
|
emits("select", currentParams.value);
|
||||||
const { dataIndex, seriesIndex } = currentParams.value || {
|
updateOptions(currentParams.value || undefined);
|
||||||
dataIndex: 0,
|
|
||||||
seriesIndex: 0,
|
|
||||||
};
|
|
||||||
updateOptions({ dataIndex, seriesIndex });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOptions(params?: { dataIndex: number; seriesIndex: number }) {
|
function updateOptions(params?: EventParams) {
|
||||||
const instance = getInstance();
|
const instance = getInstance();
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
return;
|
return;
|
||||||
@ -160,16 +178,10 @@ limitations under the License. -->
|
|||||||
setOptions(options || props.option);
|
setOptions(options || props.option);
|
||||||
} else {
|
} else {
|
||||||
instance.dispatchAction({
|
instance.dispatchAction({
|
||||||
type: "updateAxisPointer",
|
type: "showTip",
|
||||||
dataIndex: params ? params.dataIndex : props.filters.dataIndex,
|
dataIndex: params ? params.dataIndex : props.filters.dataIndex,
|
||||||
seriesIndex: params ? params.seriesIndex : 0,
|
seriesIndex: params ? params.seriesIndex : 0,
|
||||||
});
|
});
|
||||||
const ids = props.option.series.map((_: unknown, index: number) => index);
|
|
||||||
instance.dispatchAction({
|
|
||||||
type: "highlight",
|
|
||||||
dataIndex: params ? params.dataIndex : props.filters.dataIndex,
|
|
||||||
seriesIndex: ids,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,6 +195,13 @@ limitations under the License. -->
|
|||||||
visMenus.value = true;
|
visMenus.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hideTooltips() {
|
||||||
|
const instance = getInstance();
|
||||||
|
instance.dispatchAction({
|
||||||
|
type: "hideTip",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.option,
|
() => props.option,
|
||||||
(newVal, oldVal) => {
|
(newVal, oldVal) => {
|
||||||
|
@ -15,37 +15,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const ProfileSegment = {
|
|
||||||
variable: "$segmentId: String",
|
|
||||||
query: `
|
|
||||||
segment: getProfiledSegment(segmentId: $segmentId) {
|
|
||||||
spans {
|
|
||||||
spanId
|
|
||||||
parentSpanId
|
|
||||||
serviceCode
|
|
||||||
startTime
|
|
||||||
endTime
|
|
||||||
endpointName
|
|
||||||
type
|
|
||||||
peer
|
|
||||||
component
|
|
||||||
isError
|
|
||||||
layer
|
|
||||||
tags {
|
|
||||||
key value
|
|
||||||
}
|
|
||||||
logs {
|
|
||||||
time
|
|
||||||
data {
|
|
||||||
key
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CreateProfileTask = {
|
export const CreateProfileTask = {
|
||||||
variable: "$creationRequest: ProfileTaskCreationRequest",
|
variable: "$creationRequest: ProfileTaskCreationRequest",
|
||||||
query: `
|
query: `
|
||||||
@ -79,23 +48,55 @@ export const GetProfileTaskList = {
|
|||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
export const GetProfileTaskSegmentList = {
|
export const GetProfileTaskSegmentList = {
|
||||||
variable: "$taskID: String",
|
variable: "$taskID: ID!",
|
||||||
query: `
|
query: `
|
||||||
segmentList: getProfileTaskSegmentList(taskID: $taskID) {
|
segmentList: getProfileTaskSegments(taskID: $taskID) {
|
||||||
segmentId
|
traceId
|
||||||
|
instanceId
|
||||||
|
instanceName
|
||||||
endpointNames
|
endpointNames
|
||||||
start
|
|
||||||
duration
|
duration
|
||||||
traceIds
|
start
|
||||||
isError
|
spans {
|
||||||
|
spanId
|
||||||
|
parentSpanId
|
||||||
|
segmentId
|
||||||
|
refs {
|
||||||
|
traceId
|
||||||
|
parentSegmentId
|
||||||
|
parentSpanId
|
||||||
|
type
|
||||||
|
}
|
||||||
|
serviceCode
|
||||||
|
serviceInstanceName
|
||||||
|
startTime
|
||||||
|
endTime
|
||||||
|
endpointName
|
||||||
|
type
|
||||||
|
peer
|
||||||
|
component
|
||||||
|
isError
|
||||||
|
layer
|
||||||
|
tags {
|
||||||
|
key value
|
||||||
|
}
|
||||||
|
logs {
|
||||||
|
time
|
||||||
|
data {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profiled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GetProfileAnalyze = {
|
export const GetProfileAnalyze = {
|
||||||
variable: "$segmentId: String!, $timeRanges: [ProfileAnalyzeTimeRange!]!",
|
variable: "$queries: [SegmentProfileAnalyzeQuery!]!",
|
||||||
query: `
|
query: `
|
||||||
analyze: getProfileAnalyze(segmentId: $segmentId, timeRanges: $timeRanges) {
|
analyze: getSegmentsProfileAnalyze(queries: $queries) {
|
||||||
tip
|
tip
|
||||||
trees {
|
trees {
|
||||||
elements {
|
elements {
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ProfileSegment,
|
|
||||||
CreateProfileTask,
|
CreateProfileTask,
|
||||||
GetProfileTaskList,
|
GetProfileTaskList,
|
||||||
GetProfileTaskSegmentList,
|
GetProfileTaskSegmentList,
|
||||||
@ -24,8 +23,6 @@ import {
|
|||||||
GetProfileTaskLogs,
|
GetProfileTaskLogs,
|
||||||
} from "../fragments/profile";
|
} from "../fragments/profile";
|
||||||
|
|
||||||
export const queryProfileSegment = `query queryProfileSegment(${ProfileSegment.variable}) {${ProfileSegment.query}}`;
|
|
||||||
|
|
||||||
export const saveProfileTask = `mutation createProfileTask(${CreateProfileTask.variable}) {${CreateProfileTask.query}}`;
|
export const saveProfileTask = `mutation createProfileTask(${CreateProfileTask.variable}) {${CreateProfileTask.query}}`;
|
||||||
|
|
||||||
export const getProfileTaskList = `query getProfileTaskList(${GetProfileTaskList.variable}) {
|
export const getProfileTaskList = `query getProfileTaskList(${GetProfileTaskList.variable}) {
|
||||||
|
@ -30,7 +30,6 @@ export enum Calculations {
|
|||||||
ByteToMB = "byteToMB",
|
ByteToMB = "byteToMB",
|
||||||
ByteToGB = "byteToGB",
|
ByteToGB = "byteToGB",
|
||||||
Apdex = "apdex",
|
Apdex = "apdex",
|
||||||
Precision = "precision",
|
|
||||||
ConvertSeconds = "convertSeconds",
|
ConvertSeconds = "convertSeconds",
|
||||||
ConvertMilliseconds = "convertMilliseconds",
|
ConvertMilliseconds = "convertMilliseconds",
|
||||||
MsToS = "msTos",
|
MsToS = "msTos",
|
||||||
|
@ -115,5 +115,6 @@ export default function associateProcessor(props: Indexable) {
|
|||||||
item.metricValue = value;
|
item.metricValue = value;
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { eventAssociate, traceFilters };
|
return { eventAssociate, traceFilters };
|
||||||
}
|
}
|
||||||
|
@ -404,9 +404,6 @@ export function aggregation(val: number, config: { calculation?: string }): numb
|
|||||||
case Calculations.ConvertMilliseconds:
|
case Calculations.ConvertMilliseconds:
|
||||||
data = dayjs(val).format("YYYY-MM-DD HH:mm:ss");
|
data = dayjs(val).format("YYYY-MM-DD HH:mm:ss");
|
||||||
break;
|
break;
|
||||||
case Calculations.Precision:
|
|
||||||
data = data.toFixed(2);
|
|
||||||
break;
|
|
||||||
case Calculations.MsToS:
|
case Calculations.MsToS:
|
||||||
data = (val / 1000).toFixed(2);
|
data = (val / 1000).toFixed(2);
|
||||||
break;
|
break;
|
||||||
@ -416,6 +413,9 @@ export function aggregation(val: number, config: { calculation?: string }): numb
|
|||||||
case Calculations.NanosecondToMillisecond:
|
case Calculations.NanosecondToMillisecond:
|
||||||
data = (val / 1000 / 1000).toFixed(2);
|
data = (val / 1000 / 1000).toFixed(2);
|
||||||
break;
|
break;
|
||||||
|
case Calculations.ApdexAvg:
|
||||||
|
data = (val / 10000).toFixed(2);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
data;
|
data;
|
||||||
break;
|
break;
|
||||||
|
@ -25,5 +25,6 @@ limitations under the License. -->
|
|||||||
.app-main {
|
.app-main {
|
||||||
height: calc(100% - 40px);
|
height: calc(100% - 40px);
|
||||||
background: #f7f9fa;
|
background: #f7f9fa;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License. -->
|
limitations under the License. -->
|
||||||
<template>
|
<template>
|
||||||
<div class="nav-bar flex-h">
|
<div class="nav-bar flex-h">
|
||||||
<div class="title">{{ appStore.pageTitle || t(pageName) }}</div>
|
<div class="title">{{ route.name === "ViewWidget" ? "" : appStore.pageTitle || t(pageName) }}</div>
|
||||||
<div class="app-config">
|
<div class="app-config">
|
||||||
<span class="red" v-show="timeRange">{{ t("timeTips") }}</span>
|
<span class="red" v-show="timeRange">{{ t("timeTips") }}</span>
|
||||||
<TimePicker
|
<TimePicker
|
||||||
@ -102,7 +102,7 @@ limitations under the License. -->
|
|||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.nav-bar {
|
.nav-bar {
|
||||||
padding: 5px 10px 5px 28px;
|
padding: 5px 10px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: #fafbfc;
|
background-color: #fafbfc;
|
||||||
|
@ -13,62 +13,55 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License. -->
|
limitations under the License. -->
|
||||||
<template>
|
<template>
|
||||||
<div class="side-bar">
|
<div class="side-bar" v-if="showMenu" @click="isCollapse = false" @mouseleave="closeMenu">
|
||||||
<div :class="isCollapse ? 'logo-icon-collapse' : 'logo-icon'">
|
<div :class="isCollapse ? 'logo-icon-collapse' : 'logo-icon'">
|
||||||
<Icon :size="isCollapse ? 'xl' : 'logo'" :iconName="isCollapse ? 'logo' : 'logo-sw'" />
|
<Icon :size="isCollapse ? 'xl' : 'logo'" :iconName="isCollapse ? 'logo' : 'logo-sw'" />
|
||||||
</div>
|
</div>
|
||||||
<el-menu
|
<div class="menu scroll_bar_dark" :style="isCollapse ? {} : { width: '220px' }">
|
||||||
active-text-color="#448dfe"
|
<el-menu
|
||||||
background-color="#252a2f"
|
active-text-color="#448dfe"
|
||||||
class="el-menu-vertical"
|
background-color="#252a2f"
|
||||||
:default-active="name"
|
class="el-menu-vertical"
|
||||||
text-color="#efefef"
|
:default-active="name"
|
||||||
:unique-opened="true"
|
text-color="#efefef"
|
||||||
:collapse="isCollapse"
|
:collapse="isCollapse"
|
||||||
:style="{ border: 'none' }"
|
:collapse-transition="false"
|
||||||
>
|
:style="{ border: 'none' }"
|
||||||
<template v-for="(menu, index) in routes" :key="index">
|
>
|
||||||
<el-sub-menu :index="String(menu.name)" v-if="menu.meta.hasGroup">
|
<template v-for="(menu, index) in routes" :key="index">
|
||||||
<template #title>
|
<el-sub-menu :index="String(menu.name)" v-if="menu.meta.hasGroup" popper-class="sub-list">
|
||||||
<router-link class="items" :to="menu.path">
|
<template #title>
|
||||||
<el-icon class="menu-icons" :style="{ marginRight: '12px' }">
|
<router-link class="items" :to="menu.path">
|
||||||
<Icon size="lg" :iconName="menu.meta.icon" />
|
<el-icon class="menu-icons" :style="{ marginRight: '12px' }" @mouseover="setCollapse">
|
||||||
</el-icon>
|
<Icon size="lg" :iconName="menu.meta.icon" />
|
||||||
<span class="title" :class="isCollapse ? 'collapse' : ''">
|
</el-icon>
|
||||||
{{ t(menu.meta.title) }}
|
<span class="title" :class="isCollapse ? 'collapse' : ''">
|
||||||
</span>
|
{{ t(menu.meta.title) }}
|
||||||
</router-link>
|
</span>
|
||||||
</template>
|
|
||||||
<el-menu-item-group>
|
|
||||||
<el-menu-item v-for="(m, idx) in filterMenus(menu.children)" :index="m.name" :key="idx">
|
|
||||||
<router-link class="items" :to="m.path">
|
|
||||||
<span class="title">{{ m.meta && t(m.meta.title) }}</span>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</el-menu-item>
|
</template>
|
||||||
</el-menu-item-group>
|
<el-menu-item-group>
|
||||||
</el-sub-menu>
|
<el-menu-item v-for="(m, idx) in filterMenus(menu.children)" :index="m.name" :key="idx">
|
||||||
<el-menu-item :index="String(menu.name)" @click="changePage(menu)" v-else>
|
<router-link class="items" :to="m.path">
|
||||||
<el-icon class="menu-icons" :style="{ marginRight: '12px' }">
|
<span class="title">{{ m.meta && t(m.meta.title) }}</span>
|
||||||
<router-link class="items" :to="menu.children[0].path">
|
</router-link>
|
||||||
<Icon size="lg" :iconName="menu.meta.icon" />
|
</el-menu-item>
|
||||||
</router-link>
|
</el-menu-item-group>
|
||||||
</el-icon>
|
</el-sub-menu>
|
||||||
<template #title>
|
<el-menu-item :index="String(menu.name)" @click="changePage(menu)" v-else>
|
||||||
<router-link class="items" :to="menu.children[0].path">
|
<el-icon class="menu-icons" :style="{ marginRight: '12px' }" @mouseover="setCollapse">
|
||||||
<span class="title">{{ t(menu.meta.title) }}</span>
|
<router-link class="items menu-title" :to="menu.children[0].path">
|
||||||
</router-link>
|
<Icon size="lg" :iconName="menu.meta.icon" />
|
||||||
</template>
|
</router-link>
|
||||||
</el-menu-item>
|
</el-icon>
|
||||||
</template>
|
<template #title>
|
||||||
</el-menu>
|
<router-link class="items menu-title" :to="menu.children[0].path">
|
||||||
<div
|
<span class="title">{{ t(menu.meta.title) }}</span>
|
||||||
class="menu-control"
|
</router-link>
|
||||||
:class="isCollapse ? 'collapse' : ''"
|
</template>
|
||||||
:style="{
|
</el-menu-item>
|
||||||
color: theme === 'light' ? '#eee' : '#252a2f',
|
</template>
|
||||||
}"
|
</el-menu>
|
||||||
>
|
|
||||||
<Icon size="middle" iconName="format_indent_decrease" @click="controlMenu" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -76,7 +69,7 @@ limitations under the License. -->
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import type { RouteRecordRaw } from "vue-router";
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter, useRoute } from "vue-router";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import Icon from "@/components/Icon.vue";
|
import Icon from "@/components/Icon.vue";
|
||||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||||
@ -87,40 +80,64 @@ limitations under the License. -->
|
|||||||
const name = ref<string>(String(useRouter().currentRoute.value.name));
|
const name = ref<string>(String(useRouter().currentRoute.value.name));
|
||||||
const theme = ["VirtualMachine", "Kubernetes"].includes(name.value || "") ? ref("light") : ref("black");
|
const theme = ["VirtualMachine", "Kubernetes"].includes(name.value || "") ? ref("light") : ref("black");
|
||||||
const routes = ref<RouteRecordRaw[] | any>(useRouter().options.routes);
|
const routes = ref<RouteRecordRaw[] | any>(useRouter().options.routes);
|
||||||
|
const route = useRoute();
|
||||||
|
const isCollapse = ref(true);
|
||||||
|
const showMenu = ref(true);
|
||||||
|
const open = ref<boolean>(false);
|
||||||
|
|
||||||
if (/Android|webOS|iPhone|iPod|iPad|BlackBerry/i.test(navigator.userAgent)) {
|
if (/Android|webOS|iPhone|iPod|iPad|BlackBerry/i.test(navigator.userAgent)) {
|
||||||
appStore.setIsMobile(true);
|
appStore.setIsMobile(true);
|
||||||
} else {
|
} else {
|
||||||
appStore.setIsMobile(false);
|
appStore.setIsMobile(false);
|
||||||
}
|
}
|
||||||
const isCollapse = ref(false);
|
if (route.name === "ViewWidget") {
|
||||||
const controlMenu = () => {
|
showMenu.value = false;
|
||||||
isCollapse.value = !isCollapse.value;
|
}
|
||||||
};
|
|
||||||
const changePage = (menu: RouteRecordRaw) => {
|
const changePage = (menu: RouteRecordRaw) => {
|
||||||
theme.value = ["VirtualMachine", "Kubernetes"].includes(String(menu.name)) ? "light" : "black";
|
theme.value = ["VirtualMachine", "Kubernetes"].includes(String(menu.name)) ? "light" : "black";
|
||||||
};
|
};
|
||||||
const filterMenus = (menus: Recordable[]) => {
|
const filterMenus = (menus: Recordable[]) => {
|
||||||
return menus.filter((d) => d.meta && !d.meta.notShow);
|
return menus.filter((d) => d.meta && !d.meta.notShow);
|
||||||
};
|
};
|
||||||
|
function setCollapse() {
|
||||||
|
open.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (open.value) {
|
||||||
|
isCollapse.value = false;
|
||||||
|
}
|
||||||
|
open.value = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
function closeMenu() {
|
||||||
|
isCollapse.value = true;
|
||||||
|
open.value = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.side-bar {
|
.side-bar {
|
||||||
background: #252a2f;
|
background: #252a2f;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin-bottom: 100px;
|
margin-bottom: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
height: calc(100% - 30px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu:hover {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-menu-vertical:not(.el-menu--collapse) {
|
.el-menu-vertical:not(.el-menu--collapse) {
|
||||||
width: 220px;
|
width: 220px;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-icon-collapse {
|
.logo-icon-collapse {
|
||||||
width: 65px;
|
width: 65px;
|
||||||
margin: 15px 0 10px 0;
|
margin: 5px 0 10px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,16 +154,6 @@ limitations under the License. -->
|
|||||||
width: 110px;
|
width: 110px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-control {
|
|
||||||
position: absolute;
|
|
||||||
top: 7px;
|
|
||||||
left: 220px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s linear;
|
|
||||||
z-index: 99;
|
|
||||||
color: #252a2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-control.collapse {
|
.menu-control.collapse {
|
||||||
left: 70px;
|
left: 70px;
|
||||||
}
|
}
|
||||||
@ -178,7 +185,7 @@ limitations under the License. -->
|
|||||||
|
|
||||||
.title {
|
.title {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 110px;
|
max-width: 200px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@ -179,6 +179,13 @@ const msg = {
|
|||||||
when4xx: "Sample HTTP requests and responses with tracing when response code between 400 and 499",
|
when4xx: "Sample HTTP requests and responses with tracing when response code between 400 and 499",
|
||||||
when5xx: "Sample HTTP requests and responses with tracing when response code between 500 and 599",
|
when5xx: "Sample HTTP requests and responses with tracing when response code between 500 and 599",
|
||||||
taskTitle: "HTTP request and response collecting rules",
|
taskTitle: "HTTP request and response collecting rules",
|
||||||
|
iframeWidgetTip: "Add a link to a widget",
|
||||||
|
iframeSrc: "Iframe Link",
|
||||||
|
generateLink: "Generate Link",
|
||||||
|
setDuration: "Lock Query Duration",
|
||||||
|
openFunction: "OpenFunction",
|
||||||
|
period: "Period",
|
||||||
|
windows: "Windows",
|
||||||
seconds: "Seconds",
|
seconds: "Seconds",
|
||||||
hourTip: "Select Hour",
|
hourTip: "Select Hour",
|
||||||
minuteTip: "Select Minute",
|
minuteTip: "Select Minute",
|
||||||
@ -197,7 +204,7 @@ const msg = {
|
|||||||
topology: "Topology",
|
topology: "Topology",
|
||||||
trace: "Trace",
|
trace: "Trace",
|
||||||
alarm: "Alerting",
|
alarm: "Alerting",
|
||||||
auto: "Auto",
|
auto: "Auto Fresh",
|
||||||
reload: "Reload",
|
reload: "Reload",
|
||||||
version: "Version",
|
version: "Version",
|
||||||
copy: "Copy",
|
copy: "Copy",
|
||||||
@ -373,5 +380,10 @@ const msg = {
|
|||||||
virtualMQ: "Virtual MQ",
|
virtualMQ: "Virtual MQ",
|
||||||
AWSCloud: "AWS Cloud",
|
AWSCloud: "AWS Cloud",
|
||||||
AWSCloudEKS: "EKS",
|
AWSCloudEKS: "EKS",
|
||||||
|
AWSCloudS3: "S3",
|
||||||
|
AWSCloudDynamoDB: "DynamoDB",
|
||||||
|
AWSGateway: "AWS API Gateway",
|
||||||
|
APIGateway: "API Gateway",
|
||||||
|
redis: "Redis",
|
||||||
};
|
};
|
||||||
export default msg;
|
export default msg;
|
||||||
|
@ -162,6 +162,11 @@ const msg = {
|
|||||||
latency: "Retraso",
|
latency: "Retraso",
|
||||||
metricValues: "Valor métrico",
|
metricValues: "Valor métrico",
|
||||||
legendValues: "Valor de la leyenda",
|
legendValues: "Valor de la leyenda",
|
||||||
|
iframeWidgetTip: "Añadir enlaces a los gadgets",
|
||||||
|
iframeSrc: "Enlace Iframe",
|
||||||
|
generateLink: "Generar enlaces",
|
||||||
|
setDuration: "Duración de la consulta de bloqueo",
|
||||||
|
openFunction: "OpenFunction",
|
||||||
seconds: "Segundos",
|
seconds: "Segundos",
|
||||||
hourTip: "Seleccione Hora",
|
hourTip: "Seleccione Hora",
|
||||||
minuteTip: "Seleccione Minuto",
|
minuteTip: "Seleccione Minuto",
|
||||||
@ -181,6 +186,8 @@ const msg = {
|
|||||||
when4xx: "Ejemplo de solicitud y respuesta http con seguimiento cuando el Código de respuesta está entre 400 y 499",
|
when4xx: "Ejemplo de solicitud y respuesta http con seguimiento cuando el Código de respuesta está entre 400 y 499",
|
||||||
when5xx: "Ejemplo de solicitud y respuesta http con seguimiento cuando el Código de respuesta está entre 500 y 599",
|
when5xx: "Ejemplo de solicitud y respuesta http con seguimiento cuando el Código de respuesta está entre 500 y 599",
|
||||||
taskTitle: "Reglas de recolección de peticiones y respuestas HTTP",
|
taskTitle: "Reglas de recolección de peticiones y respuestas HTTP",
|
||||||
|
period: "Period",
|
||||||
|
windows: "Windows",
|
||||||
second: "s",
|
second: "s",
|
||||||
yearSuffix: "Año",
|
yearSuffix: "Año",
|
||||||
monthsHead: "Ene_Feb_Mar_Abr_May_Jun_Jul_Ago_Set_Oct_Nov_Dic",
|
monthsHead: "Ene_Feb_Mar_Abr_May_Jun_Jul_Ago_Set_Oct_Nov_Dic",
|
||||||
@ -195,7 +202,7 @@ const msg = {
|
|||||||
topology: "Topología",
|
topology: "Topología",
|
||||||
trace: "Traza",
|
trace: "Traza",
|
||||||
alarm: "Recordatorio en curso",
|
alarm: "Recordatorio en curso",
|
||||||
auto: "Auto",
|
auto: "Auto Fresh",
|
||||||
reload: "Recargar",
|
reload: "Recargar",
|
||||||
version: "Versión",
|
version: "Versión",
|
||||||
copy: "Copiar",
|
copy: "Copiar",
|
||||||
@ -372,5 +379,10 @@ const msg = {
|
|||||||
virtualMQ: "MQ virtual",
|
virtualMQ: "MQ virtual",
|
||||||
AWSCloud: "AWS Cloud",
|
AWSCloud: "AWS Cloud",
|
||||||
AWSCloudEKS: "EKS",
|
AWSCloudEKS: "EKS",
|
||||||
|
AWSCloudS3: "S3",
|
||||||
|
AWSCloudDynamoDB: "DynamoDB",
|
||||||
|
AWSGateway: "AWS API Gateway",
|
||||||
|
APIGateway: "API Gateway",
|
||||||
|
redis: "Redis",
|
||||||
};
|
};
|
||||||
export default msg;
|
export default msg;
|
||||||
|
@ -176,6 +176,13 @@ const msg = {
|
|||||||
when4xx: "当响应代码介于400和499之间时,带有跟踪的HTTP请求和响应示例",
|
when4xx: "当响应代码介于400和499之间时,带有跟踪的HTTP请求和响应示例",
|
||||||
when5xx: "当响应代码介于500和599之间时,带有跟踪的HTTP请求和响应示例",
|
when5xx: "当响应代码介于500和599之间时,带有跟踪的HTTP请求和响应示例",
|
||||||
taskTitle: "HTTP请求和响应收集规则",
|
taskTitle: "HTTP请求和响应收集规则",
|
||||||
|
iframeWidgetTip: "添加widget的链接",
|
||||||
|
iframeSrc: "Iframe链接",
|
||||||
|
generateLink: "生成链接",
|
||||||
|
setDuration: "锁定查询持续时间",
|
||||||
|
openFunction: "OpenFunction",
|
||||||
|
period: "周期",
|
||||||
|
windows: "Windows",
|
||||||
seconds: "秒",
|
seconds: "秒",
|
||||||
hourTip: "选择小时",
|
hourTip: "选择小时",
|
||||||
minuteTip: "选择分钟",
|
minuteTip: "选择分钟",
|
||||||
@ -194,7 +201,7 @@ const msg = {
|
|||||||
trace: "追踪",
|
trace: "追踪",
|
||||||
alarm: "告警",
|
alarm: "告警",
|
||||||
event: "事件",
|
event: "事件",
|
||||||
auto: "自动",
|
auto: "自动更新",
|
||||||
reload: "刷新",
|
reload: "刷新",
|
||||||
editmode: "编辑模式",
|
editmode: "编辑模式",
|
||||||
version: "版本",
|
version: "版本",
|
||||||
@ -370,5 +377,10 @@ const msg = {
|
|||||||
virtualMQ: "虚拟消息队列",
|
virtualMQ: "虚拟消息队列",
|
||||||
AWSCloud: "AWS云服务",
|
AWSCloud: "AWS云服务",
|
||||||
AWSCloudEKS: "EKS",
|
AWSCloudEKS: "EKS",
|
||||||
|
AWSCloudS3: "S3",
|
||||||
|
AWSCloudDynamoDB: "DynamoDB",
|
||||||
|
AWSGateway: "AWS API Gateway",
|
||||||
|
APIGateway: "API Gateway",
|
||||||
|
redis: "Redis",
|
||||||
};
|
};
|
||||||
export default msg;
|
export default msg;
|
||||||
|
50
src/mock/index.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* 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 Mock from "mockjs";
|
||||||
|
|
||||||
|
const Random = Mock.Random;
|
||||||
|
const nodes = Mock.mock({
|
||||||
|
"nodes|500": [
|
||||||
|
{
|
||||||
|
//id
|
||||||
|
id: "@guid",
|
||||||
|
name: "@name",
|
||||||
|
"type|1": ["ActiveMQ", "activemq-consumer", "H2", "APISIX", "Express", "USER", "Flash"],
|
||||||
|
"isReal|1": [true, false],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const calls = Mock.mock({
|
||||||
|
"links|500": [
|
||||||
|
{
|
||||||
|
//id
|
||||||
|
id: "@guid",
|
||||||
|
detectPoints: ["SERVER", "CLIENT"],
|
||||||
|
source: function () {
|
||||||
|
const d = Random.integer(0, 250);
|
||||||
|
return nodes.nodes[d].id;
|
||||||
|
},
|
||||||
|
target: function () {
|
||||||
|
const d = Random.integer(250, 499);
|
||||||
|
return nodes.nodes[d].id;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const callsMock = calls.links;
|
||||||
|
const nodesMock = nodes.nodes;
|
||||||
|
export { callsMock, nodesMock };
|
@ -176,6 +176,21 @@ export const routesDashboard: Array<RouteRecordRaw> = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
name: "Widget",
|
||||||
|
component: () => import("@/views/dashboard/Widget.vue"),
|
||||||
|
meta: {
|
||||||
|
notShow: true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/page/:layer/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:config/:duration?",
|
||||||
|
component: () => import("@/views/dashboard/Widget.vue"),
|
||||||
|
name: "ViewWidget",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -42,6 +42,54 @@ export default [
|
|||||||
layer: "AWS_EKS",
|
layer: "AWS_EKS",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/aws-s3",
|
||||||
|
name: "AWSCloudS3",
|
||||||
|
meta: {
|
||||||
|
title: "AWSCloudS3",
|
||||||
|
layer: "AWS_S3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/aws-s3/tab/:activeTabIndex",
|
||||||
|
name: "S3ActiveTabIndex",
|
||||||
|
meta: {
|
||||||
|
notShow: true,
|
||||||
|
layer: "AWS_S3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/aws-dynamodb",
|
||||||
|
name: "AWSCloudDynamoDB",
|
||||||
|
meta: {
|
||||||
|
title: "AWSCloudDynamoDB",
|
||||||
|
layer: "AWS_DYNAMODB",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/aws-dynamodb/tab/:activeTabIndex",
|
||||||
|
name: "DynamoDBActiveTabIndex",
|
||||||
|
meta: {
|
||||||
|
notShow: true,
|
||||||
|
layer: "AWS_DYNAMODB",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/aws-api-gateway",
|
||||||
|
name: "APIGateway",
|
||||||
|
meta: {
|
||||||
|
title: "APIGateway",
|
||||||
|
layer: "AWS_GATEWAY",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/aws-api-gateway/tab/:activeTabIndex",
|
||||||
|
name: "APIGatewayActiveTabIndex",
|
||||||
|
meta: {
|
||||||
|
notShow: true,
|
||||||
|
layer: "AWS_GATEWAY",
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -58,6 +58,38 @@ export default [
|
|||||||
layer: "POSTGRESQL",
|
layer: "POSTGRESQL",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/aws-dynamodb",
|
||||||
|
name: "AWSCloudDynamoDB",
|
||||||
|
meta: {
|
||||||
|
title: "AWSCloudDynamoDB",
|
||||||
|
layer: "AWS_DYNAMODB",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/aws-dynamodb/tab/:activeTabIndex",
|
||||||
|
name: "DynamoDBActiveTabIndex",
|
||||||
|
meta: {
|
||||||
|
notShow: true,
|
||||||
|
layer: "AWS_DYNAMODB",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/redis",
|
||||||
|
name: "Redis",
|
||||||
|
meta: {
|
||||||
|
title: "redis",
|
||||||
|
layer: "REDIS",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/redis/tab/:activeTabIndex",
|
||||||
|
name: "RedisActiveTabIndex",
|
||||||
|
meta: {
|
||||||
|
notShow: true,
|
||||||
|
layer: "REDIS",
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -22,17 +22,24 @@ export default [
|
|||||||
meta: {
|
meta: {
|
||||||
title: "functions",
|
title: "functions",
|
||||||
icon: "functions",
|
icon: "functions",
|
||||||
layer: "FAAS",
|
hasGroup: true,
|
||||||
},
|
},
|
||||||
redirect: "/functions",
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "/functions",
|
path: "/openFunction",
|
||||||
name: "Functions",
|
name: "OpenFunction",
|
||||||
|
meta: {
|
||||||
|
title: "openFunction",
|
||||||
|
layer: "FAAS",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/functions/tab/:activeTabIndex",
|
path: "/openFunction/tab/:activeTabIndex",
|
||||||
name: "FunctionsActiveTabIndex",
|
name: "OpenFunctionActiveTabIndex",
|
||||||
|
meta: {
|
||||||
|
notShow: true,
|
||||||
|
layer: "FAAS",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -42,6 +42,22 @@ export default [
|
|||||||
layer: "APISIX",
|
layer: "APISIX",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/aws-gateway",
|
||||||
|
name: "AWSGateway",
|
||||||
|
meta: {
|
||||||
|
title: "AWSGateway",
|
||||||
|
layer: "AWS_GATEWAY",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/aws-gateway/tab/:activeTabIndex",
|
||||||
|
name: "GatewayActiveTabIndex",
|
||||||
|
meta: {
|
||||||
|
notShow: true,
|
||||||
|
layer: "AWS_GATEWAY",
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -43,6 +43,23 @@ export default [
|
|||||||
layer: "OS_LINUX",
|
layer: "OS_LINUX",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/windows",
|
||||||
|
name: "Windows",
|
||||||
|
meta: {
|
||||||
|
title: "windows",
|
||||||
|
layer: "OS_WINDOWS",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/windows/tab/:activeTabIndex",
|
||||||
|
name: "WindowsActiveTabIndex",
|
||||||
|
meta: {
|
||||||
|
title: "windows",
|
||||||
|
notShow: true,
|
||||||
|
layer: "OS_WINDOWS",
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -37,3 +37,5 @@ export const TimeRangeConfig = {
|
|||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
text: "text",
|
text: "text",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ControlsTypes = ["Trace", "Profile", "Log", "DemandLog", "Ebpf", "NetworkProfiling", "ThirdPartyApp"];
|
||||||
|
@ -21,7 +21,7 @@ import graphql from "@/graphql";
|
|||||||
import query from "@/graphql/fetch";
|
import query from "@/graphql/fetch";
|
||||||
import type { DashboardItem } from "@/types/dashboard";
|
import type { DashboardItem } from "@/types/dashboard";
|
||||||
import { useSelectorStore } from "@/store/modules/selectors";
|
import { useSelectorStore } from "@/store/modules/selectors";
|
||||||
import { NewControl, TextConfig, TimeRangeConfig } from "../data";
|
import { NewControl, TextConfig, TimeRangeConfig, ControlsTypes } from "../data";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
@ -40,6 +40,7 @@ interface DashboardState {
|
|||||||
currentDashboard: Nullable<DashboardItem>;
|
currentDashboard: Nullable<DashboardItem>;
|
||||||
editMode: boolean;
|
editMode: boolean;
|
||||||
currentTabIndex: number;
|
currentTabIndex: number;
|
||||||
|
showLinkConfig: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dashboardStore = defineStore({
|
export const dashboardStore = defineStore({
|
||||||
@ -58,6 +59,7 @@ export const dashboardStore = defineStore({
|
|||||||
currentDashboard: null,
|
currentDashboard: null,
|
||||||
editMode: false,
|
editMode: false,
|
||||||
currentTabIndex: 0,
|
currentTabIndex: 0,
|
||||||
|
showLinkConfig: false,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setLayout(data: LayoutConfig[]) {
|
setLayout(data: LayoutConfig[]) {
|
||||||
@ -66,6 +68,9 @@ export const dashboardStore = defineStore({
|
|||||||
setMode(mode: boolean) {
|
setMode(mode: boolean) {
|
||||||
this.editMode = mode;
|
this.editMode = mode;
|
||||||
},
|
},
|
||||||
|
setWidgetLink(show: boolean) {
|
||||||
|
this.showLinkConfig = show;
|
||||||
|
},
|
||||||
resetDashboards(list: DashboardItem[]) {
|
resetDashboards(list: DashboardItem[]) {
|
||||||
this.dashboards = list;
|
this.dashboards = list;
|
||||||
sessionStorage.setItem("dashboards", JSON.stringify(list));
|
sessionStorage.setItem("dashboards", JSON.stringify(list));
|
||||||
@ -108,7 +113,7 @@ export const dashboardStore = defineStore({
|
|||||||
depth: this.entity === EntityType[1].value ? 1 : this.entity === EntityType[0].value ? 2 : 3,
|
depth: this.entity === EntityType[1].value ? 1 : this.entity === EntityType[0].value ? 2 : 3,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (["Trace", "Profile", "Log", "DemandLog", "Ebpf", "NetworkProfiling"].includes(type)) {
|
if (ControlsTypes.includes(type)) {
|
||||||
newItem.h = 36;
|
newItem.h = 36;
|
||||||
}
|
}
|
||||||
if (type === "Text") {
|
if (type === "Text") {
|
||||||
@ -168,7 +173,7 @@ export const dashboardStore = defineStore({
|
|||||||
showDepth: true,
|
showDepth: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (["Trace", "Profile", "Log", "DemandLog", "Ebpf", "NetworkProfiling"].includes(type)) {
|
if (ControlsTypes.includes(type)) {
|
||||||
newItem.h = 32;
|
newItem.h = 32;
|
||||||
}
|
}
|
||||||
if (type === "Text") {
|
if (type === "Text") {
|
||||||
@ -418,13 +423,15 @@ export const dashboardStore = defineStore({
|
|||||||
res = await graphql.query("addNewTemplate").params({ setting: { configuration: JSON.stringify(c) } });
|
res = await graphql.query("addNewTemplate").params({ setting: { configuration: JSON.stringify(c) } });
|
||||||
|
|
||||||
json = res.data.data.addTemplate;
|
json = res.data.data.addTemplate;
|
||||||
|
if (!json.status) {
|
||||||
|
ElMessage.error(json.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (res.data.errors || res.errors) {
|
if (res.data.errors || res.errors) {
|
||||||
ElMessage.error(res.data.errors);
|
ElMessage.error(res.data.errors);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
if (!json.status) {
|
if (!json.status) {
|
||||||
ElMessage.error(json.message);
|
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
if (!this.currentDashboard.id) {
|
if (!this.currentDashboard.id) {
|
||||||
|
@ -34,6 +34,7 @@ interface ProfileState {
|
|||||||
taskEndpoints: Endpoint[];
|
taskEndpoints: Endpoint[];
|
||||||
condition: { serviceId: string; endpointName: string };
|
condition: { serviceId: string; endpointName: string };
|
||||||
taskList: TaskListItem[];
|
taskList: TaskListItem[];
|
||||||
|
currentTask: Recordable<TaskListItem>;
|
||||||
segmentList: Trace[];
|
segmentList: Trace[];
|
||||||
currentSegment: Recordable<Trace>;
|
currentSegment: Recordable<Trace>;
|
||||||
segmentSpans: Array<Recordable<SegmentSpan>>;
|
segmentSpans: Array<Recordable<SegmentSpan>>;
|
||||||
@ -51,6 +52,7 @@ export const profileStore = defineStore({
|
|||||||
condition: { serviceId: "", endpointName: "" },
|
condition: { serviceId: "", endpointName: "" },
|
||||||
taskList: [],
|
taskList: [],
|
||||||
segmentList: [],
|
segmentList: [],
|
||||||
|
currentTask: {},
|
||||||
currentSegment: {},
|
currentSegment: {},
|
||||||
segmentSpans: [],
|
segmentSpans: [],
|
||||||
currentSpan: {},
|
currentSpan: {},
|
||||||
@ -65,11 +67,27 @@ export const profileStore = defineStore({
|
|||||||
...data,
|
...data,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
setCurrentTask(task: TaskListItem) {
|
||||||
|
this.currentTask = task || {};
|
||||||
|
this.analyzeTrees = [];
|
||||||
|
},
|
||||||
|
setSegmentSpans(spans: Recordable<SegmentSpan>[]) {
|
||||||
|
this.currentSpan = spans[0] || {};
|
||||||
|
this.segmentSpans = spans;
|
||||||
|
},
|
||||||
setCurrentSpan(span: Recordable<SegmentSpan>) {
|
setCurrentSpan(span: Recordable<SegmentSpan>) {
|
||||||
this.currentSpan = span;
|
this.currentSpan = span;
|
||||||
|
this.analyzeTrees = [];
|
||||||
},
|
},
|
||||||
setCurrentSegment(s: Recordable<Trace>) {
|
setCurrentSegment(segment: Trace) {
|
||||||
this.currentSegment = s;
|
this.currentSegment = segment;
|
||||||
|
this.segmentSpans = segment.spans || [];
|
||||||
|
if (segment.spans) {
|
||||||
|
this.currentSpan = segment.spans[0] || {};
|
||||||
|
} else {
|
||||||
|
this.currentSpan = {};
|
||||||
|
}
|
||||||
|
this.analyzeTrees = [];
|
||||||
},
|
},
|
||||||
setHighlightTop() {
|
setHighlightTop() {
|
||||||
this.highlightTop = !this.highlightTop;
|
this.highlightTop = !this.highlightTop;
|
||||||
@ -104,8 +122,9 @@ export const profileStore = defineStore({
|
|||||||
if (res.data.errors) {
|
if (res.data.errors) {
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
const list = res.data.data.taskList;
|
const list = res.data.data.taskList || [];
|
||||||
this.taskList = list;
|
this.taskList = list;
|
||||||
|
this.currentTask = list[0] || {};
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
this.segmentList = [];
|
this.segmentList = [];
|
||||||
this.segmentSpans = [];
|
this.segmentSpans = [];
|
||||||
@ -128,7 +147,7 @@ export const profileStore = defineStore({
|
|||||||
}
|
}
|
||||||
const { segmentList } = res.data.data;
|
const { segmentList } = res.data.data;
|
||||||
|
|
||||||
this.segmentList = segmentList;
|
this.segmentList = segmentList || [];
|
||||||
if (!segmentList.length) {
|
if (!segmentList.length) {
|
||||||
this.segmentSpans = [];
|
this.segmentSpans = [];
|
||||||
this.analyzeTrees = [];
|
this.analyzeTrees = [];
|
||||||
@ -137,7 +156,7 @@ export const profileStore = defineStore({
|
|||||||
}
|
}
|
||||||
if (segmentList[0]) {
|
if (segmentList[0]) {
|
||||||
this.currentSegment = segmentList[0];
|
this.currentSegment = segmentList[0];
|
||||||
this.getSegmentSpans({ segmentId: segmentList[0].segmentId });
|
this.getSegmentSpans(segmentList[0].segmentId);
|
||||||
} else {
|
} else {
|
||||||
this.currentSegment = {};
|
this.currentSegment = {};
|
||||||
}
|
}
|
||||||
@ -173,14 +192,11 @@ export const profileStore = defineStore({
|
|||||||
this.currentSpan = segment.spans[index];
|
this.currentSpan = segment.spans[index];
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
async getProfileAnalyze(params: { segmentId: string; timeRanges: Array<{ start: number; end: number }> }) {
|
async getProfileAnalyze(params: Array<{ segmentId: string; timeRange: { start: number; end: number } }>) {
|
||||||
if (!params.segmentId) {
|
if (!params.length) {
|
||||||
return new Promise((resolve) => resolve({}));
|
return new Promise((resolve) => resolve({}));
|
||||||
}
|
}
|
||||||
if (!params.timeRanges.length) {
|
const res: AxiosResponse = await graphql.query("getProfileAnalyze").params({ queries: params });
|
||||||
return new Promise((resolve) => resolve({}));
|
|
||||||
}
|
|
||||||
const res: AxiosResponse = await graphql.query("getProfileAnalyze").params(params);
|
|
||||||
|
|
||||||
if (res.data.errors) {
|
if (res.data.errors) {
|
||||||
this.analyzeTrees = [];
|
this.analyzeTrees = [];
|
||||||
|
@ -211,12 +211,12 @@ export const selectorStore = defineStore({
|
|||||||
|
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
async getProcess(instanceId: string, isRelation?: boolean) {
|
async getProcess(processId: string, isRelation?: boolean) {
|
||||||
if (!instanceId) {
|
if (!processId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res: AxiosResponse = await graphql.query("queryProcess").params({
|
const res: AxiosResponse = await graphql.query("queryProcess").params({
|
||||||
instanceId,
|
processId,
|
||||||
});
|
});
|
||||||
if (!res.data.errors) {
|
if (!res.data.errors) {
|
||||||
if (isRelation) {
|
if (isRelation) {
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.flex-v {
|
.flex-v {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -102,6 +103,13 @@
|
|||||||
.mt-0 {
|
.mt-0 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
.mt-10 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-20 {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.mb-5 {
|
.mb-5 {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
@ -189,3 +197,49 @@
|
|||||||
box-shadow: inset 0 0 6px #ccc;
|
box-shadow: inset 0 0 6px #ccc;
|
||||||
background-color: #aaa;
|
background-color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scroll_bar_dark::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll_bar_dark::-webkit-scrollbar-track {
|
||||||
|
background-color: #252a2f;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: inset 0 0 6px #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll_bar_dark::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: inset 0 0 6px #888;
|
||||||
|
background-color: #999;
|
||||||
|
}
|
||||||
|
.d3-tip {
|
||||||
|
line-height: 1;
|
||||||
|
padding: 8px;
|
||||||
|
color: #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.d3-tip {
|
||||||
|
background: #252a2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d3-tip:after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
font-size: 10px;
|
||||||
|
width: 100%;
|
||||||
|
line-height: 0.8;
|
||||||
|
color: #252a2f;
|
||||||
|
content: "\25BC";
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d3-tip.n:after {
|
||||||
|
margin: -2px 0 0 0;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
@ -135,7 +135,7 @@ pre {
|
|||||||
.el-sub-menu .el-menu-item {
|
.el-sub-menu .el-menu-item {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
padding-left: 56px !important;
|
padding: 0 0 0 56px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-sub-menu__title {
|
.el-sub-menu__title {
|
||||||
@ -204,3 +204,11 @@ div.vis-tooltip {
|
|||||||
.vis-item.vis-selected.Normal {
|
.vis-item.vis-selected.Normal {
|
||||||
color: #1a1a1a !important;
|
color: #1a1a1a !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-menu--vertical.sub-list {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div:has(> a.menu-title) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
1
src/types/components.d.ts
vendored
@ -22,7 +22,6 @@ declare module '@vue/runtime-core' {
|
|||||||
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
|
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
|
||||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
|
17
src/types/mock.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
declare module "mockjs";
|
8
src/types/topology.d.ts
vendored
@ -26,6 +26,10 @@ export interface Call {
|
|||||||
lowerArc?: boolean;
|
lowerArc?: boolean;
|
||||||
sourceComponents: string[];
|
sourceComponents: string[];
|
||||||
targetComponents: string[];
|
targetComponents: string[];
|
||||||
|
sourceX?: number;
|
||||||
|
sourceY?: number;
|
||||||
|
targetY?: number;
|
||||||
|
targetX?: number;
|
||||||
}
|
}
|
||||||
export interface Node {
|
export interface Node {
|
||||||
id: string;
|
id: string;
|
||||||
@ -34,4 +38,8 @@ export interface Node {
|
|||||||
isReal: boolean;
|
isReal: boolean;
|
||||||
layer?: string;
|
layer?: string;
|
||||||
serviceName?: string;
|
serviceName?: string;
|
||||||
|
height?: number;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
level?: number;
|
||||||
}
|
}
|
||||||
|
1
src/types/trace.d.ts
vendored
@ -22,6 +22,7 @@ export interface Trace {
|
|||||||
start: string;
|
start: string;
|
||||||
traceIds: Array<string | any>;
|
traceIds: Array<string | any>;
|
||||||
segmentId: string;
|
segmentId: string;
|
||||||
|
spans: Span[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Span {
|
export interface Span {
|
||||||
|
@ -30,6 +30,14 @@ limitations under the License. -->
|
|||||||
>
|
>
|
||||||
<component :is="dashboardStore.selectedGrid.type" />
|
<component :is="dashboardStore.selectedGrid.type" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
<el-dialog
|
||||||
|
v-model="dashboardStore.showLinkConfig"
|
||||||
|
width="800px"
|
||||||
|
:destroy-on-close="true"
|
||||||
|
@closed="dashboardStore.setWidgetLink(false)"
|
||||||
|
>
|
||||||
|
<WidgetLink />
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -42,16 +50,18 @@ limitations under the License. -->
|
|||||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||||
import Configuration from "./configuration";
|
import Configuration from "./configuration";
|
||||||
import type { LayoutConfig } from "@/types/dashboard";
|
import type { LayoutConfig } from "@/types/dashboard";
|
||||||
|
import WidgetLink from "./components/WidgetLink.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Dashboard",
|
name: "Dashboard",
|
||||||
components: { ...Configuration, GridLayout, Tool },
|
components: { ...Configuration, GridLayout, Tool, WidgetLink },
|
||||||
setup() {
|
setup() {
|
||||||
const dashboardStore = useDashboardStore();
|
const dashboardStore = useDashboardStore();
|
||||||
const appStore = useAppStoreWithOut();
|
const appStore = useAppStoreWithOut();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const p = useRoute().params;
|
const p = useRoute().params;
|
||||||
const layoutKey = ref<string>(`${p.layerId}_${p.entity}_${p.name}`);
|
const layoutKey = ref<string>(`${p.layerId}_${p.entity}_${p.name}`);
|
||||||
|
|
||||||
setTemplate();
|
setTemplate();
|
||||||
async function setTemplate() {
|
async function setTemplate() {
|
||||||
await dashboardStore.setDashboards();
|
await dashboardStore.setDashboards();
|
||||||
|
@ -226,6 +226,7 @@ limitations under the License. -->
|
|||||||
standard?: unknown;
|
standard?: unknown;
|
||||||
label?: string;
|
label?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
|
filters?: unknown;
|
||||||
})[],
|
})[],
|
||||||
) {
|
) {
|
||||||
for (const child of children || []) {
|
for (const child of children || []) {
|
||||||
@ -235,6 +236,7 @@ limitations under the License. -->
|
|||||||
delete child.id;
|
delete child.id;
|
||||||
delete child.label;
|
delete child.label;
|
||||||
delete child.value;
|
delete child.value;
|
||||||
|
delete child.filters;
|
||||||
if (isEmptyObject(child.graph)) {
|
if (isEmptyObject(child.graph)) {
|
||||||
delete child.graph;
|
delete child.graph;
|
||||||
}
|
}
|
||||||
@ -469,7 +471,7 @@ limitations under the License. -->
|
|||||||
.dashboard-list {
|
.dashboard-list {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-with-search {
|
.input-with-search {
|
||||||
|
200
src/views/dashboard/Widget.vue
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
<!-- 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. -->
|
||||||
|
<template>
|
||||||
|
<div class="content">
|
||||||
|
<div class="header">
|
||||||
|
<span>{{ decodeURIComponent(title) }}</span>
|
||||||
|
<div class="tips" v-show="tips">
|
||||||
|
<el-tooltip :content="decodeURIComponent(tips) || ''">
|
||||||
|
<span>
|
||||||
|
<Icon iconName="info_outline" size="sm" />
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="widget-chart" :style="{ height: config.height - 60 + 'px' }">
|
||||||
|
<component
|
||||||
|
:is="graph.type"
|
||||||
|
:intervalTime="appStoreWithOut.intervalTime"
|
||||||
|
:data="source"
|
||||||
|
:config="{
|
||||||
|
i: 0,
|
||||||
|
...graph,
|
||||||
|
metrics: config.metrics,
|
||||||
|
metricTypes: config.metricTypes,
|
||||||
|
metricConfig: config.metricConfig,
|
||||||
|
}"
|
||||||
|
:needQuery="true"
|
||||||
|
/>
|
||||||
|
<div v-show="!config.type" class="no-data">
|
||||||
|
{{ t("noData") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, ref, defineComponent, watch } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { useSelectorStore } from "@/store/modules/selectors";
|
||||||
|
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||||
|
import { useQueryProcessor, useSourceProcessor, useGetMetricEntity } from "@/hooks/useMetricsProcessor";
|
||||||
|
import graphs from "./graphs";
|
||||||
|
import { EntityType } from "./data";
|
||||||
|
import timeFormat from "@/utils/timeFormat";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "WidgetPage",
|
||||||
|
components: {
|
||||||
|
...graphs,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const appStoreWithOut = useAppStoreWithOut();
|
||||||
|
const selectorStore = useSelectorStore();
|
||||||
|
const route = useRoute();
|
||||||
|
const config = computed<any>(() => JSON.parse(decodeURIComponent(route.params.config as string) as string));
|
||||||
|
const graph = computed(() => config.value.graph || {});
|
||||||
|
const source = ref<unknown>({});
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
const title = computed(() => (config.value.widget && config.value.widget.title) || "");
|
||||||
|
const tips = computed(() => (config.value.widget && config.value.widget.tips) || "");
|
||||||
|
|
||||||
|
init();
|
||||||
|
async function init() {
|
||||||
|
dashboardStore.setLayer(route.params.layer);
|
||||||
|
dashboardStore.setEntity(route.params.entity);
|
||||||
|
const { auto, autoPeriod } = config.value;
|
||||||
|
|
||||||
|
if (auto) {
|
||||||
|
await setDuration();
|
||||||
|
appStoreWithOut.setReloadTimer(setInterval(setDuration, autoPeriod * 1000));
|
||||||
|
}
|
||||||
|
await setSelector();
|
||||||
|
await queryMetrics();
|
||||||
|
}
|
||||||
|
async function setDuration() {
|
||||||
|
const dates: Date[] = [new Date(new Date().getTime() - config.value.auto), new Date()];
|
||||||
|
|
||||||
|
appStoreWithOut.setDuration(timeFormat(dates));
|
||||||
|
}
|
||||||
|
async function setSelector() {
|
||||||
|
const { serviceId, podId, processId, destServiceId, destPodId, destProcessId, entity } = route.params;
|
||||||
|
if (serviceId) {
|
||||||
|
await selectorStore.getService(serviceId);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[EntityType[4].value, EntityType[5].value, EntityType[6].value, EntityType[7].value].includes(
|
||||||
|
entity as string,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await selectorStore.getService(destServiceId, true);
|
||||||
|
}
|
||||||
|
if ([EntityType[3].value, EntityType[5].value, EntityType[7].value].includes(entity as string)) {
|
||||||
|
await selectorStore.getInstance(podId);
|
||||||
|
}
|
||||||
|
if ([EntityType[2].value, EntityType[6].value].includes(entity as string)) {
|
||||||
|
await selectorStore.getEndpoint(podId);
|
||||||
|
}
|
||||||
|
if (EntityType[6].value === entity) {
|
||||||
|
await selectorStore.getEndpoint(destPodId, true);
|
||||||
|
}
|
||||||
|
if ([EntityType[5].value, EntityType[7].value].includes(entity as string)) {
|
||||||
|
await selectorStore.getInstance(destPodId, true);
|
||||||
|
}
|
||||||
|
if (EntityType[7].value === entity) {
|
||||||
|
selectorStore.getProcess(processId);
|
||||||
|
selectorStore.getProcess(destProcessId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function queryMetrics() {
|
||||||
|
const metricTypes = config.value.metricTypes || [];
|
||||||
|
const metrics = config.value.metrics || [];
|
||||||
|
const catalog = await useGetMetricEntity(metrics[0], metricTypes[0]);
|
||||||
|
const params = await useQueryProcessor({ ...config.value, catalog });
|
||||||
|
if (!params) {
|
||||||
|
source.value = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
const json = await dashboardStore.fetchMetricValue(params);
|
||||||
|
loading.value = false;
|
||||||
|
if (!json) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const d = {
|
||||||
|
metrics: config.value.metrics || [],
|
||||||
|
metricTypes: config.value.metricTypes || [],
|
||||||
|
metricConfig: config.value.metricConfig || [],
|
||||||
|
};
|
||||||
|
source.value = useSourceProcessor(json, d);
|
||||||
|
}
|
||||||
|
watch(
|
||||||
|
() => appStoreWithOut.durationTime,
|
||||||
|
() => {
|
||||||
|
queryMetrics();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
t,
|
||||||
|
graph,
|
||||||
|
source,
|
||||||
|
appStoreWithOut,
|
||||||
|
config,
|
||||||
|
title,
|
||||||
|
tips,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.content {
|
||||||
|
min-width: 100px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
background-color: #fff;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-chart {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0px 1px 4px 0px #00000029;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
height: 25px;
|
||||||
|
line-height: 25px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: aliceblue;
|
||||||
|
font-size: 12px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
165
src/views/dashboard/components/WidgetLink.vue
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<!-- 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. -->
|
||||||
|
<template>
|
||||||
|
<div class="link-content">
|
||||||
|
<div>
|
||||||
|
<label>{{ t("setDuration") }}</label>
|
||||||
|
<el-switch v-model="hasDuration" />
|
||||||
|
</div>
|
||||||
|
<div v-if="hasDuration">
|
||||||
|
<label>{{ t("duration") }}</label>
|
||||||
|
<TimePicker
|
||||||
|
:value="[appStore.durationRow.start, appStore.durationRow.end]"
|
||||||
|
position="right"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
@input="changeTimeRange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="!hasDuration">
|
||||||
|
<label>{{ t("auto") }}</label>
|
||||||
|
<el-switch class="mr-5" v-model="auto" style="height: 25px" />
|
||||||
|
<Selector v-model="freshOpt" :options="RefreshOptions" size="small" />
|
||||||
|
<div class="mt-5">
|
||||||
|
<label>{{ t("period") }}</label>
|
||||||
|
<el-input class="auto-period" size="small" type="number" v-model="period" min="1" />
|
||||||
|
<span class="ml-5">{{ t("second") }}</span>
|
||||||
|
<i class="ml-10">{{ t("timeReload") }}</i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button size="small" type="primary" class="mt-20" @click="getLink">{{ t("generateLink") }}</el-button>
|
||||||
|
<div v-show="widgetLink" class="mt-10">
|
||||||
|
<span @click="viewPage" class="link">
|
||||||
|
{{ host + widgetLink }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<Icon class="cp ml-10" iconName="copy" @click="copyLink" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||||
|
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||||
|
import { useSelectorStore } from "@/store/modules/selectors";
|
||||||
|
import router from "@/router";
|
||||||
|
import copy from "@/utils/copy";
|
||||||
|
import { RefreshOptions } from "@/views/dashboard/data";
|
||||||
|
import { TimeType } from "@/constants/data";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const appStore = useAppStoreWithOut();
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
const selectorStore = useSelectorStore();
|
||||||
|
const hasDuration = ref<boolean>(false);
|
||||||
|
const widgetLink = ref<string>("");
|
||||||
|
const dates = ref<Date[]>([]);
|
||||||
|
const host = window.location.host;
|
||||||
|
const auto = ref<boolean>(true);
|
||||||
|
const period = ref<number>(6);
|
||||||
|
const freshOpt = ref<string>(RefreshOptions[0].value);
|
||||||
|
|
||||||
|
function changeTimeRange(val: Date[] | any) {
|
||||||
|
dates.value = val;
|
||||||
|
}
|
||||||
|
function getLink() {
|
||||||
|
if (!dashboardStore.selectedGrid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const serviceId = selectorStore.currentService ? selectorStore.currentService.id : null;
|
||||||
|
const podId = selectorStore.currentPod ? selectorStore.currentPod.id : null;
|
||||||
|
const processId = selectorStore.currentProcess ? selectorStore.currentProcess.id : null;
|
||||||
|
const destServiceId = selectorStore.currentDestService ? selectorStore.currentDestService.id : null;
|
||||||
|
const destPodId = selectorStore.currentDestPod ? selectorStore.currentDestPod.id : null;
|
||||||
|
const destProcessId = selectorStore.currentDestProcess ? selectorStore.currentDestProcess.id : null;
|
||||||
|
const duration = JSON.stringify({
|
||||||
|
start: dates.value[0] ? new Date(dates.value[0]).getTime() : appStore.durationRow.start.getTime(),
|
||||||
|
end: dates.value[1] ? new Date(dates.value[1]).getTime() : appStore.durationRow.end.getTime(),
|
||||||
|
step: appStore.durationRow.step,
|
||||||
|
utc: appStore.utc,
|
||||||
|
});
|
||||||
|
const { widget, graph, metrics, metricTypes, metricConfig } = dashboardStore.selectedGrid;
|
||||||
|
const c = (metricConfig || []).map((d: any) => {
|
||||||
|
const t: any = {};
|
||||||
|
if (d.label) {
|
||||||
|
t.label = encodeURIComponent(d.label);
|
||||||
|
}
|
||||||
|
if (d.unit) {
|
||||||
|
t.unit = encodeURIComponent(d.unit);
|
||||||
|
}
|
||||||
|
return { ...d, ...t };
|
||||||
|
});
|
||||||
|
const opt: any = {
|
||||||
|
type: dashboardStore.selectedGrid.type,
|
||||||
|
graph: graph,
|
||||||
|
metrics: metrics,
|
||||||
|
metricTypes: metricTypes,
|
||||||
|
metricConfig: c,
|
||||||
|
height: dashboardStore.selectedGrid.h * 20 + 60,
|
||||||
|
};
|
||||||
|
if (widget) {
|
||||||
|
opt.widget = {
|
||||||
|
title: encodeURIComponent(widget.title || ""),
|
||||||
|
tips: encodeURIComponent(widget.tips || ""),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (auto.value) {
|
||||||
|
const f = RefreshOptions.filter((d: { value: string }) => d.value === freshOpt.value)[0] || {};
|
||||||
|
opt.auto = Number(f.value) * 60 * 1000;
|
||||||
|
opt.autoPeriod = period.value;
|
||||||
|
if (f.step === TimeType.HOUR_TIME) {
|
||||||
|
opt.auto = Number(f.value) * 60 * 60 * 1000;
|
||||||
|
}
|
||||||
|
if (f.step === TimeType.DAY_TIME) {
|
||||||
|
opt.auto = Number(f.value) * 60 * 60 * 60 * 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const config = JSON.stringify(opt);
|
||||||
|
const path = `/page/${dashboardStore.layerId}/${
|
||||||
|
dashboardStore.entity
|
||||||
|
}/${serviceId}/${podId}/${processId}/${destServiceId}/${destPodId}/${destProcessId}/${encodeURIComponent(config)}`;
|
||||||
|
widgetLink.value = hasDuration.value ? `${path}/${encodeURIComponent(duration)}` : path;
|
||||||
|
}
|
||||||
|
function viewPage() {
|
||||||
|
const routeUrl = router.resolve({ path: widgetLink.value });
|
||||||
|
window.open(routeUrl.href, "_blank");
|
||||||
|
}
|
||||||
|
function copyLink() {
|
||||||
|
copy(host + widgetLink.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.link {
|
||||||
|
color: #409eff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-content {
|
||||||
|
height: 300px;
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: auto;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-period {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
89
src/views/dashboard/configuration/ThirdPartyApp.vue
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<!-- 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. -->
|
||||||
|
<template>
|
||||||
|
<div class="item">
|
||||||
|
<span class="label">{{ t("iframeSrc") }}</span>
|
||||||
|
<el-input class="input" v-model="url" size="small" @change="changeConfig({ url: encodeURIComponent(url) })" />
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<el-button size="small" @click="cancelConfig">
|
||||||
|
{{ t("cancel") }}
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" type="primary" @click="applyConfig">
|
||||||
|
{{ t("apply") }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||||
|
const { t } = useI18n();
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
const originConfig = dashboardStore.selectedGrid;
|
||||||
|
const widget = originConfig.widget || {};
|
||||||
|
const url = ref(widget.url || "");
|
||||||
|
function changeConfig(param: { [key: string]: string }) {
|
||||||
|
const key = Object.keys(param)[0];
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { selectedGrid } = dashboardStore;
|
||||||
|
const widget = {
|
||||||
|
...dashboardStore.selectedGrid.widget,
|
||||||
|
[key]: decodeURIComponent(param[key]),
|
||||||
|
};
|
||||||
|
dashboardStore.selectWidget({ ...selectedGrid, widget });
|
||||||
|
}
|
||||||
|
function applyConfig() {
|
||||||
|
dashboardStore.setConfigPanel(false);
|
||||||
|
dashboardStore.setConfigs(dashboardStore.selectedGrid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelConfig() {
|
||||||
|
dashboardStore.selectWidget(originConfig);
|
||||||
|
dashboardStore.setConfigPanel(false);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.slider {
|
||||||
|
width: 500px;
|
||||||
|
margin-top: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: right;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
</style>
|
@ -20,6 +20,7 @@ import Widget from "./Widget.vue";
|
|||||||
import Topology from "./Topology.vue";
|
import Topology from "./Topology.vue";
|
||||||
import Event from "./Event.vue";
|
import Event from "./Event.vue";
|
||||||
import TimeRange from "./TimeRange.vue";
|
import TimeRange from "./TimeRange.vue";
|
||||||
|
import ThirdPartyApp from "./ThirdPartyApp.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Text,
|
Text,
|
||||||
@ -27,4 +28,5 @@ export default {
|
|||||||
Topology,
|
Topology,
|
||||||
Event,
|
Event,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
|
ThirdPartyApp,
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License. -->
|
limitations under the License. -->
|
||||||
<template>
|
<template>
|
||||||
<div class="topology">
|
<div class="text">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<el-popover placement="bottom" trigger="click" :width="100" v-if="dashboardStore.editMode">
|
<el-popover placement="bottom" trigger="click" :width="100" v-if="dashboardStore.editMode">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
@ -77,7 +77,7 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.topology {
|
.text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
116
src/views/dashboard/controls/ThirdPartyApp.vue
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<!-- 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. -->
|
||||||
|
<template>
|
||||||
|
<div class="text">
|
||||||
|
<div class="header">
|
||||||
|
<el-popover placement="bottom" trigger="click" :width="100" v-if="dashboardStore.editMode">
|
||||||
|
<template #reference>
|
||||||
|
<span>
|
||||||
|
<Icon iconName="ellipsis_v" size="middle" class="operation" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<div class="tools" @click="editConfig">
|
||||||
|
<span>{{ t("edit") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="tools" @click="removeTopo">
|
||||||
|
<span>{{ t("delete") }}</span>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<iframe
|
||||||
|
v-if="widget.url"
|
||||||
|
:src="widget.url"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
scrolling="no"
|
||||||
|
style="border: none"
|
||||||
|
></iframe>
|
||||||
|
<div v-else class="tips">{{ t("iframeWidgetTip") }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import type { PropType } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||||
|
|
||||||
|
/*global defineProps */
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object as PropType<any>,
|
||||||
|
default: () => ({ graph: {} }),
|
||||||
|
},
|
||||||
|
activeIndex: { type: String, default: "" },
|
||||||
|
});
|
||||||
|
const { t } = useI18n();
|
||||||
|
const dashboardStore = useDashboardStore();
|
||||||
|
const widget = computed(() => props.data.widget || {});
|
||||||
|
|
||||||
|
function removeTopo() {
|
||||||
|
dashboardStore.removeControls(props.data);
|
||||||
|
}
|
||||||
|
function editConfig() {
|
||||||
|
dashboardStore.setConfigPanel(true);
|
||||||
|
dashboardStore.selectWidget(props.data);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.text {
|
||||||
|
font-size: 12px;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools {
|
||||||
|
padding: 5px 0;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409eff;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
@ -59,7 +59,7 @@ limitations under the License. -->
|
|||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.topology {
|
.topology {
|
||||||
background-color: #333840;
|
// background-color: #333840;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
@ -26,18 +26,21 @@ limitations under the License. -->
|
|||||||
<Icon iconName="info_outline" size="sm" class="operation" v-show="widget.tips" />
|
<Icon iconName="info_outline" size="sm" class="operation" v-show="widget.tips" />
|
||||||
</span>
|
</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-popover placement="bottom" trigger="click" :width="100" v-if="dashboardStore.editMode">
|
<el-popover placement="bottom" trigger="click" :width="100">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<span>
|
<span>
|
||||||
<Icon iconName="ellipsis_v" size="middle" class="operation" />
|
<Icon iconName="ellipsis_v" size="middle" class="operation" />
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<div class="tools" @click="editConfig">
|
<div class="tools" @click="editConfig" v-if="dashboardStore.editMode">
|
||||||
<span>{{ t("edit") }}</span>
|
<span>{{ t("edit") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="tools" @click="removeWidget">
|
<div class="tools" @click="removeWidget" v-if="dashboardStore.editMode">
|
||||||
<span>{{ t("delete") }}</span>
|
<span>{{ t("delete") }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tools" @click="generateLink">
|
||||||
|
<span>{{ t("generateLink") }}</span>
|
||||||
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -161,6 +164,10 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function generateLink() {
|
||||||
|
dashboardStore.setWidgetLink(true);
|
||||||
|
dashboardStore.selectWidget(props.data);
|
||||||
|
}
|
||||||
watch(
|
watch(
|
||||||
() => [props.data.metricTypes, props.data.metrics],
|
() => [props.data.metricTypes, props.data.metrics],
|
||||||
() => {
|
() => {
|
||||||
@ -227,6 +234,7 @@ limitations under the License. -->
|
|||||||
state,
|
state,
|
||||||
appStore,
|
appStore,
|
||||||
removeWidget,
|
removeWidget,
|
||||||
|
generateLink,
|
||||||
editConfig,
|
editConfig,
|
||||||
data,
|
data,
|
||||||
loading,
|
loading,
|
||||||
|
@ -26,6 +26,7 @@ import DemandLog from "./DemandLog.vue";
|
|||||||
import Event from "./Event.vue";
|
import Event from "./Event.vue";
|
||||||
import NetworkProfiling from "./NetworkProfiling.vue";
|
import NetworkProfiling from "./NetworkProfiling.vue";
|
||||||
import TimeRange from "./TimeRange.vue";
|
import TimeRange from "./TimeRange.vue";
|
||||||
|
import ThirdPartyApp from "./ThirdPartyApp.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Tab,
|
Tab,
|
||||||
@ -40,4 +41,5 @@ export default {
|
|||||||
Event,
|
Event,
|
||||||
NetworkProfiling,
|
NetworkProfiling,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
|
ThirdPartyApp,
|
||||||
};
|
};
|
||||||
|
@ -25,6 +25,7 @@ import DemandLog from "./DemandLog.vue";
|
|||||||
import Event from "./Event.vue";
|
import Event from "./Event.vue";
|
||||||
import NetworkProfiling from "./NetworkProfiling.vue";
|
import NetworkProfiling from "./NetworkProfiling.vue";
|
||||||
import TimeRange from "./TimeRange.vue";
|
import TimeRange from "./TimeRange.vue";
|
||||||
|
import ThirdPartyApp from "./ThirdPartyApp.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Widget,
|
Widget,
|
||||||
@ -38,4 +39,5 @@ export default {
|
|||||||
Event,
|
Event,
|
||||||
NetworkProfiling,
|
NetworkProfiling,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
|
ThirdPartyApp,
|
||||||
};
|
};
|
||||||
|
@ -184,6 +184,7 @@ export const AllTools = [
|
|||||||
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
|
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
|
||||||
{ name: "merge", content: "Add Trace", id: "addTrace" },
|
{ name: "merge", content: "Add Trace", id: "addTrace" },
|
||||||
{ name: "assignment", content: "Add Log", id: "addLog" },
|
{ name: "assignment", content: "Add Log", id: "addLog" },
|
||||||
|
{ name: "add_iframe", content: "Add Iframe", id: "addIframe" },
|
||||||
];
|
];
|
||||||
export const ServiceTools = [
|
export const ServiceTools = [
|
||||||
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
||||||
@ -196,6 +197,7 @@ export const ServiceTools = [
|
|||||||
{ name: "assignment", content: "Add Log", id: "addLog" },
|
{ name: "assignment", content: "Add Log", id: "addLog" },
|
||||||
{ name: "demand", content: "Add On Demand Log", id: "addDemandLog" },
|
{ name: "demand", content: "Add On Demand Log", id: "addDemandLog" },
|
||||||
{ name: "event", content: "Add Event", id: "addEvent" },
|
{ name: "event", content: "Add Event", id: "addEvent" },
|
||||||
|
{ name: "add_iframe", content: "Add Iframe", id: "addIframe" },
|
||||||
];
|
];
|
||||||
export const InstanceTools = [
|
export const InstanceTools = [
|
||||||
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
||||||
@ -205,6 +207,7 @@ export const InstanceTools = [
|
|||||||
{ name: "assignment", content: "Add Log", id: "addLog" },
|
{ name: "assignment", content: "Add Log", id: "addLog" },
|
||||||
{ name: "demand", content: "Add On Demand Log", id: "addDemandLog" },
|
{ name: "demand", content: "Add On Demand Log", id: "addDemandLog" },
|
||||||
{ name: "event", content: "Add Event", id: "addEvent" },
|
{ name: "event", content: "Add Event", id: "addEvent" },
|
||||||
|
{ name: "add_iframe", content: "Add Iframe", id: "addIframe" },
|
||||||
{
|
{
|
||||||
name: "timeline",
|
name: "timeline",
|
||||||
content: "Add Network Profiling",
|
content: "Add Network Profiling",
|
||||||
@ -218,31 +221,36 @@ export const EndpointTools = [
|
|||||||
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
|
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
|
||||||
{ name: "merge", content: "Add Trace", id: "addTrace" },
|
{ name: "merge", content: "Add Trace", id: "addTrace" },
|
||||||
{ name: "assignment", content: "Add Log", id: "addLog" },
|
{ name: "assignment", content: "Add Log", id: "addLog" },
|
||||||
{ name: "event", content: "Add Event", id: "addEvent" },
|
{ name: "event", content: "Add Event", id: "c" },
|
||||||
|
{ name: "add_iframe", content: "Add Iframe", id: "addIframe" },
|
||||||
];
|
];
|
||||||
export const ProcessTools = [
|
export const ProcessTools = [
|
||||||
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
||||||
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
|
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
|
||||||
{ name: "library_books", content: "Add Text", id: "addText" },
|
{ name: "library_books", content: "Add Text", id: "addText" },
|
||||||
{ name: "time_range", content: "Add Time Range Text", id: "addTimeRange" },
|
{ name: "time_range", content: "Add Time Range Text", id: "addTimeRange" },
|
||||||
|
{ name: "add_iframe", content: "Add Iframe", id: "addIframe" },
|
||||||
];
|
];
|
||||||
export const ServiceRelationTools = [
|
export const ServiceRelationTools = [
|
||||||
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
||||||
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
|
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
|
||||||
{ name: "library_books", content: "Add Text", id: "addText" },
|
{ name: "library_books", content: "Add Text", id: "addText" },
|
||||||
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
|
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
|
||||||
|
{ name: "add_iframe", content: "Add Iframe", id: "addIframe" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const EndpointRelationTools = [
|
export const EndpointRelationTools = [
|
||||||
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
||||||
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
|
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
|
||||||
{ name: "library_books", content: "Add Text", id: "addText" },
|
{ name: "library_books", content: "Add Text", id: "addText" },
|
||||||
|
{ name: "add_iframe", content: "Add Iframe", id: "addIframe" },
|
||||||
];
|
];
|
||||||
export const InstanceRelationTools = [
|
export const InstanceRelationTools = [
|
||||||
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
|
||||||
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
|
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
|
||||||
{ name: "library_books", content: "Add Text", id: "addText" },
|
{ name: "library_books", content: "Add Text", id: "addText" },
|
||||||
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
|
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
|
||||||
|
{ name: "add_iframe", content: "Add Iframe", id: "addIframe" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ScopeType = [
|
export const ScopeType = [
|
||||||
@ -301,7 +309,6 @@ export const CalculationOpts = [
|
|||||||
value: "convertMilliseconds",
|
value: "convertMilliseconds",
|
||||||
},
|
},
|
||||||
{ label: "Seconds to YYYY-MM-DD HH:mm:ss", value: "convertSeconds" },
|
{ label: "Seconds to YYYY-MM-DD HH:mm:ss", value: "convertSeconds" },
|
||||||
{ label: "Precision is 2", value: "precision" },
|
|
||||||
{ label: "Milliseconds to seconds", value: "msTos" },
|
{ label: "Milliseconds to seconds", value: "msTos" },
|
||||||
{ label: "Seconds to days", value: "secondToDay" },
|
{ label: "Seconds to days", value: "secondToDay" },
|
||||||
{ label: "Nanoseconds to milliseconds", value: "nanosecondToMillisecond" },
|
{ label: "Nanoseconds to milliseconds", value: "nanosecondToMillisecond" },
|
||||||
@ -310,3 +317,8 @@ export const RefIdTypes = [
|
|||||||
{ label: "Trace ID", value: "traceId" },
|
{ label: "Trace ID", value: "traceId" },
|
||||||
{ label: "None", value: "none" },
|
{ label: "None", value: "none" },
|
||||||
];
|
];
|
||||||
|
export const RefreshOptions = [
|
||||||
|
{ label: "Last 30 minutes", value: "30", step: "MINUTE" },
|
||||||
|
{ label: "Last 8 hours", value: "8", step: "HOUR" },
|
||||||
|
{ label: "Last 7 days", value: "7", step: "DAY" },
|
||||||
|
];
|
||||||
|
@ -74,12 +74,14 @@ limitations under the License. -->
|
|||||||
return {
|
return {
|
||||||
color,
|
color,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: "none",
|
trigger: "axis",
|
||||||
axisPointer: {
|
textStyle: {
|
||||||
type: "cross",
|
fontSize: 12,
|
||||||
color: "#333",
|
color: "#333",
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.8)",
|
|
||||||
},
|
},
|
||||||
|
enterable: true,
|
||||||
|
confine: true,
|
||||||
|
extraCssText: "max-height: 300px; overflow: auto; border: none;",
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
type: "scroll",
|
type: "scroll",
|
||||||
@ -99,12 +101,6 @@ limitations under the License. -->
|
|||||||
bottom: 5,
|
bottom: 5,
|
||||||
containLabel: true,
|
containLabel: true,
|
||||||
},
|
},
|
||||||
axisPointer: {
|
|
||||||
label: {
|
|
||||||
color: "#333",
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.8)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: "category",
|
type: "category",
|
||||||
axisTick: {
|
axisTick: {
|
||||||
|
@ -22,7 +22,7 @@ limitations under the License. -->
|
|||||||
justifyContent: config.textAlign || 'center',
|
justifyContent: config.textAlign || 'center',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ singleVal.toFixed(2) }}
|
{{ singleVal }}
|
||||||
<span class="unit" v-show="config.showUnit && unit">
|
<span class="unit" v-show="config.showUnit && unit">
|
||||||
{{ decodeURIComponent(unit) }}
|
{{ decodeURIComponent(unit) }}
|
||||||
</span>
|
</span>
|
||||||
@ -38,7 +38,7 @@ limitations under the License. -->
|
|||||||
/*global defineProps */
|
/*global defineProps */
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
type: Object as PropType<{ [key: string]: number }>,
|
type: Object as PropType<{ [key: string]: any }>,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
config: {
|
config: {
|
||||||
@ -54,7 +54,9 @@ limitations under the License. -->
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const metricConfig = computed(() => props.config.metricConfig || []);
|
const metricConfig = computed(() => props.config.metricConfig || []);
|
||||||
const key = computed(() => Object.keys(props.data)[0]);
|
const key = computed(() => Object.keys(props.data)[0]);
|
||||||
const singleVal = computed(() => Number(props.data[key.value]));
|
const singleVal = computed(() =>
|
||||||
|
Array.isArray(props.data[key.value]) ? props.data[key.value][0] : props.data[key.value],
|
||||||
|
);
|
||||||
const unit = computed(() => metricConfig.value[0] && encodeURIComponent(metricConfig.value[0].unit || ""));
|
const unit = computed(() => metricConfig.value[0] && encodeURIComponent(metricConfig.value[0].unit || ""));
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -91,19 +91,13 @@ limitations under the License. -->
|
|||||||
});
|
});
|
||||||
const color: string[] = chartColors(keys);
|
const color: string[] = chartColors(keys);
|
||||||
const tooltip = {
|
const tooltip = {
|
||||||
trigger: "none",
|
trigger: "axis",
|
||||||
axisPointer: {
|
textStyle: {
|
||||||
type: "cross",
|
fontSize: 12,
|
||||||
color: "#333",
|
color: "#333",
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.8)",
|
|
||||||
},
|
},
|
||||||
// trigger: "axis",
|
enterable: true,
|
||||||
// textStyle: {
|
confine: true,
|
||||||
// fontSize: 12,
|
|
||||||
// color: "#333",
|
|
||||||
// },
|
|
||||||
// enterable: true,
|
|
||||||
// confine: true,
|
|
||||||
extraCssText: "max-height: 300px; overflow: auto; border: none;",
|
extraCssText: "max-height: 300px; overflow: auto; border: none;",
|
||||||
};
|
};
|
||||||
const tips = {
|
const tips = {
|
||||||
@ -133,12 +127,6 @@ limitations under the License. -->
|
|||||||
color: props.theme === "dark" ? "#fff" : "#333",
|
color: props.theme === "dark" ? "#fff" : "#333",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
axisPointer: {
|
|
||||||
label: {
|
|
||||||
color: "#333",
|
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.8)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
grid: {
|
||||||
top: showEchartsLegend(keys) ? 35 : 10,
|
top: showEchartsLegend(keys) ? 35 : 10,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
@ -64,7 +64,7 @@ limitations under the License. -->
|
|||||||
import copy from "@/utils/copy";
|
import copy from "@/utils/copy";
|
||||||
import { TextColors } from "@/views/dashboard/data";
|
import { TextColors } from "@/views/dashboard/data";
|
||||||
import Trace from "@/views/dashboard/related/trace/Index.vue";
|
import Trace from "@/views/dashboard/related/trace/Index.vue";
|
||||||
import { QueryOrders, Status, RefIdTypes } from "../data";
|
import { QueryOrders, Status, RefIdTypes, ProtocolTypes } from "../data";
|
||||||
/*global defineProps */
|
/*global defineProps */
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
@ -77,6 +77,7 @@ limitations under the License. -->
|
|||||||
type: Object as PropType<{
|
type: Object as PropType<{
|
||||||
color: string;
|
color: string;
|
||||||
metrics: string[];
|
metrics: string[];
|
||||||
|
metricTypes: string[];
|
||||||
relatedTrace: any;
|
relatedTrace: any;
|
||||||
}>,
|
}>,
|
||||||
default: () => ({ color: "purple" }),
|
default: () => ({ color: "purple" }),
|
||||||
@ -112,6 +113,7 @@ limitations under the License. -->
|
|||||||
status: Status[2].value,
|
status: Status[2].value,
|
||||||
id: item.id || item.name,
|
id: item.id || item.name,
|
||||||
metricValue: [{ label: props.config.metrics[0], data: item.value, value: item.name }],
|
metricValue: [{ label: props.config.metrics[0], data: item.value, value: item.name }],
|
||||||
|
isReadRecords: props.config.metricTypes.includes(ProtocolTypes.ReadRecords) || undefined,
|
||||||
};
|
};
|
||||||
traceOptions.value = {
|
traceOptions.value = {
|
||||||
...traceOptions.value,
|
...traceOptions.value,
|
||||||
|
@ -448,6 +448,9 @@ limitations under the License. -->
|
|||||||
case "addTimeRange":
|
case "addTimeRange":
|
||||||
dashboardStore.addTabControls("TimeRange");
|
dashboardStore.addTabControls("TimeRange");
|
||||||
break;
|
break;
|
||||||
|
case "addIframe":
|
||||||
|
dashboardStore.addTabControls("ThirdPartyApp");
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
ElMessage.info("Don't support this control");
|
ElMessage.info("Don't support this control");
|
||||||
break;
|
break;
|
||||||
@ -492,6 +495,9 @@ limitations under the License. -->
|
|||||||
case "addTimeRange":
|
case "addTimeRange":
|
||||||
dashboardStore.addControl("TimeRange");
|
dashboardStore.addControl("TimeRange");
|
||||||
break;
|
break;
|
||||||
|
case "addIframe":
|
||||||
|
dashboardStore.addControl("ThirdPartyApp");
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
dashboardStore.addControl("Widget");
|
dashboardStore.addControl("Widget");
|
||||||
}
|
}
|
||||||
|
@ -128,7 +128,7 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
|
|
||||||
function processTree(arr: StackElement[]) {
|
function processTree(arr: StackElement[]) {
|
||||||
const copyArr = (window as any).structuredClone(arr);
|
const copyArr = JSON.parse(JSON.stringify(arr));
|
||||||
const obj: any = {};
|
const obj: any = {};
|
||||||
let res = null;
|
let res = null;
|
||||||
for (const item of copyArr) {
|
for (const item of copyArr) {
|
||||||
|
@ -319,6 +319,9 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
logStore.resetState();
|
logStore.resetState();
|
||||||
|
const config = props.data;
|
||||||
|
delete config.filters;
|
||||||
|
dashboardStore.setWidget(config);
|
||||||
});
|
});
|
||||||
watch(
|
watch(
|
||||||
() => selectorStore.currentService,
|
() => selectorStore.currentService,
|
||||||
@ -340,6 +343,7 @@ limitations under the License. -->
|
|||||||
watch(
|
watch(
|
||||||
() => appStore.durationTime,
|
() => appStore.durationTime,
|
||||||
() => {
|
() => {
|
||||||
|
duration.value = appStore.durationTime;
|
||||||
if (dashboardStore.entity === EntityType[1].value) {
|
if (dashboardStore.entity === EntityType[1].value) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ limitations under the License. -->
|
|||||||
class="content mb-10"
|
class="content mb-10"
|
||||||
:readonly="true"
|
:readonly="true"
|
||||||
v-else-if="item.label === 'content'"
|
v-else-if="item.label === 'content'"
|
||||||
:value="currentLog[item.label]"
|
:value="contentFormat(item.label)"
|
||||||
/>
|
/>
|
||||||
<span v-else-if="item.label === 'tags'" class="g-sm-8 mb-10">
|
<span v-else-if="item.label === 'tags'" class="g-sm-8 mb-10">
|
||||||
<div v-for="(d, index) in logTags" :key="index">{{ d }}</div>
|
<div v-for="(d, index) in logTags" :key="index">{{ d }}</div>
|
||||||
@ -38,6 +38,7 @@ limitations under the License. -->
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import type { Option } from "@/types/app";
|
import type { Option } from "@/types/app";
|
||||||
import { dateFormat } from "@/utils/dateFormat";
|
import { dateFormat } from "@/utils/dateFormat";
|
||||||
|
import { formatJson } from "@/utils/formatJson";
|
||||||
|
|
||||||
/*global defineProps */
|
/*global defineProps */
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -53,6 +54,16 @@ limitations under the License. -->
|
|||||||
return `${d.key} = ${d.value}`;
|
return `${d.key} = ${d.value}`;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function contentFormat(label: string) {
|
||||||
|
try {
|
||||||
|
return props.currentLog.contentType === "JSON"
|
||||||
|
? formatJson(JSON.parse(props.currentLog[label]))
|
||||||
|
: props.currentLog[label];
|
||||||
|
} catch (e) {
|
||||||
|
return props.currentLog[label];
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.content {
|
.content {
|
||||||
|
@ -104,7 +104,6 @@ limitations under the License. -->
|
|||||||
() => selectorStore.currentService,
|
() => selectorStore.currentService,
|
||||||
() => {
|
() => {
|
||||||
searchTasks();
|
searchTasks();
|
||||||
console.log("service");
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
watch(
|
watch(
|
||||||
|
@ -20,11 +20,11 @@ limitations under the License. -->
|
|||||||
{{ t("noData") }}
|
{{ t("noData") }}
|
||||||
</div>
|
</div>
|
||||||
<table class="profile-t">
|
<table class="profile-t">
|
||||||
<tr class="profile-tr cp" v-for="(i, index) in profileStore.segmentList" @click="selectTrace(i)" :key="index">
|
<tr class="profile-tr cp" v-for="(i, index) in profileStore.segmentList" @click="selectSegment(i)" :key="index">
|
||||||
<td
|
<td
|
||||||
class="profile-td"
|
class="profile-td"
|
||||||
:class="{
|
:class="{
|
||||||
selected: selectedKey == i.segmentId,
|
selected: key === i.spans[0].segmentId,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -47,25 +47,25 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from "vue";
|
import { computed } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useProfileStore } from "@/store/modules/profile";
|
import { useProfileStore } from "@/store/modules/profile";
|
||||||
import type { Trace } from "@/types/trace";
|
import type { Trace } from "@/types/trace";
|
||||||
import { ElMessage } from "element-plus";
|
|
||||||
import { dateFormat } from "@/utils/dateFormat";
|
import { dateFormat } from "@/utils/dateFormat";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const profileStore = useProfileStore();
|
const profileStore = useProfileStore();
|
||||||
const selectedKey = ref<string>("");
|
const key = computed(
|
||||||
|
() =>
|
||||||
|
(profileStore.currentSegment &&
|
||||||
|
profileStore.currentSegment.spans.length &&
|
||||||
|
profileStore.currentSegment.spans[0].segmentId) ||
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
async function selectTrace(item: Trace) {
|
async function selectSegment(item: Trace) {
|
||||||
profileStore.setCurrentSegment(item);
|
profileStore.setCurrentSegment(item);
|
||||||
selectedKey.value = item.segmentId;
|
profileStore.setSegmentSpans(item.spans);
|
||||||
const res = await profileStore.getSegmentSpans({ segmentId: item.segmentId });
|
|
||||||
|
|
||||||
if (res.errors) {
|
|
||||||
ElMessage.error(res.errors);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -15,14 +15,8 @@ limitations under the License. -->
|
|||||||
<template>
|
<template>
|
||||||
<div class="profile-trace-dashboard" v-if="profileStore.currentSegment">
|
<div class="profile-trace-dashboard" v-if="profileStore.currentSegment">
|
||||||
<div class="profile-trace-detail-wrapper">
|
<div class="profile-trace-detail-wrapper">
|
||||||
<Selector
|
<label>Trace ID</label>
|
||||||
size="small"
|
<el-input class="input mr-10 ml-5" readonly :value="profileStore.currentSegment.traceId" size="small" />
|
||||||
:value="traceId || (traceIds[0] && traceIds[0].value) || ''"
|
|
||||||
:options="traceIds"
|
|
||||||
placeholder="Select a trace id"
|
|
||||||
@change="changeTraceId"
|
|
||||||
class="profile-trace-detail-ids mr-10"
|
|
||||||
/>
|
|
||||||
<Selector
|
<Selector
|
||||||
size="small"
|
size="small"
|
||||||
:value="mode"
|
:value="mode"
|
||||||
@ -31,14 +25,14 @@ limitations under the License. -->
|
|||||||
@change="spanModeChange"
|
@change="spanModeChange"
|
||||||
class="mr-10"
|
class="mr-10"
|
||||||
/>
|
/>
|
||||||
<el-button type="primary" size="small" @click="analyzeProfile()">
|
<el-button type="primary" size="small" :disabled="!profileStore.currentSpan.profiled" @click="analyzeProfile()">
|
||||||
{{ t("analyze") }}
|
{{ t("analyze") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-table">
|
<div class="profile-table">
|
||||||
<Table
|
<Table
|
||||||
:data="profileStore.segmentSpans"
|
:data="profileStore.segmentSpans"
|
||||||
:traceId="profileStore.currentSegment.traceIds && profileStore.currentSegment.traceIds[0]"
|
:traceId="profileStore.currentSegment.traceId"
|
||||||
:showBtnDetail="true"
|
:showBtnDetail="true"
|
||||||
headerType="profile"
|
headerType="profile"
|
||||||
@select="selectSpan"
|
@select="selectSpan"
|
||||||
@ -47,7 +41,7 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from "vue";
|
import { ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import Table from "../../trace/components/Table/Index.vue";
|
import Table from "../../trace/components/Table/Index.vue";
|
||||||
import { useProfileStore } from "@/store/modules/profile";
|
import { useProfileStore } from "@/store/modules/profile";
|
||||||
@ -64,13 +58,6 @@ limitations under the License. -->
|
|||||||
const mode = ref<string>("include");
|
const mode = ref<string>("include");
|
||||||
const message = ref<string>("");
|
const message = ref<string>("");
|
||||||
const timeRange = ref<Array<{ start: number; end: number }>>([]);
|
const timeRange = ref<Array<{ start: number; end: number }>>([]);
|
||||||
const traceId = ref<string>("");
|
|
||||||
const traceIds = computed(() =>
|
|
||||||
(profileStore.currentSegment.traceIds || []).map((id: string) => ({
|
|
||||||
label: id,
|
|
||||||
value: id,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
function selectSpan(span: Span) {
|
function selectSpan(span: Span) {
|
||||||
profileStore.setCurrentSpan(span);
|
profileStore.setCurrentSpan(span);
|
||||||
@ -81,17 +68,20 @@ limitations under the License. -->
|
|||||||
updateTimeRange();
|
updateTimeRange();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeTraceId(opt: Option[]) {
|
|
||||||
traceId.value = opt[0].value;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function analyzeProfile() {
|
async function analyzeProfile() {
|
||||||
|
if (!profileStore.currentSpan.profiled) {
|
||||||
|
ElMessage.info("It's a un-profiled span");
|
||||||
|
return;
|
||||||
|
}
|
||||||
emits("loading", true);
|
emits("loading", true);
|
||||||
updateTimeRange();
|
updateTimeRange();
|
||||||
const res = await profileStore.getProfileAnalyze({
|
const params = timeRange.value.map((t: { start: number; end: number }) => {
|
||||||
segmentId: profileStore.currentSegment.segmentId,
|
return {
|
||||||
timeRanges: timeRange.value,
|
segmentId: profileStore.currentSpan.segmentId,
|
||||||
|
timeRange: t,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
const res = await profileStore.getProfileAnalyze(params);
|
||||||
emits("loading", false);
|
emits("loading", false);
|
||||||
if (res.errors) {
|
if (res.errors) {
|
||||||
ElMessage.error(res.errors);
|
ElMessage.error(res.errors);
|
||||||
@ -175,4 +165,8 @@ limitations under the License. -->
|
|||||||
.profile-trace-detail-ids {
|
.profile-trace-detail-ids {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -25,7 +25,7 @@ limitations under the License. -->
|
|||||||
<td
|
<td
|
||||||
class="profile-td"
|
class="profile-td"
|
||||||
:class="{
|
:class="{
|
||||||
selected: selectedTask.id === i.id,
|
selected: profileStore.currentTask && profileStore.currentTask.id === i.id,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="ell">
|
<div class="ell">
|
||||||
@ -49,7 +49,7 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-dialog v-model="viewDetail" :destroy-on-close="true" fullscreen @closed="viewDetail = false">
|
<el-dialog v-model="viewDetail" :destroy-on-close="true" fullscreen @closed="viewDetail = false">
|
||||||
<div class="profile-detail flex-v">
|
<div class="profile-detail flex-v" v-if="profileStore.currentTask">
|
||||||
<div>
|
<div>
|
||||||
<h5 class="mb-10">{{ t("task") }}.</h5>
|
<h5 class="mb-10">{{ t("task") }}.</h5>
|
||||||
<div class="mb-10 clear item">
|
<div class="mb-10 clear item">
|
||||||
@ -58,33 +58,35 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-10 clear item">
|
<div class="mb-10 clear item">
|
||||||
<span class="g-sm-4 grey">{{ t("endpoint") }}:</span>
|
<span class="g-sm-4 grey">{{ t("endpoint") }}:</span>
|
||||||
<span class="g-sm-8 wba">{{ selectedTask.endpointName }}</span>
|
<span class="g-sm-8 wba">{{ profileStore.currentTask.endpointName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-10 clear item">
|
<div class="mb-10 clear item">
|
||||||
<span class="g-sm-4 grey">{{ t("monitorTime") }}:</span>
|
<span class="g-sm-4 grey">{{ t("monitorTime") }}:</span>
|
||||||
<span class="g-sm-8 wba">
|
<span class="g-sm-8 wba">
|
||||||
{{ dateFormat(selectedTask.startTime) }}
|
{{ dateFormat(profileStore.currentTask.startTime) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-10 clear item">
|
<div class="mb-10 clear item">
|
||||||
<span class="g-sm-4 grey">{{ t("monitorDuration") }}:</span
|
<span class="g-sm-4 grey">{{ t("monitorDuration") }}:</span
|
||||||
><span class="g-sm-8 wba">{{ selectedTask.duration }} min</span>
|
><span class="g-sm-8 wba">{{ profileStore.currentTask.duration }} min</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-10 clear item">
|
<div class="mb-10 clear item">
|
||||||
<span class="g-sm-4 grey">{{ t("minThreshold") }}:</span>
|
<span class="g-sm-4 grey">{{ t("minThreshold") }}:</span>
|
||||||
<span class="g-sm-8 wba"> {{ selectedTask.minDurationThreshold }} ms </span>
|
<span class="g-sm-8 wba"> {{ profileStore.currentTask.minDurationThreshold }} ms </span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-10 clear item">
|
<div class="mb-10 clear item">
|
||||||
<span class="g-sm-4 grey">{{ t("dumpPeriod") }}:</span>
|
<span class="g-sm-4 grey">{{ t("dumpPeriod") }}:</span>
|
||||||
<span class="g-sm-8 wba">{{ selectedTask.dumpPeriod }}</span>
|
<span class="g-sm-8 wba">{{ profileStore.currentTask.dumpPeriod }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-10 clear item">
|
<div class="mb-10 clear item">
|
||||||
<span class="g-sm-4 grey">{{ t("maxSamplingCount") }}:</span>
|
<span class="g-sm-4 grey">{{ t("maxSamplingCount") }}:</span>
|
||||||
<span class="g-sm-8 wba">{{ selectedTask.maxSamplingCount }}</span>
|
<span class="g-sm-8 wba">{{ profileStore.currentTask.maxSamplingCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5 class="mb-10 mt-10" v-show="selectedTask.logs && selectedTask.logs.length"> {{ t("logs") }}. </h5>
|
<h5 class="mb-10 mt-10" v-show="profileStore.currentTask.logs && profileStore.currentTask.logs.length">
|
||||||
|
{{ t("logs") }}.
|
||||||
|
</h5>
|
||||||
<div class="log-item" v-for="(i, index) in Object.keys(instanceLogs)" :key="index">
|
<div class="log-item" v-for="(i, index) in Object.keys(instanceLogs)" :key="index">
|
||||||
<div class="mb-10 sm">
|
<div class="mb-10 sm">
|
||||||
<span class="mr-10 grey">{{ t("instance") }}:</span>
|
<span class="mr-10 grey">{{ t("instance") }}:</span>
|
||||||
@ -115,12 +117,12 @@ limitations under the License. -->
|
|||||||
const selectorStore = useSelectorStore();
|
const selectorStore = useSelectorStore();
|
||||||
const viewDetail = ref<boolean>(false);
|
const viewDetail = ref<boolean>(false);
|
||||||
const service = ref<string>("");
|
const service = ref<string>("");
|
||||||
const selectedTask = ref<TaskListItem | Record<string, never>>({});
|
// const selectedTask = ref<TaskListItem | Record<string, never>>({});
|
||||||
const instanceLogs = ref<TaskLog | any>({});
|
const instanceLogs = ref<TaskLog | any>({});
|
||||||
|
|
||||||
async function changeTask(item: TaskListItem) {
|
async function changeTask(item: TaskListItem) {
|
||||||
profileStore.setCurrentSegment({});
|
profileStore.setCurrentSegment({});
|
||||||
selectedTask.value = item;
|
profileStore.setCurrentTask(item);
|
||||||
const res = await profileStore.getSegmentList({ taskID: item.id });
|
const res = await profileStore.getSegmentList({ taskID: item.id });
|
||||||
if (res.errors) {
|
if (res.errors) {
|
||||||
ElMessage.error(res.errors);
|
ElMessage.error(res.errors);
|
||||||
@ -130,7 +132,7 @@ limitations under the License. -->
|
|||||||
async function viewTask(e: Event, item: TaskListItem) {
|
async function viewTask(e: Event, item: TaskListItem) {
|
||||||
window.event ? (window.event.cancelBubble = true) : e.stopPropagation();
|
window.event ? (window.event.cancelBubble = true) : e.stopPropagation();
|
||||||
viewDetail.value = true;
|
viewDetail.value = true;
|
||||||
selectedTask.value = item;
|
profileStore.setCurrentTask(item);
|
||||||
service.value = (selectorStore.services.filter((s: any) => s.id === item.serviceId)[0] || {}).label;
|
service.value = (selectorStore.services.filter((s: any) => s.id === item.serviceId)[0] || {}).label;
|
||||||
const res = await profileStore.getTaskLogs({ taskID: item.id });
|
const res = await profileStore.getTaskLogs({ taskID: item.id });
|
||||||
|
|
||||||
@ -150,7 +152,7 @@ limitations under the License. -->
|
|||||||
instanceLogs.value[d.instanceName] = [{ operationType: d.operationType, operationTime: d.operationTime }];
|
instanceLogs.value[d.instanceName] = [{ operationType: d.operationType, operationTime: d.operationTime }];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selectedTask.value = item;
|
profileStore.setCurrentTask(item);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -20,6 +20,73 @@ limitations under the License. -->
|
|||||||
element-loading-background="rgba(0, 0, 0, 0)"
|
element-loading-background="rgba(0, 0, 0, 0)"
|
||||||
:style="`height: ${height}px`"
|
:style="`height: ${height}px`"
|
||||||
>
|
>
|
||||||
|
<svg class="svg-topology" :width="width - 100" :height="height" style="background-color: #fff" @click="svgEvent">
|
||||||
|
<g class="svg-graph" :transform="`translate(${diff[0]}, ${diff[1]})`">
|
||||||
|
<g
|
||||||
|
class="topo-node"
|
||||||
|
v-for="(n, index) in topologyLayout.nodes"
|
||||||
|
:key="index"
|
||||||
|
@mouseout="hideTip"
|
||||||
|
@mouseover="showNodeTip($event, n)"
|
||||||
|
@click="handleNodeClick($event, n)"
|
||||||
|
@mousedown="startMoveNode($event, n)"
|
||||||
|
@mouseup="stopMoveNode($event)"
|
||||||
|
>
|
||||||
|
<image width="36" height="36" :x="n.x - 15" :y="n.y - 18" :href="getNodeStatus(n)" />
|
||||||
|
<!-- <circle :cx="n.x" :cy="n.y" r="12" fill="none" stroke="red"/> -->
|
||||||
|
<image width="28" height="25" :x="n.x - 14" :y="n.y - 43" :href="icons.LOCAL" style="opacity: 0.8" />
|
||||||
|
<image
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
:x="n.x - 6"
|
||||||
|
:y="n.y - 38"
|
||||||
|
:href="!n.type || n.type === `N/A` ? icons.UNDEFINED : icons[n.type.toUpperCase().replace('-', '')]"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
:x="n.x - (Math.min(n.name.length, 20) * 6) / 2 + 6"
|
||||||
|
:y="n.y + n.height + 8"
|
||||||
|
style="pointer-events: none"
|
||||||
|
>
|
||||||
|
{{ n.name.length > 20 ? `${n.name.substring(0, 20)}...` : n.name }}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
<g v-for="(l, index) in topologyLayout.calls" :key="index">
|
||||||
|
<path
|
||||||
|
class="topo-line"
|
||||||
|
:d="`M${l.sourceX} ${l.sourceY} L${l.targetX} ${l.targetY}`"
|
||||||
|
stroke="#97B0F8"
|
||||||
|
marker-end="url(#arrow)"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
class="topo-line-anchor"
|
||||||
|
:cx="(l.sourceX + l.targetX) / 2"
|
||||||
|
:cy="(l.sourceY + l.targetY) / 2"
|
||||||
|
r="4"
|
||||||
|
fill="#97B0F8"
|
||||||
|
@click="handleLinkClick($event, l)"
|
||||||
|
@mouseover="showLinkTip($event, l)"
|
||||||
|
@mouseout="hideTip"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g class="arrows">
|
||||||
|
<defs v-for="(_, index) in topologyLayout.calls" :key="index">
|
||||||
|
<marker
|
||||||
|
id="arrow"
|
||||||
|
markerUnits="strokeWidth"
|
||||||
|
markerWidth="16"
|
||||||
|
markerHeight="16"
|
||||||
|
viewBox="0 0 12 12"
|
||||||
|
refX="10"
|
||||||
|
refY="6"
|
||||||
|
orient="auto"
|
||||||
|
>
|
||||||
|
<path d="M2,2 L10,6 L2,10 L6,6 L2,2" fill="#97B0F8" />
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<div id="tooltip"></div>
|
||||||
<div class="legend">
|
<div class="legend">
|
||||||
<div>
|
<div>
|
||||||
<img :src="icons.CUBE" />
|
<img :src="icons.CUBE" />
|
||||||
@ -53,8 +120,8 @@ limitations under the License. -->
|
|||||||
class="operations-list"
|
class="operations-list"
|
||||||
v-if="topologyStore.node"
|
v-if="topologyStore.node"
|
||||||
:style="{
|
:style="{
|
||||||
top: operationsPos.y + 'px',
|
top: operationsPos.y + 5 + 'px',
|
||||||
left: operationsPos.x + 'px',
|
left: operationsPos.x + 5 + 'px',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<span v-for="(item, index) of items" :key="index" @click="item.func(item.dashboard)">
|
<span v-for="(item, index) of items" :key="index" @click="item.func(item.dashboard)">
|
||||||
@ -68,11 +135,6 @@ limitations under the License. -->
|
|||||||
import { ref, onMounted, onBeforeUnmount, reactive, watch, computed, nextTick } from "vue";
|
import { ref, onMounted, onBeforeUnmount, reactive, watch, computed, nextTick } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import d3tip from "d3-tip";
|
|
||||||
import zoom from "../../components/utils/zoom";
|
|
||||||
import { simulationInit, simulationSkip } from "./utils/simulation";
|
|
||||||
import nodeElement from "./utils/nodeElement";
|
|
||||||
import { linkElement, anchorElement, arrowMarker } from "./utils/linkElement";
|
|
||||||
import type { Node, Call } from "@/types/topology";
|
import type { Node, Call } from "@/types/topology";
|
||||||
import { useSelectorStore } from "@/store/modules/selectors";
|
import { useSelectorStore } from "@/store/modules/selectors";
|
||||||
import { useTopologyStore } from "@/store/modules/topology";
|
import { useTopologyStore } from "@/store/modules/topology";
|
||||||
@ -89,6 +151,8 @@ limitations under the License. -->
|
|||||||
import { aggregation } from "@/hooks/useMetricsProcessor";
|
import { aggregation } from "@/hooks/useMetricsProcessor";
|
||||||
import icons from "@/assets/img/icons";
|
import icons from "@/assets/img/icons";
|
||||||
import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor";
|
import { useQueryTopologyMetrics } from "@/hooks/useMetricsProcessor";
|
||||||
|
import { layout, circleIntersection, computeCallPos } from "./utils/layout";
|
||||||
|
import zoom from "../../components/utils/zoom";
|
||||||
|
|
||||||
/*global Nullable, defineProps */
|
/*global Nullable, defineProps */
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -105,70 +169,186 @@ limitations under the License. -->
|
|||||||
const height = ref<number>(100);
|
const height = ref<number>(100);
|
||||||
const width = ref<number>(100);
|
const width = ref<number>(100);
|
||||||
const loading = ref<boolean>(false);
|
const loading = ref<boolean>(false);
|
||||||
const simulation = ref<any>(null);
|
|
||||||
const svg = ref<Nullable<any>>(null);
|
const svg = ref<Nullable<any>>(null);
|
||||||
|
const graph = ref<Nullable<any>>(null);
|
||||||
const chart = ref<Nullable<HTMLDivElement>>(null);
|
const chart = ref<Nullable<HTMLDivElement>>(null);
|
||||||
const tip = ref<Nullable<HTMLDivElement>>(null);
|
|
||||||
const graph = ref<any>(null);
|
|
||||||
const node = ref<any>(null);
|
|
||||||
const link = ref<any>(null);
|
|
||||||
const anchor = ref<any>(null);
|
|
||||||
const arrow = ref<any>(null);
|
|
||||||
const showSetting = ref<boolean>(false);
|
const showSetting = ref<boolean>(false);
|
||||||
const settings = ref<any>(props.config);
|
const settings = ref<any>(props.config);
|
||||||
const operationsPos = reactive<{ x: number; y: number }>({ x: NaN, y: NaN });
|
const operationsPos = reactive<{ x: number; y: number }>({ x: NaN, y: NaN });
|
||||||
const items = ref<{ id: string; title: string; func: any; dashboard?: string }[]>([]);
|
const items = ref<{ id: string; title: string; func: any; dashboard?: string }[]>([]);
|
||||||
const graphConfig = computed(() => props.config.graph || {});
|
const graphConfig = computed(() => props.config.graph || {});
|
||||||
const depth = ref<number>(graphConfig.value.depth || 2);
|
const depth = ref<number>(graphConfig.value.depth || 2);
|
||||||
|
const topologyLayout = ref<any>({});
|
||||||
|
const tooltip = ref<Nullable<any>>(null);
|
||||||
|
const graphWidth = ref<number>(100);
|
||||||
|
const currentNode = ref<Nullable<Node>>();
|
||||||
|
const diff = computed(() => [(width.value - graphWidth.value - 130) / 2, 100]);
|
||||||
|
const radius = 8;
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
async function init() {
|
||||||
const dom = document.querySelector(".topology")?.getBoundingClientRect() || {
|
const dom = document.querySelector(".topology")?.getBoundingClientRect() || {
|
||||||
height: 40,
|
height: 40,
|
||||||
width: 0,
|
width: 0,
|
||||||
};
|
};
|
||||||
height.value = dom.height - 40;
|
height.value = dom.height - 40;
|
||||||
width.value = dom.width;
|
width.value = dom.width;
|
||||||
|
svg.value = d3.select(".svg-topology");
|
||||||
|
graph.value = d3.select(".svg-graph");
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const json = await selectorStore.fetchServices(dashboardStore.layerId);
|
const json = await selectorStore.fetchServices(dashboardStore.layerId);
|
||||||
if (json.errors) {
|
if (json.errors) {
|
||||||
ElMessage.error(json.errors);
|
ElMessage.error(json.errors);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await freshNodes();
|
||||||
|
svg.value.call(zoom(d3, graph.value, diff.value));
|
||||||
|
}
|
||||||
|
async function freshNodes() {
|
||||||
|
topologyStore.setNode(null);
|
||||||
|
topologyStore.setLink(null);
|
||||||
const resp = await getTopology();
|
const resp = await getTopology();
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
|
||||||
if (resp && resp.errors) {
|
if (resp && resp.errors) {
|
||||||
ElMessage.error(resp.errors);
|
ElMessage.error(resp.errors);
|
||||||
}
|
}
|
||||||
|
await update();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update() {
|
||||||
|
topologyStore.queryNodeMetrics(settings.value.nodeMetrics || []);
|
||||||
topologyStore.getLinkClientMetrics(settings.value.linkClientMetrics || []);
|
topologyStore.getLinkClientMetrics(settings.value.linkClientMetrics || []);
|
||||||
topologyStore.getLinkServerMetrics(settings.value.linkServerMetrics || []);
|
topologyStore.getLinkServerMetrics(settings.value.linkServerMetrics || []);
|
||||||
topologyStore.queryNodeMetrics(settings.value.nodeMetrics || []);
|
|
||||||
window.addEventListener("resize", resize);
|
window.addEventListener("resize", resize);
|
||||||
svg.value = d3.select(chart.value).append("svg").attr("class", "topo-svg");
|
|
||||||
await initLegendMetrics();
|
await initLegendMetrics();
|
||||||
await init();
|
draw();
|
||||||
update();
|
tooltip.value = d3.select("#tooltip");
|
||||||
setNodeTools(settings.value.nodeDashboard);
|
setNodeTools(settings.value.nodeDashboard);
|
||||||
});
|
}
|
||||||
async function init() {
|
function draw() {
|
||||||
tip.value = (d3tip as any)().attr("class", "d3-tip").offset([-8, 0]);
|
const node = findMostFrequent(topologyStore.calls);
|
||||||
graph.value = svg.value.append("g").attr("class", "topo-svg-graph").attr("transform", `translate(-100, -100)`);
|
const levels = [];
|
||||||
graph.value.call(tip.value);
|
const nodes = topologyStore.nodes.sort((a: Node, b: Node) => {
|
||||||
simulation.value = simulationInit(d3, topologyStore.nodes, topologyStore.calls, ticked);
|
if (a.name.toLowerCase() < b.name.toLowerCase()) {
|
||||||
node.value = graph.value.append("g").selectAll(".topo-node");
|
return -1;
|
||||||
link.value = graph.value.append("g").selectAll(".topo-line");
|
}
|
||||||
anchor.value = graph.value.append("g").selectAll(".topo-line-anchor");
|
if (a.name.toLowerCase() > b.name.toLowerCase()) {
|
||||||
arrow.value = graph.value.append("g").selectAll(".topo-line-arrow");
|
return 1;
|
||||||
svg.value.call(zoom(d3, graph.value, [-100, -100]));
|
}
|
||||||
svg.value.on("click", (event: any) => {
|
return 0;
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
topologyStore.setNode(null);
|
|
||||||
topologyStore.setLink(null);
|
|
||||||
dashboardStore.selectWidget(props.config);
|
|
||||||
});
|
});
|
||||||
|
const index = nodes.findIndex((n: Node) => n.type === "USER");
|
||||||
|
let key = index;
|
||||||
|
if (index < 0) {
|
||||||
|
const idx = nodes.findIndex((n: Node) => n.id === node.id);
|
||||||
|
key = idx;
|
||||||
|
}
|
||||||
|
levels.push([nodes[key]]);
|
||||||
|
nodes.splice(key, 1);
|
||||||
|
for (const level of levels) {
|
||||||
|
const a = [];
|
||||||
|
for (const l of level) {
|
||||||
|
for (const n of topologyStore.calls) {
|
||||||
|
if (n.target === l.id) {
|
||||||
|
const i = nodes.findIndex((d: Node) => d.id === n.source);
|
||||||
|
if (i > -1) {
|
||||||
|
a.push(nodes[i]);
|
||||||
|
nodes.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (n.source === l.id) {
|
||||||
|
const i = nodes.findIndex((d: Node) => d.id === n.target);
|
||||||
|
if (i > -1) {
|
||||||
|
a.push(nodes[i]);
|
||||||
|
nodes.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (a.length) {
|
||||||
|
levels.push(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
topologyLayout.value = layout(levels, topologyStore.calls, radius);
|
||||||
|
graphWidth.value = topologyLayout.value.layout.width;
|
||||||
|
const drag: any = d3.drag().on("drag", (d: { x: number; y: number }) => {
|
||||||
|
moveNode(d);
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
d3.selectAll(".topo-node").call(drag);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveNode(d: { x: number; y: number }) {
|
||||||
|
if (!currentNode.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const node of topologyLayout.value.nodes) {
|
||||||
|
if (node.id === currentNode.value.id) {
|
||||||
|
node.x = d.x;
|
||||||
|
node.y = d.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const call of topologyLayout.value.calls) {
|
||||||
|
if (call.sourceObj.id === currentNode.value.id) {
|
||||||
|
call.sourceObj.x = d.x;
|
||||||
|
call.sourceObj.y = d.y;
|
||||||
|
}
|
||||||
|
if (call.targetObj.id === currentNode.value.id) {
|
||||||
|
call.targetObj.x = d.x;
|
||||||
|
call.targetObj.y = d.y;
|
||||||
|
}
|
||||||
|
if (call.targetObj.id === currentNode.value.id || call.sourceObj.id === currentNode.value.id) {
|
||||||
|
const pos: any = circleIntersection(
|
||||||
|
call.sourceObj.x,
|
||||||
|
call.sourceObj.y,
|
||||||
|
radius,
|
||||||
|
call.targetObj.x,
|
||||||
|
call.targetObj.y,
|
||||||
|
radius,
|
||||||
|
);
|
||||||
|
call.sourceX = pos[0].x;
|
||||||
|
call.sourceY = pos[0].y;
|
||||||
|
call.targetX = pos[1].x;
|
||||||
|
call.targetY = pos[1].y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
topologyLayout.value.calls = computeCallPos(topologyLayout.value.calls, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startMoveNode(event: MouseEvent, d: Node) {
|
||||||
|
event.stopPropagation();
|
||||||
|
currentNode.value = d;
|
||||||
|
}
|
||||||
|
function stopMoveNode(event: MouseEvent) {
|
||||||
|
event.stopPropagation();
|
||||||
|
currentNode.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMostFrequent(arr: Call[]) {
|
||||||
|
let count: any = {};
|
||||||
|
let maxCount = 0;
|
||||||
|
let maxItem = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
let item = arr[i];
|
||||||
|
count[item.sourceObj.id] = (count[item.sourceObj.id] || 0) + 1;
|
||||||
|
if (count[item.sourceObj.id] > maxCount) {
|
||||||
|
maxCount = count[item.sourceObj.id];
|
||||||
|
maxItem = item.sourceObj;
|
||||||
|
}
|
||||||
|
count[item.targetObj.id] = (count[item.targetObj.id] || 0) + 1;
|
||||||
|
if (count[item.targetObj.id] > maxCount) {
|
||||||
|
maxCount = count[item.targetObj.id];
|
||||||
|
maxItem = item.targetObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initLegendMetrics() {
|
async function initLegendMetrics() {
|
||||||
@ -182,60 +362,109 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function ticked() {
|
function getNodeStatus(d: any) {
|
||||||
link.value.attr(
|
const legend = settings.value.legend;
|
||||||
"d",
|
if (!legend) {
|
||||||
(d: Call | any) =>
|
return icons.CUBE;
|
||||||
`M${d.source.x} ${d.source.y} Q ${(d.source.x + d.target.x) / 2} ${
|
}
|
||||||
(d.target.y + d.source.y) / 2 - d.loopFactor * 90
|
if (!legend.length) {
|
||||||
} ${d.target.x} ${d.target.y}`,
|
return icons.CUBE;
|
||||||
);
|
}
|
||||||
anchor.value.attr(
|
let c = true;
|
||||||
"transform",
|
for (const l of legend) {
|
||||||
(d: Call | any) =>
|
if (l.condition === "<") {
|
||||||
`translate(${(d.source.x + d.target.x) / 2}, ${(d.target.y + d.source.y) / 2 - d.loopFactor * 45})`,
|
c = c && d[l.name] < Number(l.value);
|
||||||
);
|
} else {
|
||||||
node.value.attr("transform", (d: Node | any) => `translate(${d.x - 22},${d.y - 22})`);
|
c = c && d[l.name] > Number(l.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c && d.isReal ? icons.CUBEERROR : icons.CUBE;
|
||||||
}
|
}
|
||||||
function dragstart(d: any) {
|
function showNodeTip(event: MouseEvent, data: Node) {
|
||||||
node.value._groups[0].forEach((g: any) => {
|
const nodeMetrics: string[] = settings.value.nodeMetrics || [];
|
||||||
g.__data__.fx = g.__data__.x;
|
const nodeMetricConfig = settings.value.nodeMetricConfig || [];
|
||||||
g.__data__.fy = g.__data__.y;
|
const html = nodeMetrics.map((m, index) => {
|
||||||
|
const metric =
|
||||||
|
topologyStore.nodeMetricValue[m].values.find((val: { id: string; value: unknown }) => val.id === data.id) || {};
|
||||||
|
const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
|
||||||
|
const v = aggregation(metric.value, opt);
|
||||||
|
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || "unknown"}</div>`;
|
||||||
});
|
});
|
||||||
if (!d.active) {
|
const tipHtml = [
|
||||||
simulation.value.alphaTarget(0.1).restart();
|
`<div class="mb-5"><span class="grey">name: </span>${
|
||||||
}
|
data.name
|
||||||
d.subject.fx = d.subject.x;
|
}</div><div class="mb-5"><span class="grey">type: </span>${data.type || "UNKNOWN"}</div>`,
|
||||||
d.subject.fy = d.subject.y;
|
...html,
|
||||||
d.sourceEvent.stopPropagation();
|
].join(" ");
|
||||||
|
|
||||||
|
tooltip.value
|
||||||
|
.style("top", event.offsetY + 10 + "px")
|
||||||
|
.style("left", event.offsetX + 10 + "px")
|
||||||
|
.style("visibility", "visible")
|
||||||
|
.html(tipHtml);
|
||||||
}
|
}
|
||||||
function dragged(d: any) {
|
function showLinkTip(event: MouseEvent, data: Call) {
|
||||||
d.subject.fx = d.x;
|
const linkClientMetrics: string[] = settings.value.linkClientMetrics || [];
|
||||||
d.subject.fy = d.y;
|
const linkServerMetricConfig: MetricConfigOpt[] = settings.value.linkServerMetricConfig || [];
|
||||||
|
const linkClientMetricConfig: MetricConfigOpt[] = settings.value.linkClientMetricConfig || [];
|
||||||
|
const linkServerMetrics: string[] = settings.value.linkServerMetrics || [];
|
||||||
|
const htmlServer = linkServerMetrics.map((m, index) => {
|
||||||
|
const metric = topologyStore.linkServerMetrics[m].values.find(
|
||||||
|
(val: { id: string; value: unknown }) => val.id === data.id,
|
||||||
|
);
|
||||||
|
if (metric) {
|
||||||
|
const opt: MetricConfigOpt = linkServerMetricConfig[index] || {};
|
||||||
|
const v = aggregation(metric.value, opt);
|
||||||
|
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const htmlClient = linkClientMetrics.map((m: string, index: number) => {
|
||||||
|
const opt: MetricConfigOpt = linkClientMetricConfig[index] || {};
|
||||||
|
const metric = topologyStore.linkClientMetrics[m].values.find(
|
||||||
|
(val: { id: string; value: unknown }) => val.id === data.id,
|
||||||
|
);
|
||||||
|
if (metric) {
|
||||||
|
const v = aggregation(metric.value, opt);
|
||||||
|
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const html = [
|
||||||
|
...htmlServer,
|
||||||
|
...htmlClient,
|
||||||
|
`<div><span class="grey">${t("detectPoint")}:</span>${data.detectPoints.join(" | ")}</div>`,
|
||||||
|
].join(" ");
|
||||||
|
|
||||||
|
tooltip.value
|
||||||
|
.style("top", event.offsetY + "px")
|
||||||
|
.style("left", event.offsetX + "px")
|
||||||
|
.style("visibility", "visible")
|
||||||
|
.html(html);
|
||||||
}
|
}
|
||||||
function dragended(d: any) {
|
|
||||||
if (!d.active) {
|
function hideTip() {
|
||||||
simulation.value.alphaTarget(0);
|
tooltip.value.style("visibility", "hidden");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function handleNodeClick(d: Node & { x: number; y: number }) {
|
function handleNodeClick(event: MouseEvent, d: Node & { x: number; y: number }) {
|
||||||
|
event.stopPropagation();
|
||||||
|
hideTip();
|
||||||
topologyStore.setNode(d);
|
topologyStore.setNode(d);
|
||||||
topologyStore.setLink(null);
|
topologyStore.setLink(null);
|
||||||
operationsPos.x = d.x - 100;
|
operationsPos.x = event.offsetX;
|
||||||
operationsPos.y = d.y - 70;
|
operationsPos.y = event.offsetY;
|
||||||
if (d.layer === String(dashboardStore.layerId)) {
|
if (d.layer === String(dashboardStore.layerId)) {
|
||||||
|
setNodeTools(settings.value.nodeDashboard);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
items.value = [
|
items.value = [
|
||||||
{ id: "inspect", title: "Inspect", func: handleInspect },
|
{ id: "inspect", title: "Inspect", func: handleInspect },
|
||||||
{ id: "alarm", title: "Alarm", func: handleGoAlarm },
|
{ id: "alerting", title: "Alerting", func: handleGoAlerting },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
function handleLinkClick(event: any, d: Call) {
|
function handleLinkClick(event: MouseEvent, d: Call) {
|
||||||
if (d.source.layer !== dashboardStore.layerId || d.target.layer !== dashboardStore.layerId) {
|
event.stopPropagation();
|
||||||
|
if (d.sourceObj.layer !== dashboardStore.layerId || d.targetObj.layer !== dashboardStore.layerId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.stopPropagation();
|
|
||||||
topologyStore.setNode(null);
|
topologyStore.setNode(null);
|
||||||
topologyStore.setLink(d);
|
topologyStore.setLink(d);
|
||||||
if (!settings.value.linkDashboard) {
|
if (!settings.value.linkDashboard) {
|
||||||
@ -253,132 +482,22 @@ limitations under the License. -->
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dashboardStore.setEntity(dashboard.entity);
|
dashboardStore.setEntity(dashboard.entity);
|
||||||
const path = `/dashboard/related/${dashboard.layer}/${e}Relation/${d.source.id}/${d.target.id}/${dashboard.name}`;
|
const path = `/dashboard/related/${dashboard.layer}/${e}Relation/${d.sourceObj.id}/${d.targetObj.id}/${dashboard.name}`;
|
||||||
const routeUrl = router.resolve({ path });
|
const routeUrl = router.resolve({ path });
|
||||||
window.open(routeUrl.href, "_blank");
|
window.open(routeUrl.href, "_blank");
|
||||||
dashboardStore.setEntity(origin);
|
dashboardStore.setEntity(origin);
|
||||||
}
|
}
|
||||||
function update() {
|
|
||||||
// node element
|
|
||||||
if (!node.value || !link.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
node.value = node.value.data(topologyStore.nodes, (d: Node) => d.id);
|
|
||||||
node.value.exit().remove();
|
|
||||||
node.value = nodeElement(
|
|
||||||
d3,
|
|
||||||
node.value.enter(),
|
|
||||||
{
|
|
||||||
dragstart: dragstart,
|
|
||||||
dragged: dragged,
|
|
||||||
dragended: dragended,
|
|
||||||
handleNodeClick: handleNodeClick,
|
|
||||||
tipHtml: (data: Node) => {
|
|
||||||
const nodeMetrics: string[] = settings.value.nodeMetrics || [];
|
|
||||||
const nodeMetricConfig = settings.value.nodeMetricConfig || [];
|
|
||||||
const html = nodeMetrics.map((m, index) => {
|
|
||||||
const metric =
|
|
||||||
topologyStore.nodeMetricValue[m].values.find(
|
|
||||||
(val: { id: string; value: unknown }) => val.id === data.id,
|
|
||||||
) || {};
|
|
||||||
const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
|
|
||||||
const v = aggregation(metric.value, opt);
|
|
||||||
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
|
|
||||||
});
|
|
||||||
return [` <div class="mb-5"><span class="grey">name: </span>${data.name}</div>`, ...html].join(" ");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tip.value,
|
|
||||||
settings.value.legend,
|
|
||||||
).merge(node.value);
|
|
||||||
// line element
|
|
||||||
link.value = link.value.data(topologyStore.calls, (d: Call) => d.id);
|
|
||||||
link.value.exit().remove();
|
|
||||||
link.value = linkElement(link.value.enter()).merge(link.value);
|
|
||||||
// anchorElement
|
|
||||||
anchor.value = anchor.value.data(topologyStore.calls, (d: Call) => d.id);
|
|
||||||
anchor.value.exit().remove();
|
|
||||||
anchor.value = anchorElement(
|
|
||||||
anchor.value.enter(),
|
|
||||||
{
|
|
||||||
handleLinkClick: handleLinkClick,
|
|
||||||
tipHtml: (data: Call) => {
|
|
||||||
const linkClientMetrics: string[] = settings.value.linkClientMetrics || [];
|
|
||||||
const linkServerMetricConfig: MetricConfigOpt[] = settings.value.linkServerMetricConfig || [];
|
|
||||||
const linkClientMetricConfig: MetricConfigOpt[] = settings.value.linkClientMetricConfig || [];
|
|
||||||
const linkServerMetrics: string[] = settings.value.linkServerMetrics || [];
|
|
||||||
const htmlServer = linkServerMetrics.map((m, index) => {
|
|
||||||
const metric = topologyStore.linkServerMetrics[m].values.find(
|
|
||||||
(val: { id: string; value: unknown }) => val.id === data.id,
|
|
||||||
);
|
|
||||||
if (metric) {
|
|
||||||
const opt: MetricConfigOpt = linkServerMetricConfig[index] || {};
|
|
||||||
const v = aggregation(metric.value, opt);
|
|
||||||
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const htmlClient = linkClientMetrics.map((m: string, index: number) => {
|
|
||||||
const opt: MetricConfigOpt = linkClientMetricConfig[index] || {};
|
|
||||||
const metric = topologyStore.linkClientMetrics[m].values.find(
|
|
||||||
(val: { id: string; value: unknown }) => val.id === data.id,
|
|
||||||
);
|
|
||||||
if (metric) {
|
|
||||||
const v = aggregation(metric.value, opt);
|
|
||||||
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${v} ${opt.unit || ""}</div>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const html = [
|
|
||||||
...htmlServer,
|
|
||||||
...htmlClient,
|
|
||||||
`<div><span class="grey">${t("detectPoint")}:</span>${data.detectPoints.join(" | ")}</div>`,
|
|
||||||
].join(" ");
|
|
||||||
|
|
||||||
return html;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tip.value,
|
|
||||||
).merge(anchor.value);
|
|
||||||
// arrow marker
|
|
||||||
arrow.value = arrow.value.data(topologyStore.calls, (d: Call) => d.id);
|
|
||||||
arrow.value.exit().remove();
|
|
||||||
arrow.value = arrowMarker(arrow.value.enter()).merge(arrow.value);
|
|
||||||
// force element
|
|
||||||
simulation.value.nodes(topologyStore.nodes);
|
|
||||||
simulation.value
|
|
||||||
.force("link")
|
|
||||||
.links(topologyStore.calls)
|
|
||||||
.id((d: Call) => d.id);
|
|
||||||
simulationSkip(d3, simulation.value, ticked);
|
|
||||||
const loopMap: any = {};
|
|
||||||
for (let i = 0; i < topologyStore.calls.length; i++) {
|
|
||||||
const link: any = topologyStore.calls[i];
|
|
||||||
link.loopFactor = 1;
|
|
||||||
for (let j = 0; j < topologyStore.calls.length; j++) {
|
|
||||||
if (i === j || loopMap[i]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const otherLink = topologyStore.calls[j];
|
|
||||||
if (link.source.id === otherLink.target.id && link.target.id === otherLink.source.id) {
|
|
||||||
link.loopFactor = -1;
|
|
||||||
loopMap[j] = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function handleInspect() {
|
async function handleInspect() {
|
||||||
svg.value.selectAll(".topo-svg-graph").remove();
|
|
||||||
const id = topologyStore.node.id;
|
const id = topologyStore.node.id;
|
||||||
topologyStore.setNode(null);
|
|
||||||
topologyStore.setLink(null);
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const resp = await topologyStore.getDepthServiceTopology([id], Number(depth.value));
|
const resp = await topologyStore.getDepthServiceTopology([id], Number(depth.value));
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
if (resp && resp.errors) {
|
if (resp && resp.errors) {
|
||||||
ElMessage.error(resp.errors);
|
ElMessage.error(resp.errors);
|
||||||
}
|
}
|
||||||
await init();
|
await update();
|
||||||
update();
|
topologyStore.setNode(null);
|
||||||
|
topologyStore.setLink(null);
|
||||||
}
|
}
|
||||||
function handleGoEndpoint(name: string) {
|
function handleGoEndpoint(name: string) {
|
||||||
const path = `/dashboard/${dashboardStore.layerId}/${EntityType[2].value}/${topologyStore.node.id}/${name}`;
|
const path = `/dashboard/${dashboardStore.layerId}/${EntityType[2].value}/${topologyStore.node.id}/${name}`;
|
||||||
@ -401,23 +520,15 @@ limitations under the License. -->
|
|||||||
window.open(routeUrl.href, "_blank");
|
window.open(routeUrl.href, "_blank");
|
||||||
dashboardStore.setEntity(origin);
|
dashboardStore.setEntity(origin);
|
||||||
}
|
}
|
||||||
function handleGoAlarm() {
|
function handleGoAlerting() {
|
||||||
const path = `/alarm`;
|
const path = `/alerting`;
|
||||||
const routeUrl = router.resolve({ path });
|
const routeUrl = router.resolve({ path });
|
||||||
|
|
||||||
window.open(routeUrl.href, "_blank");
|
window.open(routeUrl.href, "_blank");
|
||||||
}
|
}
|
||||||
async function backToTopology() {
|
async function backToTopology() {
|
||||||
svg.value.selectAll(".topo-svg-graph").remove();
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const resp = await getTopology();
|
await freshNodes();
|
||||||
loading.value = false;
|
|
||||||
|
|
||||||
if (resp && resp.errors) {
|
|
||||||
ElMessage.error(resp.errors);
|
|
||||||
}
|
|
||||||
await init();
|
|
||||||
update();
|
|
||||||
topologyStore.setNode(null);
|
topologyStore.setNode(null);
|
||||||
topologyStore.setLink(null);
|
topologyStore.setLink(null);
|
||||||
}
|
}
|
||||||
@ -438,7 +549,6 @@ limitations under the License. -->
|
|||||||
};
|
};
|
||||||
height.value = dom.height - 40;
|
height.value = dom.height - 40;
|
||||||
width.value = dom.width;
|
width.value = dom.width;
|
||||||
svg.value.attr("height", height.value).attr("width", width.value);
|
|
||||||
}
|
}
|
||||||
function updateSettings(config: any) {
|
function updateSettings(config: any) {
|
||||||
settings.value = config;
|
settings.value = config;
|
||||||
@ -447,7 +557,7 @@ limitations under the License. -->
|
|||||||
function setNodeTools(nodeDashboard: any) {
|
function setNodeTools(nodeDashboard: any) {
|
||||||
items.value = [
|
items.value = [
|
||||||
{ id: "inspect", title: "Inspect", func: handleInspect },
|
{ id: "inspect", title: "Inspect", func: handleInspect },
|
||||||
{ id: "alarm", title: "Alarm", func: handleGoAlarm },
|
{ id: "alerting", title: "Alerting", func: handleGoAlerting },
|
||||||
];
|
];
|
||||||
if (!(nodeDashboard && nodeDashboard.length)) {
|
if (!(nodeDashboard && nodeDashboard.length)) {
|
||||||
return;
|
return;
|
||||||
@ -479,18 +589,14 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function freshNodes() {
|
function svgEvent() {
|
||||||
if (!svg.value) {
|
topologyStore.setNode(null);
|
||||||
return;
|
topologyStore.setLink(null);
|
||||||
}
|
dashboardStore.selectWidget(props.config);
|
||||||
svg.value.selectAll(".topo-svg-graph").remove();
|
|
||||||
await init();
|
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeDepth(opt: Option[] | any) {
|
async function changeDepth(opt: Option[] | any) {
|
||||||
depth.value = opt[0].value;
|
depth.value = opt[0].value;
|
||||||
await getTopology();
|
|
||||||
freshNodes();
|
freshNodes();
|
||||||
}
|
}
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@ -498,7 +604,13 @@ limitations under the License. -->
|
|||||||
});
|
});
|
||||||
watch(
|
watch(
|
||||||
() => [selectorStore.currentService, selectorStore.currentDestService],
|
() => [selectorStore.currentService, selectorStore.currentDestService],
|
||||||
() => {
|
(newVal, oldVal) => {
|
||||||
|
if (oldVal[0].id === newVal[0].id && !oldVal[1]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (oldVal[0].id === newVal[0].id && oldVal[1].id === newVal[1].id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
freshNodes();
|
freshNodes();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -512,23 +624,20 @@ limitations under the License. -->
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.topo-svg {
|
|
||||||
width: 100%;
|
|
||||||
height: calc(100% - 5px);
|
|
||||||
cursor: move;
|
|
||||||
}
|
|
||||||
|
|
||||||
.micro-topo-chart {
|
.micro-topo-chart {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: calc(100% - 30px);
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.svg-topology {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
.legend {
|
.legend {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
left: 15px;
|
left: 25px;
|
||||||
color: #ccc;
|
color: #666;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
@ -553,16 +662,22 @@ limitations under the License. -->
|
|||||||
right: 10px;
|
right: 10px;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
height: 600px;
|
height: 600px;
|
||||||
background-color: #2b3037;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: #eee 1px 2px 10px;
|
||||||
transition: all 0.5ms linear;
|
transition: all 0.5ms linear;
|
||||||
|
|
||||||
|
&.dark {
|
||||||
|
background-color: #2b3037;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: #ccc;
|
color: #666;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
@ -572,8 +687,10 @@ limitations under the License. -->
|
|||||||
color: #333;
|
color: #333;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 3px;
|
border-radius: 5px;
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
|
border: 1px solid #999;
|
||||||
|
box-shadow: #ddd 1px 2px 10px;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: block;
|
display: block;
|
||||||
@ -598,22 +715,23 @@ limitations under the License. -->
|
|||||||
.switch-icon {
|
.switch-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.5ms linear;
|
transition: all 0.5ms linear;
|
||||||
background-color: #252a2f99;
|
background: rgba(0, 0, 0, 0.3);
|
||||||
color: #ddd;
|
color: #fff;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 5px 8px 8px;
|
padding: 2px 4px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-line {
|
.topo-line {
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-width: 3px;
|
stroke-width: 1px;
|
||||||
stroke-dasharray: 13 7;
|
stroke-dasharray: 10 10;
|
||||||
fill: none;
|
fill: none;
|
||||||
animation: topo-dash 0.5s linear infinite;
|
animation: topo-dash 0.3s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topo-line-anchor {
|
.topo-line-anchor,
|
||||||
|
.topo-node {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -624,37 +742,9 @@ limitations under the License. -->
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.d3-tip {
|
|
||||||
line-height: 1;
|
|
||||||
padding: 8px;
|
|
||||||
color: #eee;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
z-index: 9999;
|
|
||||||
background: #252a2f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.d3-tip:after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: block;
|
|
||||||
font-size: 10px;
|
|
||||||
width: 100%;
|
|
||||||
line-height: 0.8;
|
|
||||||
color: #252a2f;
|
|
||||||
content: "\25BC";
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.d3-tip.n:after {
|
|
||||||
margin: -2px 0 0 0;
|
|
||||||
top: 100%;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
@keyframes topo-dash {
|
@keyframes topo-dash {
|
||||||
from {
|
from {
|
||||||
stroke-dashoffset: 20;
|
stroke-dashoffset: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
@ -665,4 +755,13 @@ limitations under the License. -->
|
|||||||
.el-loading-spinner {
|
.el-loading-spinner {
|
||||||
top: 30%;
|
top: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tooltip {
|
||||||
|
position: absolute;
|
||||||
|
visibility: hidden;
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -27,7 +27,7 @@ limitations under the License. -->
|
|||||||
/>
|
/>
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span>{{ t("linkServerMetrics") }}</span>
|
<span>{{ t("linkServerMetrics") }}</span>
|
||||||
<el-popover placement="left" :width="400" trigger="click" effect="dark" v-if="states.linkServerMetrics.length">
|
<el-popover placement="left" :width="400" trigger="click" v-if="states.linkServerMetrics.length">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<span @click="setConfigType('linkServerMetricConfig')">
|
<span @click="setConfigType('linkServerMetricConfig')">
|
||||||
<Icon class="cp ml-5" iconName="mode_edit" size="middle" />
|
<Icon class="cp ml-5" iconName="mode_edit" size="middle" />
|
||||||
@ -48,7 +48,7 @@ limitations under the License. -->
|
|||||||
<span v-show="dashboardStore.entity !== EntityType[2].value">
|
<span v-show="dashboardStore.entity !== EntityType[2].value">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span>{{ t("linkClientMetrics") }}</span>
|
<span>{{ t("linkClientMetrics") }}</span>
|
||||||
<el-popover placement="left" :width="400" trigger="click" effect="dark" v-if="states.linkClientMetrics.length">
|
<el-popover placement="left" :width="400" trigger="click" v-if="states.linkClientMetrics.length">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<span @click="setConfigType('linkClientMetricConfig')">
|
<span @click="setConfigType('linkClientMetricConfig')">
|
||||||
<Icon class="cp ml-5" iconName="mode_edit" size="middle" />
|
<Icon class="cp ml-5" iconName="mode_edit" size="middle" />
|
||||||
@ -110,7 +110,7 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span>{{ t("nodeMetrics") }}</span>
|
<span>{{ t("nodeMetrics") }}</span>
|
||||||
<el-popover placement="left" :width="400" trigger="click" effect="dark" v-if="states.nodeMetrics.length">
|
<el-popover placement="left" :width="400" trigger="click" v-if="states.nodeMetrics.length">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<span @click="setConfigType('nodeMetricConfig')">
|
<span @click="setConfigType('nodeMetricConfig')">
|
||||||
<Icon class="cp ml-5" iconName="mode_edit" size="middle" />
|
<Icon class="cp ml-5" iconName="mode_edit" size="middle" />
|
||||||
@ -454,6 +454,7 @@ limitations under the License. -->
|
|||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
|
122
src/views/dashboard/related/topology/components/utils/layout.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* 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 * as d3 from "d3";
|
||||||
|
import type { Node, Call } from "@/types/topology";
|
||||||
|
|
||||||
|
export function layout(levels: Node[][], calls: Call[], radius: number) {
|
||||||
|
// precompute level depth
|
||||||
|
levels.forEach((l: Node[], i: number) => l.forEach((n: any) => (n.level = i)));
|
||||||
|
|
||||||
|
const nodes: Node[] = levels.reduce((a, x) => a.concat(x), []);
|
||||||
|
// layout
|
||||||
|
const padding = 30;
|
||||||
|
const node_height = 120;
|
||||||
|
const node_width = 100;
|
||||||
|
const bundle_width = 14;
|
||||||
|
const metro_d = 4;
|
||||||
|
for (const n of nodes) {
|
||||||
|
n.height = 5 * metro_d;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x_offset = padding;
|
||||||
|
let y_offset = 0;
|
||||||
|
for (const level of levels) {
|
||||||
|
y_offset = 0;
|
||||||
|
x_offset += 5 * bundle_width;
|
||||||
|
for (const l of level) {
|
||||||
|
const n: any = l;
|
||||||
|
for (const call of calls) {
|
||||||
|
if (call.source === n.id) {
|
||||||
|
call.sourceObj = n;
|
||||||
|
}
|
||||||
|
if (call.target === n.id) {
|
||||||
|
call.targetObj = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n.x = n.level * node_width + x_offset;
|
||||||
|
n.y = node_height + y_offset + n.height / 2;
|
||||||
|
y_offset += node_height + n.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const layout = {
|
||||||
|
width: d3.max(nodes as any, (n: { x: number }) => n.x) || 0 + node_width + 2 * padding,
|
||||||
|
height: d3.max(nodes as any, (n: { y: number }) => n.y) || 0 + node_height / 2 + 2 * padding,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { nodes, layout, calls: computeCallPos(calls, radius) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeCallPos(calls: Call[], radius: number) {
|
||||||
|
for (const [index, call] of calls.entries()) {
|
||||||
|
const centrePoints = [call.sourceObj.x, call.sourceObj.y, call.targetObj.x, call.targetObj.y];
|
||||||
|
for (const [idx, link] of calls.entries()) {
|
||||||
|
if (
|
||||||
|
index < idx &&
|
||||||
|
call.id !== link.id &&
|
||||||
|
call.sourceObj.x === link.targetObj.x &&
|
||||||
|
call.sourceObj.y === link.targetObj.y &&
|
||||||
|
call.targetObj.x === link.sourceObj.x &&
|
||||||
|
call.targetObj.y === link.sourceObj.y
|
||||||
|
) {
|
||||||
|
if (call.targetObj.y === call.sourceObj.y) {
|
||||||
|
centrePoints[1] = centrePoints[1] - 8;
|
||||||
|
centrePoints[3] = centrePoints[3] - 8;
|
||||||
|
} else if (call.targetObj.x === call.sourceObj.x) {
|
||||||
|
centrePoints[0] = centrePoints[0] - 8;
|
||||||
|
centrePoints[2] = centrePoints[2] - 8;
|
||||||
|
} else {
|
||||||
|
centrePoints[1] = centrePoints[1] + 6;
|
||||||
|
centrePoints[3] = centrePoints[3] + 6;
|
||||||
|
centrePoints[0] = centrePoints[0] - 6;
|
||||||
|
centrePoints[2] = centrePoints[2] - 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pos: { x: number; y: number }[] = circleIntersection(
|
||||||
|
centrePoints[0],
|
||||||
|
centrePoints[1],
|
||||||
|
radius,
|
||||||
|
centrePoints[2],
|
||||||
|
centrePoints[3],
|
||||||
|
radius,
|
||||||
|
);
|
||||||
|
call.sourceX = pos[0].x;
|
||||||
|
call.sourceY = pos[0].y;
|
||||||
|
call.targetX = pos[1].x;
|
||||||
|
call.targetY = pos[1].y;
|
||||||
|
}
|
||||||
|
return calls;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function circleIntersection(ax: number, ay: number, ar: number, bx: number, by: number, br: number) {
|
||||||
|
const dab = Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
|
||||||
|
|
||||||
|
const dfx = (ar * Math.abs(ax - bx)) / dab;
|
||||||
|
const dfy = (ar * Math.abs(ay - by)) / dab;
|
||||||
|
const fx = bx > ax ? ax + dfx : ax - dfx;
|
||||||
|
const fy = ay > by ? ay - dfy : ay + dfy;
|
||||||
|
|
||||||
|
const dgx = (br * Math.abs(ax - bx)) / dab;
|
||||||
|
const dgy = (br * Math.abs(ay - by)) / dab;
|
||||||
|
const gx = bx > ax ? bx - dgx : bx + dgx;
|
||||||
|
const gy = ay > by ? by + dgy : by - dgy;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ x: fx, y: fy },
|
||||||
|
{ x: gx, y: gy },
|
||||||
|
];
|
||||||
|
}
|
@ -1,60 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 const linkElement = (graph: any) => {
|
|
||||||
const linkEnter = graph
|
|
||||||
.append("path")
|
|
||||||
.attr("class", "topo-line")
|
|
||||||
.attr("marker-end", "url(#arrow)")
|
|
||||||
.attr("stroke", "#217EF25f");
|
|
||||||
return linkEnter;
|
|
||||||
};
|
|
||||||
export const anchorElement = (graph: any, funcs: any, tip: any) => {
|
|
||||||
const linkEnter = graph
|
|
||||||
.append("circle")
|
|
||||||
.attr("class", "topo-line-anchor")
|
|
||||||
.attr("r", 5)
|
|
||||||
.attr("fill", "#217EF25f")
|
|
||||||
.on("mouseover", function (event: unknown, d: unknown) {
|
|
||||||
tip.html(funcs.tipHtml).show(d, this);
|
|
||||||
})
|
|
||||||
.on("mouseout", function () {
|
|
||||||
tip.hide(this);
|
|
||||||
})
|
|
||||||
.on("click", (event: unknown, d: unknown) => {
|
|
||||||
funcs.handleLinkClick(event, d);
|
|
||||||
});
|
|
||||||
return linkEnter;
|
|
||||||
};
|
|
||||||
export const arrowMarker = (graph: any) => {
|
|
||||||
const defs = graph.append("defs");
|
|
||||||
const arrow = defs
|
|
||||||
.append("marker")
|
|
||||||
.attr("id", "arrow")
|
|
||||||
.attr("class", "topo-line-arrow")
|
|
||||||
.attr("markerUnits", "strokeWidth")
|
|
||||||
.attr("markerWidth", "6")
|
|
||||||
.attr("markerHeight", "6")
|
|
||||||
.attr("viewBox", "0 0 12 12")
|
|
||||||
.attr("refX", "5")
|
|
||||||
.attr("refY", "6")
|
|
||||||
.attr("orient", "auto");
|
|
||||||
const arrowPath = "M2,2 L10,6 L2,10 L6,6 L2,2";
|
|
||||||
|
|
||||||
arrow.append("path").attr("d", arrowPath).attr("fill", "#217EF25f");
|
|
||||||
return arrow;
|
|
||||||
};
|
|
@ -1,85 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 icons from "@/assets/img/icons";
|
|
||||||
import type { Node } from "@/types/topology";
|
|
||||||
|
|
||||||
icons["KAFKA-CONSUMER"] = icons.KAFKA;
|
|
||||||
export default (d3: any, graph: any, funcs: any, tip: any, legend?: any) => {
|
|
||||||
const nodeEnter = graph
|
|
||||||
.append("g")
|
|
||||||
.call(d3.drag().on("start", funcs.dragstart).on("drag", funcs.dragged).on("end", funcs.dragended))
|
|
||||||
.on("mouseover", function (event: any, d: Node) {
|
|
||||||
tip.html(funcs.tipHtml).show(d, this);
|
|
||||||
})
|
|
||||||
.on("mouseout", function () {
|
|
||||||
tip.hide(this);
|
|
||||||
})
|
|
||||||
.on("click", (event: any, d: Node | any) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
funcs.handleNodeClick(d);
|
|
||||||
});
|
|
||||||
nodeEnter
|
|
||||||
.append("image")
|
|
||||||
.attr("width", 49)
|
|
||||||
.attr("height", 49)
|
|
||||||
.attr("x", 2)
|
|
||||||
.attr("y", 10)
|
|
||||||
.attr("style", "cursor: move;")
|
|
||||||
.attr("xlink:href", (d: { [key: string]: number }) => {
|
|
||||||
if (!legend) {
|
|
||||||
return icons.CUBE;
|
|
||||||
}
|
|
||||||
if (!legend.length) {
|
|
||||||
return icons.CUBE;
|
|
||||||
}
|
|
||||||
let c = true;
|
|
||||||
for (const l of legend) {
|
|
||||||
if (l.condition === "<") {
|
|
||||||
c = c && d[l.name] < Number(l.value);
|
|
||||||
} else {
|
|
||||||
c = c && d[l.name] > Number(l.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c && d.isReal ? icons.CUBEERROR : icons.CUBE;
|
|
||||||
});
|
|
||||||
nodeEnter
|
|
||||||
.append("image")
|
|
||||||
.attr("width", 32)
|
|
||||||
.attr("height", 32)
|
|
||||||
.attr("x", 6)
|
|
||||||
.attr("y", -10)
|
|
||||||
.attr("style", "opacity: 0.5;")
|
|
||||||
.attr("xlink:href", icons.LOCAL);
|
|
||||||
nodeEnter
|
|
||||||
.append("image")
|
|
||||||
.attr("width", 18)
|
|
||||||
.attr("height", 18)
|
|
||||||
.attr("x", 13)
|
|
||||||
.attr("y", -7)
|
|
||||||
.attr("xlink:href", (d: { type: string }) =>
|
|
||||||
!d.type || d.type === "N/A" ? icons.UNDEFINED : icons[d.type.toUpperCase().replace("-", "")],
|
|
||||||
);
|
|
||||||
nodeEnter
|
|
||||||
.append("text")
|
|
||||||
.attr("class", "topo-text")
|
|
||||||
.attr("text-anchor", "middle")
|
|
||||||
.attr("x", 22)
|
|
||||||
.attr("y", 70)
|
|
||||||
.text((d: { name: string }) => (d.name.length > 20 ? `${d.name.substring(0, 20)}...` : d.name));
|
|
||||||
return nodeEnter;
|
|
||||||
};
|
|
@ -1,46 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 const simulationInit = (d3: any, nodes: any, links: any, ticked: any) => {
|
|
||||||
const simulation = d3
|
|
||||||
.forceSimulation(nodes)
|
|
||||||
.force(
|
|
||||||
"collide",
|
|
||||||
d3.forceCollide().radius(() => 60),
|
|
||||||
)
|
|
||||||
.force("yPos", d3.forceY().strength(1))
|
|
||||||
.force("xPos", d3.forceX().strength(1))
|
|
||||||
.force("charge", d3.forceManyBody().strength(-520))
|
|
||||||
.force(
|
|
||||||
"link",
|
|
||||||
d3.forceLink(links).id((d: { id: string }) => d.id),
|
|
||||||
)
|
|
||||||
.force("center", d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2 - 20))
|
|
||||||
.on("tick", ticked)
|
|
||||||
.stop();
|
|
||||||
simulationSkip(d3, simulation, ticked);
|
|
||||||
return simulation;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const simulationSkip = (d3: any, simulation: any, ticked: any) => {
|
|
||||||
d3.timeout(() => {
|
|
||||||
const n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()));
|
|
||||||
for (let i = 0; i < n; i += 1) {
|
|
||||||
simulation.tick();
|
|
||||||
ticked();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
@ -245,6 +245,9 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
traceStore.resetState();
|
traceStore.resetState();
|
||||||
|
const config = props.data;
|
||||||
|
delete config.filters;
|
||||||
|
dashboardStore.setWidget(config);
|
||||||
});
|
});
|
||||||
watch(
|
watch(
|
||||||
() => [selectorStore.currentPod],
|
() => [selectorStore.currentPod],
|
||||||
@ -267,6 +270,7 @@ limitations under the License. -->
|
|||||||
watch(
|
watch(
|
||||||
() => appStore.durationTime,
|
() => appStore.durationTime,
|
||||||
() => {
|
() => {
|
||||||
|
duration.value = appStore.durationTime;
|
||||||
if (dashboardStore.entity === EntityType[1].value) {
|
if (dashboardStore.entity === EntityType[1].value) {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
@ -144,6 +144,10 @@ limitations under the License. -->
|
|||||||
await queryTraces();
|
await queryTraces();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (filters.isReadRecords) {
|
||||||
|
await queryTraces();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (dashboardStore.entity === EntityType[1].value) {
|
if (dashboardStore.entity === EntityType[1].value) {
|
||||||
await getService();
|
await getService();
|
||||||
}
|
}
|
||||||
@ -229,6 +233,9 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
traceStore.resetState();
|
traceStore.resetState();
|
||||||
|
const config = props.data;
|
||||||
|
delete config.filters;
|
||||||
|
dashboardStore.setWidget(config);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -74,7 +74,7 @@ limitations under the License. -->
|
|||||||
<h5 class="mb-10" v-if="currentSpan.attachedEvents && currentSpan.attachedEvents.length"> {{ t("events") }}. </h5>
|
<h5 class="mb-10" v-if="currentSpan.attachedEvents && currentSpan.attachedEvents.length"> {{ t("events") }}. </h5>
|
||||||
<div
|
<div
|
||||||
class="attach-events"
|
class="attach-events"
|
||||||
ref="timeline"
|
ref="eventGraph"
|
||||||
v-if="currentSpan.attachedEvents && currentSpan.attachedEvents.length"
|
v-if="currentSpan.attachedEvents && currentSpan.attachedEvents.length"
|
||||||
></div>
|
></div>
|
||||||
<el-button class="popup-btn" type="primary" @click="getTaceLogs">
|
<el-button class="popup-btn" type="primary" @click="getTaceLogs">
|
||||||
@ -131,8 +131,7 @@ limitations under the License. -->
|
|||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import type { PropType } from "vue";
|
import type { PropType } from "vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { DataSet, Timeline } from "vis-timeline/standalone";
|
import ListGraph from "../../utils/d3-trace-list";
|
||||||
import "vis-timeline/styles/vis-timeline-graph2d.css";
|
|
||||||
import copy from "@/utils/copy";
|
import copy from "@/utils/copy";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { dateFormat } from "@/utils/dateFormat";
|
import { dateFormat } from "@/utils/dateFormat";
|
||||||
@ -146,8 +145,6 @@ limitations under the License. -->
|
|||||||
});
|
});
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const traceStore = useTraceStore();
|
const traceStore = useTraceStore();
|
||||||
const timeline = ref<Nullable<HTMLDivElement>>(null);
|
|
||||||
const visGraph = ref<Nullable<any>>(null);
|
|
||||||
const pageNum = ref<number>(1);
|
const pageNum = ref<number>(1);
|
||||||
const showRelatedLogs = ref<boolean>(false);
|
const showRelatedLogs = ref<boolean>(false);
|
||||||
const showEventDetail = ref<boolean>(false);
|
const showEventDetail = ref<boolean>(false);
|
||||||
@ -156,6 +153,8 @@ limitations under the License. -->
|
|||||||
const total = computed(() =>
|
const total = computed(() =>
|
||||||
traceStore.traceList.length === pageSize ? pageSize * pageNum.value + 1 : pageSize * pageNum.value,
|
traceStore.traceList.length === pageSize ? pageSize * pageNum.value + 1 : pageSize * pageNum.value,
|
||||||
);
|
);
|
||||||
|
const tree = ref<any>(null);
|
||||||
|
const eventGraph = ref<Nullable<HTMLDivElement>>(null);
|
||||||
const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") => dayjs(date).format(pattern);
|
const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") => dayjs(date).format(pattern);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -180,52 +179,46 @@ limitations under the License. -->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function visTimeline() {
|
function visTimeline() {
|
||||||
if (!timeline.value) {
|
if (!eventGraph.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (visGraph.value) {
|
|
||||||
visGraph.value.destroy();
|
|
||||||
}
|
|
||||||
const h = timeline.value.getBoundingClientRect().height;
|
|
||||||
const attachedEvents = props.currentSpan.attachedEvents || [];
|
const attachedEvents = props.currentSpan.attachedEvents || [];
|
||||||
const events: any[] = attachedEvents.map((d: SpanAttachedEvent, index: number) => {
|
const events: any[] = attachedEvents
|
||||||
let startTimeNanos = String(d.startTime.nanos).slice(-6).padStart(6, "0");
|
.map((d: SpanAttachedEvent) => {
|
||||||
let endTimeNanos = String(d.endTime.nanos).slice(-6).padStart(6, "0");
|
let startTimeNanos = String(d.startTime.nanos).slice(-6).padStart(6, "0");
|
||||||
endTimeNanos = toString(endTimeNanos);
|
let endTimeNanos = String(d.endTime.nanos).slice(-6).padStart(6, "0");
|
||||||
startTimeNanos = toString(startTimeNanos);
|
endTimeNanos = toString(endTimeNanos);
|
||||||
return {
|
startTimeNanos = toString(startTimeNanos);
|
||||||
id: index + 1,
|
const startTime = d.startTime.seconds * 1000 + d.startTime.nanos / 1000000;
|
||||||
content: d.event,
|
const endTime = d.endTime.seconds * 1000 + d.endTime.nanos / 1000000;
|
||||||
start: new Date(Number(d.startTime.seconds * 1000 + d.startTime.nanos / 1000000)),
|
return {
|
||||||
end: new Date(Number(d.endTime.seconds * 1000 + d.endTime.nanos / 1000000)),
|
label: d.event,
|
||||||
...d,
|
...d,
|
||||||
startTime: d.startTime.seconds * 1000 + d.startTime.nanos / 1000000,
|
startTime,
|
||||||
endTime: d.endTime.seconds * 1000 + d.endTime.nanos / 1000000,
|
endTime,
|
||||||
className: "Normal",
|
startTimeNanos,
|
||||||
startTimeNanos,
|
endTimeNanos,
|
||||||
endTimeNanos,
|
};
|
||||||
};
|
})
|
||||||
});
|
.sort((a: { startTime: number; endTime: number }, b: { startTime: number; endTime: number }) => {
|
||||||
|
return a.startTime - b.startTime;
|
||||||
|
});
|
||||||
|
|
||||||
const items = new DataSet(events);
|
tree.value = new ListGraph(eventGraph.value, selectEvent);
|
||||||
const options: Recordable = {
|
tree.value.init(
|
||||||
height: h,
|
{
|
||||||
width: "100%",
|
children: events,
|
||||||
locale: "en",
|
label: "",
|
||||||
groupHeightMode: "fitItems",
|
},
|
||||||
zoomMin: 80,
|
events,
|
||||||
};
|
0,
|
||||||
|
);
|
||||||
|
tree.value.draw();
|
||||||
|
}
|
||||||
|
|
||||||
visGraph.value = new Timeline(timeline.value, items, options);
|
function selectEvent(i: any) {
|
||||||
visGraph.value.on("select", (data: { items: number[] }) => {
|
currentEvent.value = i.data;
|
||||||
const index = data.items[0];
|
showEventDetail.value = true;
|
||||||
currentEvent.value = events[index - 1 || 0] || {};
|
|
||||||
if (data.items.length) {
|
|
||||||
showEventDetail.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showEventDetail.value = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
function toString(str: string) {
|
function toString(str: string) {
|
||||||
return str.replace(/\d(?=(\d{3})+$)/g, "$&,");
|
return str.replace(/\d(?=(\d{3})+$)/g, "$&,");
|
||||||
@ -247,7 +240,8 @@ limitations under the License. -->
|
|||||||
.attach-events {
|
.attach-events {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 5px 5px 0;
|
margin: 0 5px 5px 0;
|
||||||
height: 200px;
|
height: 400px;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-btn {
|
.popup-btn {
|
||||||
|
@ -17,14 +17,14 @@ limitations under the License. -->
|
|||||||
<div v-if="type === 'statistics'">
|
<div v-if="type === 'statistics'">
|
||||||
<div class="trace-item">
|
<div class="trace-item">
|
||||||
<div :class="['method']">
|
<div :class="['method']">
|
||||||
<el-tooltip :content="data.groupRef.endpointName" placement="bottom">
|
<el-tooltip :content="data.groupRef.endpointName" placement="bottom" :show-after="300">
|
||||||
<span>
|
<span>
|
||||||
{{ data.groupRef.endpointName }}
|
{{ data.groupRef.endpointName }}
|
||||||
</span>
|
</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div :class="['type']">
|
<div :class="['type']">
|
||||||
<el-tooltip :content="data.groupRef.type" placement="bottom">
|
<el-tooltip :content="data.groupRef.type" placement="bottom" :show-after="300">
|
||||||
<span>
|
<span>
|
||||||
{{ data.groupRef.type }}
|
{{ data.groupRef.type }}
|
||||||
</span>
|
</span>
|
||||||
@ -48,7 +48,16 @@ limitations under the License. -->
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div @click="selectSpan" :class="['trace-item', 'level' + (data.level - 1), { 'trace-item-error': data.isError }]">
|
<div
|
||||||
|
@click="selectSpan"
|
||||||
|
:class="[
|
||||||
|
'trace-item',
|
||||||
|
'level' + (data.level - 1),
|
||||||
|
{ 'trace-item-error': data.isError },
|
||||||
|
{ profiled: data.profiled === false },
|
||||||
|
]"
|
||||||
|
:data-text="data.profiled === false ? 'No Thread Dump' : ''"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
:class="['method', 'level' + (data.level - 1)]"
|
:class="['method', 'level' + (data.level - 1)]"
|
||||||
:style="{
|
:style="{
|
||||||
@ -62,8 +71,24 @@ limitations under the License. -->
|
|||||||
v-if="data.children && data.children.length"
|
v-if="data.children && data.children.length"
|
||||||
iconName="arrow-down"
|
iconName="arrow-down"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
class="mr-5"
|
||||||
/>
|
/>
|
||||||
<el-tooltip :content="data.endpointName" placement="bottom">
|
<el-tooltip
|
||||||
|
:content="data.type === 'Entry' ? 'Entry' : 'Exit'"
|
||||||
|
placement="bottom"
|
||||||
|
:show-after="300"
|
||||||
|
v-if="['Entry', 'Exit'].includes(data.type)"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Icon :iconName="data.type === 'Entry' ? 'entry' : 'exit'" size="sm" class="mr-5" />
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip v-if="isCrossThread" content="CROSS_THREAD" placement="bottom" :show-after="300">
|
||||||
|
<span>
|
||||||
|
<Icon iconName="cross" size="sm" class="mr-5" />
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip :content="data.endpointName" placement="bottom" :show-after="300">
|
||||||
<span>
|
<span>
|
||||||
{{ data.endpointName }}
|
{{ data.endpointName }}
|
||||||
</span>
|
</span>
|
||||||
@ -84,12 +109,12 @@ limitations under the License. -->
|
|||||||
{{ data.dur ? data.dur + "" : "0" }}
|
{{ data.dur ? data.dur + "" : "0" }}
|
||||||
</div>
|
</div>
|
||||||
<div class="api">
|
<div class="api">
|
||||||
<el-tooltip :content="data.component || '-'" placement="bottom">
|
<el-tooltip :show-after="300" :content="data.component || '-'" placement="bottom">
|
||||||
<span>{{ data.component || "-" }}</span>
|
<span>{{ data.component || "-" }}</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="application">
|
<div class="application">
|
||||||
<el-tooltip :content="data.serviceCode || '-'" placement="bottom">
|
<el-tooltip :show-after="300" :content="data.serviceCode || '-'" placement="bottom">
|
||||||
<span>{{ data.serviceCode }}</span>
|
<span>{{ data.serviceCode }}</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@ -162,6 +187,10 @@ limitations under the License. -->
|
|||||||
const resultStr = result.toFixed(4) + "%";
|
const resultStr = result.toFixed(4) + "%";
|
||||||
return resultStr === "0.0000%" ? "0.9%" : resultStr;
|
return resultStr === "0.0000%" ? "0.9%" : resultStr;
|
||||||
});
|
});
|
||||||
|
const isCrossThread = computed(() => {
|
||||||
|
const key = props.data.refs.findIndex((d: { type: string }) => d.type === "CROSS_THREAD");
|
||||||
|
return key > -1 ? true : false;
|
||||||
|
});
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
displayChildren.value = !displayChildren.value;
|
displayChildren.value = !displayChildren.value;
|
||||||
@ -175,6 +204,10 @@ limitations under the License. -->
|
|||||||
item.style.background = "#fff";
|
item.style.background = "#fff";
|
||||||
}
|
}
|
||||||
dom.style.background = "rgba(0, 0, 0, 0.1)";
|
dom.style.background = "rgba(0, 0, 0, 0.1)";
|
||||||
|
const p: any = document.getElementsByClassName("profiled")[0];
|
||||||
|
if (p) {
|
||||||
|
p.style.background = "#eee";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function selectSpan(event: Recordable) {
|
function selectSpan(event: Recordable) {
|
||||||
const dom = event.composedPath().find((d: Recordable) => d.className.includes("trace-item"));
|
const dom = event.composedPath().find((d: Recordable) => d.className.includes("trace-item"));
|
||||||
@ -203,6 +236,7 @@ limitations under the License. -->
|
|||||||
displayChildren,
|
displayChildren,
|
||||||
outterPercent,
|
outterPercent,
|
||||||
innerPercent,
|
innerPercent,
|
||||||
|
isCrossThread,
|
||||||
viewSpanDetail,
|
viewSpanDetail,
|
||||||
toggle,
|
toggle,
|
||||||
dateFormat,
|
dateFormat,
|
||||||
@ -234,17 +268,44 @@ limitations under the License. -->
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(0, 0, 0, 0.04);
|
background: rgba(0, 0, 0, 0.04);
|
||||||
color: #448dfe;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&::before {
|
.profiled {
|
||||||
position: absolute;
|
background-color: #eee;
|
||||||
content: "";
|
position: relative;
|
||||||
width: 5px;
|
}
|
||||||
height: 100%;
|
|
||||||
background: #448dfe;
|
.profiled:before {
|
||||||
left: 0;
|
content: attr(data-text);
|
||||||
}
|
position: absolute;
|
||||||
|
top: 30px;
|
||||||
|
left: 220px;
|
||||||
|
width: 100px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: #eee 1px 2px 10px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profiled:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 250px;
|
||||||
|
top: 20px;
|
||||||
|
border: 6px solid #333;
|
||||||
|
border-color: transparent transparent #333 transparent;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profiled:hover:before,
|
||||||
|
.profiled:hover:after {
|
||||||
|
display: block;
|
||||||
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-item-error {
|
.trace-item-error {
|
||||||
|
@ -18,6 +18,8 @@
|
|||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import d3tip from "d3-tip";
|
import d3tip from "d3-tip";
|
||||||
import type { Trace } from "@/types/trace";
|
import type { Trace } from "@/types/trace";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import icons from "@/assets/img/icons";
|
||||||
|
|
||||||
export default class ListGraph {
|
export default class ListGraph {
|
||||||
private barHeight = 48;
|
private barHeight = 48;
|
||||||
@ -28,6 +30,7 @@ export default class ListGraph {
|
|||||||
private height = 0;
|
private height = 0;
|
||||||
private svg: any = null;
|
private svg: any = null;
|
||||||
private tip: any = null;
|
private tip: any = null;
|
||||||
|
private prompt: any = null;
|
||||||
private row: any[] = [];
|
private row: any[] = [];
|
||||||
private data: any = [];
|
private data: any = [];
|
||||||
private min = 0;
|
private min = 0;
|
||||||
@ -63,7 +66,14 @@ export default class ListGraph {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
this.prompt = (d3tip as any)()
|
||||||
|
.attr("class", "d3-tip")
|
||||||
|
.offset([-8, 0])
|
||||||
|
.html((d: any) => {
|
||||||
|
return `<div class="mb-5">${d.data.type}</div>`;
|
||||||
|
});
|
||||||
this.svg.call(this.tip);
|
this.svg.call(this.tip);
|
||||||
|
this.svg.call(this.prompt);
|
||||||
}
|
}
|
||||||
diagonal(d: Recordable) {
|
diagonal(d: Recordable) {
|
||||||
return `M ${d.source.y} ${d.source.x + 5}
|
return `M ${d.source.y} ${d.source.x + 5}
|
||||||
@ -91,7 +101,6 @@ export default class ListGraph {
|
|||||||
this.svg
|
this.svg
|
||||||
.append("g")
|
.append("g")
|
||||||
.attr("class", "trace-xaxis")
|
.attr("class", "trace-xaxis")
|
||||||
|
|
||||||
.attr("transform", `translate(${this.width * 0.618 - 20},${30})`)
|
.attr("transform", `translate(${this.width * 0.618 - 20},${30})`)
|
||||||
.call(this.xAxis);
|
.call(this.xAxis);
|
||||||
this.sequentialScale = d3
|
this.sequentialScale = d3
|
||||||
@ -152,6 +161,55 @@ export default class ListGraph {
|
|||||||
.attr("x", 20)
|
.attr("x", 20)
|
||||||
.attr("width", "100%")
|
.attr("width", "100%")
|
||||||
.attr("fill", "rgba(0,0,0,0)");
|
.attr("fill", "rgba(0,0,0,0)");
|
||||||
|
nodeEnter
|
||||||
|
.append("image")
|
||||||
|
.attr("width", 16)
|
||||||
|
.attr("height", 16)
|
||||||
|
.attr("x", 6)
|
||||||
|
.attr("y", -10)
|
||||||
|
.attr("xlink:href", (d: any) =>
|
||||||
|
d.data.type === "Entry" ? icons.ENTRY : d.data.type === "Exit" ? icons.EXIT : "",
|
||||||
|
)
|
||||||
|
.style("display", (d: any) => {
|
||||||
|
["Entry", "Exit"].includes(d.data.type) ? "inline" : "none";
|
||||||
|
})
|
||||||
|
.on("mouseover", function (event: any, d: Trace) {
|
||||||
|
event.stopPropagation();
|
||||||
|
t.prompt.show(d, this);
|
||||||
|
})
|
||||||
|
.on("mouseout", function (event: any, d: Trace) {
|
||||||
|
event.stopPropagation();
|
||||||
|
t.prompt.hide(d, this);
|
||||||
|
});
|
||||||
|
nodeEnter
|
||||||
|
.append("image")
|
||||||
|
.attr("width", 16)
|
||||||
|
.attr("height", 16)
|
||||||
|
.attr("x", 6)
|
||||||
|
.attr("y", -10)
|
||||||
|
.attr("xlink:href", (d: any) => {
|
||||||
|
const key = (d.data.refs || []).findIndex((d: { type: string }) => d.type === "CROSS_THREAD");
|
||||||
|
return key > -1 ? icons.STREAM : "";
|
||||||
|
})
|
||||||
|
.style("display", (d: any) => {
|
||||||
|
const key = (d.data.refs || []).findIndex((d: { type: string }) => d.type === "CROSS_THREAD");
|
||||||
|
return key > -1 ? "inline" : "none";
|
||||||
|
})
|
||||||
|
.on("mouseover", function (event: any, d: any) {
|
||||||
|
const a = {
|
||||||
|
...d,
|
||||||
|
data: {
|
||||||
|
...d.data,
|
||||||
|
type: "CROSS_THREAD",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
event.stopPropagation();
|
||||||
|
t.prompt.show(a, this);
|
||||||
|
})
|
||||||
|
.on("mouseout", function (event: any, d: Trace) {
|
||||||
|
event.stopPropagation();
|
||||||
|
t.prompt.hide(d, this);
|
||||||
|
});
|
||||||
nodeEnter
|
nodeEnter
|
||||||
.append("text")
|
.append("text")
|
||||||
.attr("x", 13)
|
.attr("x", 13)
|
||||||
@ -216,7 +274,13 @@ export default class ListGraph {
|
|||||||
.style("font-size", "11px")
|
.style("font-size", "11px")
|
||||||
.text(
|
.text(
|
||||||
(d: Recordable) =>
|
(d: Recordable) =>
|
||||||
`${d.data.layer || ""} ${d.data.component ? "- " + d.data.component : d.data.component || ""}`,
|
`${d.data.layer || ""} ${
|
||||||
|
d.data.component
|
||||||
|
? "- " + d.data.component
|
||||||
|
: d.data.event
|
||||||
|
? this.visDate(d.data.startTime) + ":" + d.data.startTimeNanos
|
||||||
|
: ""
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
nodeEnter
|
nodeEnter
|
||||||
.append("rect")
|
.append("rect")
|
||||||
@ -310,6 +374,9 @@ export default class ListGraph {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
visDate(date: number, pattern = "YYYY-MM-DD HH:mm:ss:SSS") {
|
||||||
|
return dayjs(date).format(pattern);
|
||||||
|
}
|
||||||
resize() {
|
resize() {
|
||||||
if (!this.el) {
|
if (!this.el) {
|
||||||
return;
|
return;
|
||||||
|
@ -85,13 +85,17 @@ export default ({ mode }: ConfigEnv): UserConfig => {
|
|||||||
if (id.includes("node_modules")) {
|
if (id.includes("node_modules")) {
|
||||||
if (id.includes("lodash")) {
|
if (id.includes("lodash")) {
|
||||||
return "lodash";
|
return "lodash";
|
||||||
} else if (id.includes("echarts")) {
|
}
|
||||||
|
if (id.includes("echarts")) {
|
||||||
return "echarts";
|
return "echarts";
|
||||||
} else if (id.includes("element-plus")) {
|
}
|
||||||
|
if (id.includes("element-plus")) {
|
||||||
return "element-plus";
|
return "element-plus";
|
||||||
} else if (id.includes("monaco-editor")) {
|
}
|
||||||
|
if (id.includes("monaco-editor")) {
|
||||||
return "monaco-editor";
|
return "monaco-editor";
|
||||||
} else if (id.includes("d3")) {
|
}
|
||||||
|
if (id.includes("d3")) {
|
||||||
return "d3";
|
return "d3";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|