fix: merge

This commit is contained in:
Fine 2023-04-06 18:08:21 +08:00
commit 09bff9700f
80 changed files with 2069 additions and 831 deletions

View File

@ -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
View File

@ -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",

View File

@ -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"

View File

@ -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>

View 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

View 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

View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

View File

@ -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) => {

View File

@ -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 {

View File

@ -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}) {

View File

@ -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",

View File

@ -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 };
} }

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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;
} }

View File

@ -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;

View File

@ -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;

View File

@ -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
View 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 };

View File

@ -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",
},
],
},
], ],
}, },
]; ];

View File

@ -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",
},
},
], ],
}, },
]; ];

View File

@ -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",
},
},
], ],
}, },
]; ];

View File

@ -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",
},
}, },
], ],
}, },

View File

@ -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",
},
},
], ],
}, },
]; ];

View File

@ -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",
},
},
], ],
}, },
]; ];

View File

@ -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"];

View File

@ -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) {

View File

@ -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 = [];

View File

@ -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) {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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
View 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";

View File

@ -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;
} }

View File

@ -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 {

View File

@ -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();

View File

@ -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 {

View 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>

View 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>

View 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>

View File

@ -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,
}; };

View File

@ -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;

View 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>

View File

@ -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;

View File

@ -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,

View File

@ -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,
}; };

View File

@ -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,
}; };

View File

@ -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" },
];

View File

@ -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: {

View File

@ -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>

View File

@ -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,

View File

@ -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,

View File

@ -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");
} }

View File

@ -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) {

View File

@ -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();
} }

View File

@ -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 {

View File

@ -104,7 +104,6 @@ limitations under the License. -->
() => selectorStore.currentService, () => selectorStore.currentService,
() => { () => {
searchTasks(); searchTasks();
console.log("service");
}, },
); );
watch( watch(

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View 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 },
];
}

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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();
}
});
};

View File

@ -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();
} }

View File

@ -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>

View File

@ -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 {

View File

@ -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 {

View File

@ -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;

View File

@ -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";
} }
} }