feat: implement Topology on the dashboard (#14)

This commit is contained in:
Fine0830 2022-02-19 23:05:57 +08:00 committed by GitHub
parent 7472d70720
commit f53b422782
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 4886 additions and 232 deletions

5
dist/LICENSE vendored
View File

@ -18,7 +18,7 @@ three-orbit-controls 82.1.0: https://github.com/mattdesl/three-orbit-controls MI
vue-demi 0.12.1: https://github.com/antfu/vue-demi MIT vue-demi 0.12.1: https://github.com/antfu/vue-demi MIT
vueuse/core 6.8.0: https://github.com/vueuse/vueuse MIT vueuse/core 6.8.0: https://github.com/vueuse/vueuse MIT
vue/devtools-api 6.0.0-beta.20.1: https://github.com/vuejs/vue-devtools MIT vue/devtools-api 6.0.0-beta.20.1: https://github.com/vuejs/vue-devtools MIT
element-plus 1.2.0-beta.3: https://github.com/element-plus/element-plus MIT element-plus 2.0.1: https://github.com/element-plus/element-plus MIT
vue-types 4.1.1: https://github.com/dwightjack/vue-types MIT vue-types 4.1.1: https://github.com/dwightjack/vue-types MIT
is-plain-object 5.0.0: https://github.com/jonschlinkert/is-plain-object MIT is-plain-object 5.0.0: https://github.com/jonschlinkert/is-plain-object MIT
vue-grid-layout 3.0.0-beta1: https://github.com/jbaysolutions/vue-grid-layout MIT vue-grid-layout 3.0.0-beta1: https://github.com/jbaysolutions/vue-grid-layout MIT
@ -29,3 +29,6 @@ batch-processor 1.0.0: https://github.com/wnr/batch-processor MIT
echarts 5.2.2: https://github.com/apache/echarts Apache-2.0 License echarts 5.2.2: https://github.com/apache/echarts Apache-2.0 License
zrender 5.2.1: https://github.com/ecomfe/zrender BSD-3-Clause License zrender 5.2.1: https://github.com/ecomfe/zrender BSD-3-Clause License
tslib 2.3.0: https://github.com/Microsoft/tslib 0BSD License tslib 2.3.0: https://github.com/Microsoft/tslib 0BSD License
d3-tip 0.9.1: https: //github.com/Caged/d3-tip MIT Licensee
d3 7.3.0: https://github.com/d3/d3 ISC License
ctrl/tinycolor 3.4.0: https: //github.com/scttcper/tinycolor MIT Licensee

21
dist/licenses/LICENSE-ctrl-tinycolor vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015-present Evan You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

1460
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,10 @@
}, },
"dependencies": { "dependencies": {
"axios": "^0.24.0", "axios": "^0.24.0",
"d3": "^7.3.0",
"d3-tip": "^0.9.1",
"echarts": "^5.2.2", "echarts": "^5.2.2",
"element-plus": "^1.2.0-beta.3", "element-plus": "^2.0.2",
"pinia": "^2.0.5", "pinia": "^2.0.5",
"three": "^0.131.3", "three": "^0.131.3",
"three-orbit-controls": "^82.1.0", "three-orbit-controls": "^82.1.0",
@ -24,6 +26,8 @@
"vuex": "^4.0.0-0" "vuex": "^4.0.0-0"
}, },
"devDependencies": { "devDependencies": {
"@types/d3": "^7.1.0",
"@types/d3-tip": "^3.5.5",
"@types/echarts": "^4.9.12", "@types/echarts": "^4.9.12",
"@types/jest": "^24.0.19", "@types/jest": "^24.0.19",
"@types/three": "^0.131.0", "@types/three": "^0.131.0",

View File

@ -13,6 +13,5 @@ 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. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>all_inbox</title>
<path d="M15 15.984h6v3q0 0.797-0.609 1.406t-1.406 0.609h-13.969q-0.797 0-1.406-0.609t-0.609-1.406v-3h6q0 1.219 0.891 2.109t2.109 0.891 2.109-0.891 0.891-2.109zM18.984 9v-3.984h-13.969v3.984h3.984q0 1.219 0.891 2.109t2.109 0.891 2.109-0.891 0.891-2.109h3.984zM18.984 3q0.797 0 1.406 0.609t0.609 1.406v6.984q0 0.797-0.609 1.406t-1.406 0.609h-13.969q-0.797 0-1.406-0.609t-0.609-1.406v-6.984q0-0.797 0.609-1.406t1.406-0.609h13.969z"></path> <path d="M15 15.984h6v3q0 0.797-0.609 1.406t-1.406 0.609h-13.969q-0.797 0-1.406-0.609t-0.609-1.406v-3h6q0 1.219 0.891 2.109t2.109 0.891 2.109-0.891 0.891-2.109zM18.984 9v-3.984h-13.969v3.984h3.984q0 1.219 0.891 2.109t2.109 0.891 2.109-0.891 0.891-2.109h3.984zM18.984 3q0.797 0 1.406 0.609t0.609 1.406v6.984q0 0.797-0.609 1.406t-1.406 0.609h-13.969q-0.797 0-1.406-0.609t-0.609-1.406v-6.984q0-0.797 0.609-1.406t1.406-0.609h13.969z"></path>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -13,4 +13,4 @@ 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. -->
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/chevron-left</title><path d="M7.414 7.989l2.295 2.306a1 1 0 1 1-1.418 1.41l-3-3.015a1 1 0 0 1 .004-1.414l3-2.985a1 1 0 1 1 1.41 1.418l-2.29 2.28z" id="a"/></svg> <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M7.414 7.989l2.295 2.306a1 1 0 1 1-1.418 1.41l-3-3.015a1 1 0 0 1 .004-1.414l3-2.985a1 1 0 1 1 1.41 1.418l-2.29 2.28z" id="a"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

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. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M17.016 15.984h3.984v5.016h-5.016v-3.047l-3.984-4.219-3.984 4.219v3.047h-5.016v-5.016h3.984l4.031-3.984v-3.188q-0.891-0.328-1.453-1.078t-0.563-1.734q0-1.219 0.891-2.109t2.109-0.891 2.109 0.891 0.891 2.109q0 0.984-0.563 1.734t-1.453 1.078v3.188z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -13,6 +13,5 @@ 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. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>folder_open</title>
<path d="M20.016 18v-9.984h-16.031v9.984h16.031zM20.016 6q0.797 0 1.383 0.609t0.586 1.406v9.984q0 0.797-0.586 1.406t-1.383 0.609h-16.031q-0.797 0-1.383-0.609t-0.586-1.406v-12q0-0.797 0.586-1.406t1.383-0.609h6l2.016 2.016h8.016z"></path> <path d="M20.016 18v-9.984h-16.031v9.984h16.031zM20.016 6q0.797 0 1.383 0.609t0.586 1.406v9.984q0 0.797-0.586 1.406t-1.383 0.609h-16.031q-0.797 0-1.383-0.609t-0.586-1.406v-12q0-0.797 0.586-1.406t1.383-0.609h6l2.016 2.016h8.016z"></path>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -13,6 +13,5 @@ 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. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>insert_image</title>
<path d="M8.484 13.5l-3.469 4.5h13.969l-4.5-6-3.469 4.5zM21 18.984q0 0.797-0.609 1.406t-1.406 0.609h-13.969q-0.797 0-1.406-0.609t-0.609-1.406v-13.969q0-0.797 0.609-1.406t1.406-0.609h13.969q0.797 0 1.406 0.609t0.609 1.406v13.969z"></path> <path d="M8.484 13.5l-3.469 4.5h13.969l-4.5-6-3.469 4.5zM21 18.984q0 0.797-0.609 1.406t-1.406 0.609h-13.969q-0.797 0-1.406-0.609t-0.609-1.406v-13.969q0-0.797 0.609-1.406t1.406-0.609h13.969q0.797 0 1.406 0.609t0.609 1.406v13.969z"></path>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

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. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M21 11.016v1.969h-14.156l3.563 3.609-1.406 1.406-6-6 6-6 1.406 1.406-3.563 3.609h14.156z"></path>
</svg>

After

Width:  |  Height:  |  Size: 977 B

View File

@ -13,6 +13,5 @@ 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. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>playlist_add</title>
<path d="M2.016 15.984v-1.969h7.969v1.969h-7.969zM18 14.016h3.984v1.969h-3.984v4.031h-2.016v-4.031h-3.984v-1.969h3.984v-4.031h2.016v4.031zM14.016 6v2.016h-12v-2.016h12zM14.016 9.984v2.016h-12v-2.016h12z"></path> <path d="M2.016 15.984v-1.969h7.969v1.969h-7.969zM18 14.016h3.984v1.969h-3.984v4.031h-2.016v-4.031h-3.984v-1.969h3.984v-4.031h2.016v4.031zM14.016 6v2.016h-12v-2.016h12zM14.016 9.984v2.016h-12v-2.016h12z"></path>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -13,6 +13,5 @@ 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. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>save</title>
<path d="M15 9v-3.984h-9.984v3.984h9.984zM12 18.984q1.219 0 2.109-0.891t0.891-2.109-0.891-2.109-2.109-0.891-2.109 0.891-0.891 2.109 0.891 2.109 2.109 0.891zM17.016 3l3.984 3.984v12q0 0.797-0.609 1.406t-1.406 0.609h-13.969q-0.844 0-1.43-0.586t-0.586-1.43v-13.969q0-0.844 0.586-1.43t1.43-0.586h12z"></path> <path d="M15 9v-3.984h-9.984v3.984h9.984zM12 18.984q1.219 0 2.109-0.891t0.891-2.109-0.891-2.109-2.109-0.891-2.109 0.891-0.891 2.109 0.891 2.109 2.109 0.891zM17.016 3l3.984 3.984v12q0 0.797-0.609 1.406t-1.406 0.609h-13.969q-0.844 0-1.43-0.586t-0.586-1.43v-13.969q0-0.844 0.586-1.43t1.43-0.586h12z"></path>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -13,6 +13,5 @@ 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. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>save_alt</title>
<path d="M12.984 12.656l2.625-2.578 1.406 1.406-5.016 5.016-5.016-5.016 1.406-1.406 2.625 2.578v-9.656h1.969v9.656zM18.984 12h2.016v6.984q0 0.797-0.609 1.406t-1.406 0.609h-13.969q-0.797 0-1.406-0.609t-0.609-1.406v-6.984h2.016v6.984h13.969v-6.984z"></path> <path d="M12.984 12.656l2.625-2.578 1.406 1.406-5.016 5.016-5.016-5.016 1.406-1.406 2.625 2.578v-9.656h1.969v9.656zM18.984 12h2.016v6.984q0 0.797-0.609 1.406t-1.406 0.609h-13.969q-0.797 0-1.406-0.609t-0.609-1.406v-6.984h2.016v6.984h13.969v-6.984z"></path>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -13,6 +13,5 @@ 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. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>settings</title>
<path d="M12 15.516q1.453 0 2.484-1.031t1.031-2.484-1.031-2.484-2.484-1.031-2.484 1.031-1.031 2.484 1.031 2.484 2.484 1.031zM19.453 12.984l2.109 1.641q0.328 0.234 0.094 0.656l-2.016 3.469q-0.188 0.328-0.609 0.188l-2.484-0.984q-0.984 0.703-1.688 0.984l-0.375 2.625q-0.094 0.422-0.469 0.422h-4.031q-0.375 0-0.469-0.422l-0.375-2.625q-0.891-0.375-1.688-0.984l-2.484 0.984q-0.422 0.141-0.609-0.188l-2.016-3.469q-0.234-0.422 0.094-0.656l2.109-1.641q-0.047-0.328-0.047-0.984t0.047-0.984l-2.109-1.641q-0.328-0.234-0.094-0.656l2.016-3.469q0.188-0.328 0.609-0.188l2.484 0.984q0.984-0.703 1.688-0.984l0.375-2.625q0.094-0.422 0.469-0.422h4.031q0.375 0 0.469 0.422l0.375 2.625q0.891 0.375 1.688 0.984l2.484-0.984q0.422-0.141 0.609 0.188l2.016 3.469q0.234 0.422-0.094 0.656l-2.109 1.641q0.047 0.328 0.047 0.984t-0.047 0.984z"></path> <path d="M12 15.516q1.453 0 2.484-1.031t1.031-2.484-1.031-2.484-2.484-1.031-2.484 1.031-1.031 2.484 1.031 2.484 2.484 1.031zM19.453 12.984l2.109 1.641q0.328 0.234 0.094 0.656l-2.016 3.469q-0.188 0.328-0.609 0.188l-2.484-0.984q-0.984 0.703-1.688 0.984l-0.375 2.625q-0.094 0.422-0.469 0.422h-4.031q-0.375 0-0.469-0.422l-0.375-2.625q-0.891-0.375-1.688-0.984l-2.484 0.984q-0.422 0.141-0.609-0.188l-2.016-3.469q-0.234-0.422 0.094-0.656l2.109-1.641q-0.047-0.328-0.047-0.984t0.047-0.984l-2.109-1.641q-0.328-0.234-0.094-0.656l2.016-3.469q0.188-0.328 0.609-0.188l2.484 0.984q0.984-0.703 1.688-0.984l0.375-2.625q0.094-0.422 0.469-0.422h4.031q0.375 0 0.469 0.422l0.375 2.625q0.891 0.375 1.688 0.984l2.484-0.984q0.422-0.141 0.609 0.188l2.016 3.469q0.234 0.422-0.094 0.656l-2.109 1.641q0.047 0.328 0.047 0.984t-0.047 0.984z"></path>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 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="1645261422781" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1440" width="48" height="48"><path d="M900.032 646.016h-56.064V502.976a16 16 0 0 0-16-16H544v-96h62.976c22.144 0 40-17.92 40-40V161.024a40 40 0 0 0-40-40H417.024a40 40 0 0 0-40 40v189.952c0 22.144 17.92 40 40 40H480v96H195.968a16 16 0 0 0-16 16v143.04h-55.936a38.016 38.016 0 0 0-38.016 38.016v176c0 20.928 17.024 37.952 37.952 37.952h176a38.016 38.016 0 0 0 38.016-38.016v-176a38.016 38.016 0 0 0-37.952-37.952h-56V550.976H480v95.04h-56a38.016 38.016 0 0 0-38.016 38.016v176c0 20.928 17.024 37.952 38.016 37.952h176a38.016 38.016 0 0 0 38.016-38.016v-176a38.016 38.016 0 0 0-38.016-37.952H544V550.976h236.032v95.04h-56.064a38.016 38.016 0 0 0-37.952 38.016v176c0 20.928 17.024 37.952 38.016 37.952h176a38.016 38.016 0 0 0 37.952-38.016v-176a38.016 38.016 0 0 0-38.016-37.952zM440.96 184.96h141.952v141.952H441.024V185.024zM278.016 838.016H145.92V705.92h132.032v132.032z m299.968 0H446.08V705.92H577.92v132.032z m300.032 0h-132.032V705.92h132.032v132.032z" p-id="1441"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 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="1645261422781" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1440" width="48" height="48"><path d="M900.032 646.016h-56.064V502.976a16 16 0 0 0-16-16H544v-96h62.976c22.144 0 40-17.92 40-40V161.024a40 40 0 0 0-40-40H417.024a40 40 0 0 0-40 40v189.952c0 22.144 17.92 40 40 40H480v96H195.968a16 16 0 0 0-16 16v143.04h-55.936a38.016 38.016 0 0 0-38.016 38.016v176c0 20.928 17.024 37.952 37.952 37.952h176a38.016 38.016 0 0 0 38.016-38.016v-176a38.016 38.016 0 0 0-37.952-37.952h-56V550.976H480v95.04h-56a38.016 38.016 0 0 0-38.016 38.016v176c0 20.928 17.024 37.952 38.016 37.952h176a38.016 38.016 0 0 0 38.016-38.016v-176a38.016 38.016 0 0 0-38.016-37.952H544V550.976h236.032v95.04h-56.064a38.016 38.016 0 0 0-37.952 38.016v176c0 20.928 17.024 37.952 38.016 37.952h176a38.016 38.016 0 0 0 37.952-38.016v-176a38.016 38.016 0 0 0-38.016-37.952zM440.96 184.96h141.952v141.952H441.024V185.024zM278.016 838.016H145.92V705.92h132.032v132.032z m299.968 0H446.08V705.92H577.92v132.032z m300.032 0h-132.032V705.92h132.032v132.032z" p-id="1441" fill="#ffffff"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

49
src/assets/img/icons.ts Executable file
View File

@ -0,0 +1,49 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const requireComponent = require.context("./technologies", false, /\.png$/);
const requireTool = require.context("./tools", false, /\.png$/);
const result: { [key: string]: string } = {};
const t: { [key: string]: string } = {};
function capitalizeFirstLetter(str: string) {
return str.toUpperCase();
}
function validateFileName(str: string): string | undefined {
if (/^\S+\.png$/.test(str)) {
return str.replace(/^\S+\/(\w+)\.png$/, (rs, $1) =>
capitalizeFirstLetter($1)
);
}
}
[...requireComponent.keys()].forEach((filePath: string) => {
const componentConfig = requireComponent(filePath);
const fileName = validateFileName(filePath);
if (fileName) {
result[fileName] = componentConfig;
}
});
[...requireTool.keys()].forEach((filePath: string) => {
const componentConfig = requireTool(filePath);
const fileName = validateFileName(filePath);
if (fileName) {
t[fileName] = componentConfig;
}
});
export default { ...result, ...t };

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -118,9 +118,9 @@ limitations under the License. -->
v-for="(i, j) in local.months" v-for="(i, j) in local.months"
@click=" @click="
is($event) && is($event) &&
((state.showMonths = m === 'M'), ((state.showMonths = state.m === 'M'),
(state.month = j), (state.month = j),
m === 'M' && ok('m')) state.m === 'M' && ok('m'))
" "
:class="[ :class="[
status( status(
@ -142,7 +142,7 @@ limitations under the License. -->
v-for="(i, j) in years" v-for="(i, j) in years"
@click=" @click="
is($event) && is($event) &&
((state.showYears = m === 'Y'), ((state.showYears = state.m === 'Y'),
(state.year = i), (state.year = i),
state.m === 'Y' && ok('y')) state.m === 'Y' && ok('y'))
" "
@ -278,6 +278,7 @@ limitations under the License. -->
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, watch, reactive } from "vue"; import { computed, onMounted, watch, reactive } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
/*global defineProps, defineEmits */ /*global defineProps, defineEmits */
const emit = defineEmits(["input", "setDates", "ok"]); const emit = defineEmits(["input", "setDates", "ok"]);
@ -286,7 +287,7 @@ const props = defineProps({
value: { type: Date }, value: { type: Date },
left: { type: Boolean, default: false }, left: { type: Boolean, default: false },
right: { type: Boolean, default: false }, right: { type: Boolean, default: false },
dates: { default: [] }, dates: { type: Array as PropType<number[] | string[]>, default: () => [] },
disabledDate: { type: Function, default: () => false }, disabledDate: { type: Function, default: () => false },
format: { format: {
type: String, type: String,
@ -353,10 +354,10 @@ const parse = (num: number): number => {
return Math.floor(num / 1000); return Math.floor(num / 1000);
}; };
const start = computed(() => { const start = computed(() => {
return parse(props.dates[0]); return parse(Number(props.dates[0]));
}); });
const end = computed(() => { const end = computed(() => {
return parse(props.dates[1]); return parse(Number(props.dates[1]));
}); });
const ys = computed(() => { const ys = computed(() => {
return Math.floor(state.year / 10) * 10; return Math.floor(state.year / 10) * 10;
@ -550,7 +551,7 @@ const ok = (info: any) => {
emit("setDates", _time); emit("setDates", _time);
} }
emit("input", _time); emit("input", _time);
ok(info === "h"); emit("ok", info === "h");
}; };
onMounted(() => { onMounted(() => {
const is = (c: string) => props.format.indexOf(c) !== -1; const is = (c: string) => props.format.indexOf(c) !== -1;

View File

@ -20,12 +20,15 @@ import { watch, ref, Ref, onMounted, onBeforeUnmount, unref } from "vue";
import type { PropType } from "vue"; import type { PropType } from "vue";
import { useECharts } from "@/hooks/useEcharts"; import { useECharts } from "@/hooks/useEcharts";
import { addResizeListener, removeResizeListener } from "@/utils/event"; import { addResizeListener, removeResizeListener } from "@/utils/event";
import { useTimeoutFn } from "@/hooks/useTimeout";
/*global Nullable, defineProps*/ /*global Nullable, defineProps, defineEmits*/
const emits = defineEmits(["select"]);
const chartRef = ref<Nullable<HTMLDivElement>>(null); const chartRef = ref<Nullable<HTMLDivElement>>(null);
const { setOptions, resize } = useECharts(chartRef as Ref<HTMLDivElement>); const { setOptions, resize, getInstance } = useECharts(
chartRef as Ref<HTMLDivElement>
);
const props = defineProps({ const props = defineProps({
clickEvent: { type: Function as PropType<(param: unknown) => void> },
height: { type: String, default: "100%" }, height: { type: String, default: "100%" },
width: { type: String, default: "100%" }, width: { type: String, default: "100%" },
option: { option: {
@ -34,9 +37,16 @@ const props = defineProps({
}, },
}); });
onMounted(() => { onMounted(async () => {
setOptions(props.option); await setOptions(props.option);
addResizeListener(unref(chartRef), resize); addResizeListener(unref(chartRef), resize);
useTimeoutFn(() => {
const instance = getInstance();
instance.on("click", (params: any) => {
emits("select", params);
});
}, 1000);
}); });
watch( watch(

View File

@ -43,18 +43,17 @@ interface Option {
} }
/*global defineProps, defineEmits*/ /*global defineProps, defineEmits*/
const emit = defineEmits(["change"]); const emit = defineEmits(["change"]);
const props = defineProps({ const props = defineProps({
options: { options: {
type: Array as PropType<Option[]>, type: Array as PropType<(Option & { disabled: boolean })[]>,
default: () => [], default: () => [],
}, },
value: { value: {
type: [Array, String] as PropType<string[] | string>, type: [Array, String] as PropType<string[] | string>,
default: () => [], default: () => [],
}, },
size: { type: String, default: "small" }, size: { type: null, default: "default" },
placeholder: { type: String, default: "Select a option" }, placeholder: { type: String, default: "Select a option" },
borderRadius: { type: Number, default: 3 }, borderRadius: { type: Number, default: 3 },
multiple: { type: Boolean, default: false }, multiple: { type: Boolean, default: false },
@ -77,7 +76,7 @@ watch(
} }
); );
</script> </script>
<style lang="scss" scope> <style lang="scss" scoped>
.icon { .icon {
width: 16px; width: 16px;
height: 16px; height: 16px;

View File

@ -145,11 +145,12 @@ limitations under the License. -->
import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue"; import { ref, computed, onMounted, onBeforeUnmount, watch } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import DateCalendar from "./DateCalendar.vue"; import DateCalendar from "./DateCalendar.vue";
import { useTimeoutFn } from "@/hooks/useTimeout";
/*global defineProps, defineEmits */ /*global defineProps, defineEmits */
const datepicker = ref(null); const datepicker = ref(null);
const { t } = useI18n(); const { t } = useI18n();
const show = ref<boolean>(false); const show = ref<boolean>(false);
const dates = ref<Date[]>([]); const dates = ref<Date | string[] | any>([]);
const props = defineProps({ const props = defineProps({
position: { type: String, default: "bottom" }, position: { type: String, default: "bottom" },
name: [String], name: [String],
@ -244,7 +245,7 @@ const range = computed(() => {
const text = computed(() => { const text = computed(() => {
const val = props.value; const val = props.value;
const txt = dates.value const txt = dates.value
.map((date) => tf(date)) .map((date: Date) => tf(date))
.join(` ${props.rangeSeparator} `); .join(` ${props.rangeSeparator} `);
if (Array.isArray(val)) { if (Array.isArray(val)) {
return val.length > 1 ? txt : ""; return val.length > 1 ? txt : "";
@ -270,9 +271,9 @@ const ok = (leaveOpened: boolean) => {
emit("input", get()); emit("input", get());
!leaveOpened && !leaveOpened &&
!props.showButtons && !props.showButtons &&
setTimeout(() => { useTimeoutFn(() => {
show.value = range.value; show.value = range.value;
}); }, 1);
}; };
const setDates = (d: Date) => { const setDates = (d: Date) => {
dates.value[1] = d; dates.value[1] = d;

33
src/assets/img/icons.js → src/graphql/fetch.ts Executable file → Normal file
View File

@ -14,21 +14,24 @@
* 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.
*/ */
const requireComponent = require.context("../../assets", false, /\.png$/); import axios, { AxiosResponse } from "axios";
import { cancelToken } from "@/utils/cancelToken";
const result = {}; async function query(param: {
function capitalizeFirstLetter(str) { queryStr: string;
return str.toUpperCase(); conditions: { [key: string]: unknown };
} }) {
function validateFileName(str) { const res: AxiosResponse = await axios.post(
return ( "/graphql",
/^\S+\.png$/.test(str) && { query: param.queryStr, variables: { ...param.conditions } },
str.replace(/^\S+\/(\w+)\.png$/, (rs, $1) => capitalizeFirstLetter($1)) { cancelToken: cancelToken() }
); );
if (res.data.errors) {
res.data.errors = res.data.errors
.map((e: { message: string }) => e.message)
.join(" ");
}
return res;
} }
requireComponent.keys().forEach((filePath) => {
const componentConfig = requireComponent(filePath); export default query;
const fileName = validateFileName(filePath);
result[fileName] = componentConfig;
});
export default result;

View File

@ -0,0 +1,77 @@
/**
* 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 ServicesTopology = {
variable: "$duration: Duration!, $serviceIds: [ID!]!",
query: `
topology: getServicesTopology(duration: $duration, serviceIds: $serviceIds) {
nodes {
id
name
type
isReal
}
calls {
id
source
detectPoints
target
}
}`,
};
export const EndpointTopology = {
variable: ["$endpointId: ID!", "$duration: Duration!"],
query: `
topology: getEndpointDependencies(endpointId: $endpointId, duration: $duration) {
nodes {
id
name
serviceId
serviceName
type
isReal
}
calls {
id
source
target
detectPoints
}
}`,
};
export const InstanceTopology = {
variable:
"$clientServiceId: ID!, $serverServiceId: ID!, $duration: Duration!",
query: `
topology: getServiceInstanceTopology(clientServiceId: $clientServiceId,
serverServiceId: $serverServiceId, duration: $duration) {
nodes {
id
name
type
isReal
serviceName
serviceId
}
calls {
id
source
detectPoints
target
}
}
`,
};

View File

@ -19,9 +19,15 @@ import { cancelToken } from "@/utils/cancelToken";
import * as app from "./query/app"; import * as app from "./query/app";
import * as selector from "./query/selector"; import * as selector from "./query/selector";
import * as dashboard from "./query/dashboard"; import * as dashboard from "./query/dashboard";
import * as topology from "./query/topology";
const query: { [key: string]: string } = { ...app, ...selector, ...dashboard }; const query: { [key: string]: string } = {
class Graph { ...app,
...selector,
...dashboard,
...topology,
};
class Graphql {
private queryData = ""; private queryData = "";
public query(queryData: string) { public query(queryData: string) {
this.queryData = queryData; this.queryData = queryData;
@ -51,4 +57,4 @@ class Graph {
} }
} }
export default new Graph(); export default new Graphql();

View File

@ -0,0 +1,25 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
InstanceTopology,
EndpointTopology,
ServicesTopology,
} from "../fragments/topology";
export const getInstanceTopology = `query queryData(${InstanceTopology.variable}) {${InstanceTopology.query}}`;
export const getEndpointTopology = `query queryData(${EndpointTopology.variable}) {${EndpointTopology.query}}`;
export const getServicesTopology = `query queryData(${ServicesTopology.variable}) {${ServicesTopology.query}}`;

View File

@ -19,6 +19,7 @@ import {
LineSeriesOption, LineSeriesOption,
HeatmapSeriesOption, HeatmapSeriesOption,
PieSeriesOption, PieSeriesOption,
SankeySeriesOption,
} from "echarts/charts"; } from "echarts/charts";
import { import {
TitleComponentOption, TitleComponentOption,
@ -46,6 +47,7 @@ export type ECOption = echarts.ComposeOption<
| LegendComponentOption | LegendComponentOption
| HeatmapSeriesOption | HeatmapSeriesOption
| PieSeriesOption | PieSeriesOption
| SankeySeriesOption
>; >;
export function useECharts( export function useECharts(

View File

@ -28,6 +28,7 @@ export function useQueryProcessor(config: any) {
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore(); const selectorStore = useSelectorStore();
if (!selectorStore.currentService && dashboardStore.entity !== "All") { if (!selectorStore.currentService && dashboardStore.entity !== "All") {
return; return;
} }
@ -40,6 +41,9 @@ export function useQueryProcessor(config: any) {
"ServiceInstanceRelation", "ServiceInstanceRelation",
"EndpointRelation", "EndpointRelation",
].includes(dashboardStore.entity); ].includes(dashboardStore.entity);
if (isRelation && !selectorStore.currentDestService) {
return;
}
const fragment = config.metrics.map((name: string, index: number) => { const fragment = config.metrics.map((name: string, index: number) => {
const metricType = config.metricTypes[index] || ""; const metricType = config.metricTypes[index] || "";
const labels = ["0", "1", "2", "3", "4"]; const labels = ["0", "1", "2", "3", "4"];
@ -257,3 +261,28 @@ export function usePodsSource(
}); });
return data; return data;
} }
export function useQueryTopologyMetrics(metrics: string[], ids: string[]) {
const appStore = useAppStoreWithOut();
const conditions: { [key: string]: unknown } = {
duration: appStore.durationTime,
ids,
};
const variables: string[] = [`$duration: Duration!`, `$ids: [ID!]!`];
const fragmentList = metrics.map((d: string, index: number) => {
conditions[`m${index}`] = d;
variables.push(`$m${index}: String!`);
return `${d}: getValues(metric: {
name: $m${index}
ids: $ids
}, duration: $duration) {
values {
id
value
}
}`;
});
const queryStr = `query queryData(${variables}) {${fragmentList.join(" ")}}`;
return { queryStr, conditions };
}

View File

@ -73,8 +73,23 @@ const msg = {
fontSize: "Font Size", fontSize: "Font Size",
showBackground: "Show Background", showBackground: "Show Background",
areaOpacity: "Area Opacity", areaOpacity: "Area Opacity",
editGraph: "Edit Graph Options", editGraph: "Edit Options",
dashboardName: "Select Dashboard Name", dashboardName: "Select Dashboard Name",
linkDashboard: "Dashboard name related with topology calls",
linkServerMetrics: "Server metrics related with topology calls",
linkClientMetrics: "Client metrics related with topology calls",
nodeDashboard: "Dashboard name related with topology nodes",
nodeMetrics: "Metrics related with topology nodes",
instanceDashboard: "Dashboard name related with service instances",
endpointDashboard: "Dashboard name related with endpoints",
callSettings: "Call settings",
nodeSettings: "Node Settings",
conditions: "Conditions",
legendSettings: "Legend Settings",
setLegend: "Set Legend",
backgroundColors: "Background Colors",
fontColors: "Font Colors",
iconTheme: "Icon Theme",
hourTip: "Select Hour", hourTip: "Select Hour",
minuteTip: "Select Minute", minuteTip: "Select Minute",
secondTip: "Select Second", secondTip: "Select Second",
@ -224,6 +239,7 @@ const msg = {
defaultOrder: "Default Order", defaultOrder: "Default Order",
chartType: "Chart Type", chartType: "Chart Type",
currentDepth: "Current Depth", currentDepth: "Current Depth",
defaultDepth: "Default Depth",
traceTagsTip: `Only tags defined in the core/default/searchableTracesTags are searchable. traceTagsTip: `Only tags defined in the core/default/searchableTracesTags are searchable.
Check more details on the Configuration Vocabulary page`, Check more details on the Configuration Vocabulary page`,
tagsLink: "Configuration Vocabulary page", tagsLink: "Configuration Vocabulary page",

View File

@ -23,6 +23,7 @@ const msg = {
infrastructure: "基础结构", infrastructure: "基础结构",
virtualMachine: "虚拟机", virtualMachine: "虚拟机",
kubernetes: "Kubernetes", kubernetes: "Kubernetes",
dashboardNew: "新建仪表板",
dashboardHome: "仪表盘首页", dashboardHome: "仪表盘首页",
dashboardList: "仪表盘列表", dashboardList: "仪表盘列表",
log: "日志", log: "日志",
@ -71,8 +72,23 @@ const msg = {
fontSize: "字体大小", fontSize: "字体大小",
showBackground: "显示背景", showBackground: "显示背景",
areaOpacity: "透明度", areaOpacity: "透明度",
editGraph: "编辑图表选项", editGraph: "选项编辑",
dashboardName: "选择仪表板名称", dashboardName: "选择仪表板名称",
linkDashboard: "拓扑线关联的仪表板名称",
linkServerMetrics: "拓扑线服务端关联的指标",
linkClientMetrics: "拓扑线客户端关联的指标",
nodeDashboard: "拓节点关联的仪表板名称",
nodeMetrics: "拓扑节点关联的指标",
instanceDashboard: "拓节点关联的实例的仪表板名称",
endpointDashboard: "拓节点端点的实例的仪表板名称",
callSettings: "拓扑线设置",
nodeSettings: "拓扑点设置",
conditions: "条件",
legendSettings: "图例设置",
setLegend: "设置图例",
backgroundColors: "背景颜色",
fontColors: "字体颜色",
iconTheme: "图标主题",
hourTip: "选择小时", hourTip: "选择小时",
minuteTip: "选择分钟", minuteTip: "选择分钟",
secondTip: "选择秒数", secondTip: "选择秒数",
@ -224,6 +240,7 @@ const msg = {
defaultOrder: "默认顺序", defaultOrder: "默认顺序",
chartType: "图表类型", chartType: "图表类型",
currentDepth: "当前深度", currentDepth: "当前深度",
defaultDepth: "默认深度",
traceTagsTip: traceTagsTip:
"只有core/default/searchableTracesTags中定义的标记才可搜索。查看配置词汇表页面上的更多详细信息。", "只有core/default/searchableTracesTags中定义的标记才可搜索。查看配置词汇表页面上的更多详细信息。",
tagsLink: "配置词汇页", tagsLink: "配置词汇页",

43
src/router/alarm.ts Normal file
View File

@ -0,0 +1,43 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { RouteRecordRaw } from "vue-router";
import Layout from "@/layout/Index.vue";
export const routesAlarm: Array<RouteRecordRaw> = [
{
path: "",
name: "Alarm",
meta: {
title: "alarm",
icon: "spam",
hasGroup: false,
exact: true,
},
component: Layout,
children: [
{
path: "/alarm",
name: "Alarm",
meta: {
title: "alarmList",
exact: false,
},
component: () => import("@/views/Log.vue"),
},
],
},
];

View File

@ -60,7 +60,17 @@ export const routesDashboard: Array<RouteRecordRaw> = [
{ {
path: "/dashboard/:layerId/:entity/:serviceId/:name", path: "/dashboard/:layerId/:entity/:serviceId/:name",
component: () => import("@/views/dashboard/Edit.vue"), component: () => import("@/views/dashboard/Edit.vue"),
name: "CreateService", name: "View",
meta: {
title: "dashboardEdit",
exact: false,
notShow: true,
},
},
{
path: "/dashboard/:layerId/:entity/:serviceId/:destServiceId/:name",
component: () => import("@/views/dashboard/Edit.vue"),
name: "ViewServiceRelation",
meta: { meta: {
title: "dashboardEdit", title: "dashboardEdit",
exact: false, exact: false,
@ -77,6 +87,16 @@ export const routesDashboard: Array<RouteRecordRaw> = [
notShow: true, notShow: true,
}, },
}, },
{
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:destServiceId/:destPodId/:name",
component: () => import("@/views/dashboard/Edit.vue"),
name: "ViewPodRelation",
meta: {
title: "dashboardEdit",
exact: true,
notShow: true,
},
},
], ],
}, },
]; ];

View File

@ -25,6 +25,8 @@ import { routesLog } from "./log";
import { routesEvent } from "./event"; import { routesEvent } from "./event";
import { routesAlert } from "./alert"; import { routesAlert } from "./alert";
import { routesSetting } from "./setting"; import { routesSetting } from "./setting";
import { routesAlarm } from "./alarm";
import { useTimeoutFn } from "@/hooks/useTimeout";
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
...routesGen, ...routesGen,
@ -36,6 +38,7 @@ const routes: Array<RouteRecordRaw> = [
...routesEvent, ...routesEvent,
...routesAlert, ...routesAlert,
...routesSetting, ...routesSetting,
...routesAlarm,
]; ];
const router = createRouter({ const router = createRouter({
@ -49,7 +52,7 @@ router.beforeEach((to, from, next) => {
// const token = window.localStorage.getItem("skywalking-authority"); // const token = window.localStorage.getItem("skywalking-authority");
if ((window as any).axiosCancel.length !== 0) { if ((window as any).axiosCancel.length !== 0) {
for (const func of (window as any).axiosCancel) { for (const func of (window as any).axiosCancel) {
setTimeout(func(), 0); useTimeoutFn(func(), 0);
} }
(window as any).axiosCancel = []; (window as any).axiosCancel = [];
} }

View File

@ -101,7 +101,8 @@ export const ConfigData2: any = {
}, },
children: [], children: [],
}; };
export const ConfigData3: any = { export const ConfigData3: any = [
{
x: 0, x: 0,
y: 0, y: 0,
w: 8, w: 8,
@ -121,4 +122,68 @@ export const ConfigData3: any = {
unit: "min", unit: "min",
}, },
children: [], children: [],
},
];
export const ConfigData4: any = {
x: 0,
y: 0,
w: 8,
h: 12,
i: "0",
metrics: ["service_relation_server_resp_time"],
metricTypes: ["readMetricsValues"],
type: "Widget",
widget: {
title: "service_relation_server_resp_time",
tips: "Tooltip",
},
graph: {
type: "Line",
},
standard: {
unit: "min",
},
children: [],
};
export const ConfigData5: any = {
x: 0,
y: 0,
w: 8,
h: 12,
i: "0",
metrics: ["endpoint_relation_cpm"],
metricTypes: ["readMetricsValues"],
type: "Widget",
widget: {
title: "endpoint_relation_cpm",
tips: "Tooltip",
},
graph: {
type: "Line",
},
standard: {
unit: "min",
},
children: [],
};
export const ConfigData6: any = {
x: 0,
y: 0,
w: 8,
h: 12,
i: "0",
metrics: ["service_instance_relation_server_cpm"],
metricTypes: ["readMetricsValues"],
type: "Widget",
widget: {
title: "service_instance_relation_server_cpm",
tips: "Tooltip",
},
graph: {
type: "Line",
},
standard: {
unit: "min",
},
children: [],
}; };

View File

@ -17,14 +17,22 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { store } from "@/store"; import { store } from "@/store";
import { LayoutConfig } from "@/types/dashboard"; import { LayoutConfig } from "@/types/dashboard";
import graph from "@/graph"; import graphql from "@/graphql";
import { ConfigData, ConfigData1, ConfigData2, ConfigData3 } from "../data"; import query from "@/graphql/fetch";
import {
ConfigData,
ConfigData1,
ConfigData2,
ConfigData3,
ConfigData4,
ConfigData5,
ConfigData6,
} from "../data";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { NewControl } from "../data"; import { NewControl } from "../data";
import { Duration } from "@/types/app"; import { Duration } from "@/types/app";
import axios, { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { cancelToken } from "@/utils/cancelToken";
interface DashboardState { interface DashboardState {
showConfig: boolean; showConfig: boolean;
layout: LayoutConfig[]; layout: LayoutConfig[];
@ -34,6 +42,7 @@ interface DashboardState {
activedGridItem: string; activedGridItem: string;
durationTime: Duration; durationTime: Duration;
selectorStore: any; selectorStore: any;
showTopology: boolean;
} }
export const dashboardStore = defineStore({ export const dashboardStore = defineStore({
@ -47,6 +56,7 @@ export const dashboardStore = defineStore({
activedGridItem: "", activedGridItem: "",
durationTime: useAppStoreWithOut().durationTime, durationTime: useAppStoreWithOut().durationTime,
selectorStore: useSelectorStore(), selectorStore: useSelectorStore(),
showTopology: false,
}), }),
actions: { actions: {
setLayout(data: LayoutConfig[]) { setLayout(data: LayoutConfig[]) {
@ -73,6 +83,17 @@ export const dashboardStore = defineStore({
}, },
]; ];
} }
if (type === "Topology") {
newWidget.w = 4;
newWidget.h = 6;
newWidget.graph = {
fontColor: "white",
backgroundColor: "green",
iconTheme: true,
content: "Topology",
fontSize: 18,
};
}
this.layout = this.layout.map((d: LayoutConfig) => { this.layout = this.layout.map((d: LayoutConfig) => {
d.y = d.y + newWidget.h; d.y = d.y + newWidget.h;
return d; return d;
@ -154,11 +175,23 @@ export const dashboardStore = defineStore({
this.layout = [ConfigData2]; this.layout = [ConfigData2];
} }
if (type == "All") { if (type == "All") {
this.layout = [ConfigData3]; this.layout = ConfigData3;
} }
if (type == "Service") { if (type == "Service") {
this.layout = [ConfigData]; this.layout = [ConfigData];
} }
if (type == "ServiceRelation") {
this.layout = [ConfigData4];
}
if (type == "ServiceInstanceRelation") {
this.layout = [ConfigData6];
}
if (type == "EndpointRelation") {
this.layout = [ConfigData5];
}
},
setTopology(show: boolean) {
this.showTopology = show;
}, },
setConfigs(param: { [key: string]: unknown }) { setConfigs(param: { [key: string]: unknown }) {
const actived = this.activedGridItem.split("-"); const actived = this.activedGridItem.split("-");
@ -181,14 +214,14 @@ export const dashboardStore = defineStore({
this.selectedGrid = this.layout[index]; this.selectedGrid = this.layout[index];
}, },
async fetchMetricType(item: string) { async fetchMetricType(item: string) {
const res: AxiosResponse = await graph const res: AxiosResponse = await graphql
.query("queryTypeOfMetrics") .query("queryTypeOfMetrics")
.params({ name: item }); .params({ name: item });
return res.data; return res.data;
}, },
async fetchMetricList(regex: string) { async fetchMetricList(regex: string) {
const res: AxiosResponse = await graph const res: AxiosResponse = await graphql
.query("queryMetrics") .query("queryMetrics")
.params({ regex }); .params({ regex });
@ -198,11 +231,7 @@ export const dashboardStore = defineStore({
queryStr: string; queryStr: string;
conditions: { [key: string]: unknown }; conditions: { [key: string]: unknown };
}) { }) {
const res: AxiosResponse = await axios.post( const res: AxiosResponse = await query(param);
"/graphql",
{ query: param.queryStr, variables: { ...param.conditions } },
{ cancelToken: cancelToken() }
);
return res.data; return res.data;
}, },
}, },

View File

@ -18,17 +18,19 @@ import { defineStore } from "pinia";
import { Duration } from "@/types/app"; import { Duration } from "@/types/app";
import { Service, Instance, Endpoint } from "@/types/selector"; import { Service, Instance, Endpoint } from "@/types/selector";
import { store } from "@/store"; import { store } from "@/store";
import graph from "@/graph"; import graphql from "@/graphql";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
interface SelectorState { interface SelectorState {
services: Service[]; services: Service[];
destServices: Service[];
pods: Array<Instance | Endpoint>; pods: Array<Instance | Endpoint>;
currentService: Nullable<Service>; currentService: Nullable<Service>;
currentPod: Nullable<Instance | Endpoint>; currentPod: Nullable<Instance | Endpoint>;
currentDestService: Nullable<Service>; currentDestService: Nullable<Service>;
currentDestPod: Nullable<Instance | Endpoint>; currentDestPod: Nullable<Instance | Endpoint>;
destPods: Array<Instance | Endpoint>;
durationTime: Duration; durationTime: Duration;
} }
@ -36,7 +38,9 @@ export const selectorStore = defineStore({
id: "selector", id: "selector",
state: (): SelectorState => ({ state: (): SelectorState => ({
services: [], services: [],
destServices: [],
pods: [], pods: [],
destPods: [],
currentService: null, currentService: null,
currentPod: null, currentPod: null,
currentDestService: null, currentDestService: null,
@ -44,103 +48,134 @@ export const selectorStore = defineStore({
durationTime: useAppStoreWithOut().durationTime, durationTime: useAppStoreWithOut().durationTime,
}), }),
actions: { actions: {
setCurrentService(service: Service) { setCurrentService(service: Nullable<Service>) {
this.currentService = service; this.currentService = service;
}, },
setCurrentDestService(service: Nullable<Service>) {
this.currentDestService = service;
},
setCurrentPod(pod: Nullable<Instance | Endpoint>) { setCurrentPod(pod: Nullable<Instance | Endpoint>) {
this.currentPod = pod; this.currentPod = pod;
}, },
setCurrentDestPod(pod: Nullable<Instance | Endpoint>) {
this.currentDestPod = pod;
},
async fetchLayers(): Promise<AxiosResponse> { async fetchLayers(): Promise<AxiosResponse> {
const res: AxiosResponse = await graph.query("queryLayers").params({}); const res: AxiosResponse = await graphql.query("queryLayers").params({});
return res.data || {}; return res.data || {};
}, },
async fetchServices(layer: string): Promise<AxiosResponse> { async fetchServices(layer: string): Promise<AxiosResponse> {
const res: AxiosResponse = await graph const res: AxiosResponse = await graphql
.query("queryServices") .query("queryServices")
.params({ layer }); .params({ layer });
if (!res.data.errors) { if (!res.data.errors) {
this.services = res.data.data.services || []; this.services = res.data.data.services || [];
this.destServices = res.data.data.services || [];
} }
return res.data; return res.data;
}, },
async getServiceInstances(param?: { async getServiceInstances(param?: {
serviceId: string; serviceId: string;
isRelation: boolean;
}): Promise<Nullable<AxiosResponse>> { }): Promise<Nullable<AxiosResponse>> {
const serviceId = param ? param.serviceId : this.currentService?.id; const serviceId = param ? param.serviceId : this.currentService?.id;
if (!serviceId) { if (!serviceId) {
return null; return null;
} }
const res: AxiosResponse = await graph.query("queryInstances").params({ const res: AxiosResponse = await graphql.query("queryInstances").params({
serviceId, serviceId,
duration: this.durationTime, duration: this.durationTime,
}); });
if (!res.data.errors) { if (!res.data.errors) {
if (param && param.isRelation) {
this.destPods = res.data.data.pods || [];
return res.data;
}
this.pods = res.data.data.pods || []; this.pods = res.data.data.pods || [];
} }
return res.data; return res.data;
}, },
async getEndpoints(params?: { async getEndpoints(params: {
keyword?: string; keyword?: string;
serviceId?: string; serviceId?: string;
isRelation?: boolean;
}): Promise<Nullable<AxiosResponse>> { }): Promise<Nullable<AxiosResponse>> {
if (!params) { if (!params) {
params = {}; params = {};
} }
if (!params.keyword) {
params.keyword = "";
}
const serviceId = params.serviceId || this.currentService?.id; const serviceId = params.serviceId || this.currentService?.id;
if (!serviceId) { if (!serviceId) {
return null; return null;
} }
const res: AxiosResponse = await graph.query("queryEndpoints").params({ const res: AxiosResponse = await graphql.query("queryEndpoints").params({
serviceId, serviceId,
duration: this.durationTime, duration: this.durationTime,
keyword: params.keyword, keyword: params.keyword || "",
}); });
if (!res.data.errors) { if (!res.data.errors) {
if (params.isRelation) {
this.destPods = res.data.data.pods || [];
return res.data;
}
this.pods = res.data.data.pods || []; this.pods = res.data.data.pods || [];
} }
return res.data; return res.data;
}, },
async getService(serviceId: string) { async getService(serviceId: string, isRelation: boolean) {
if (!serviceId) { if (!serviceId) {
return; return;
} }
const res: AxiosResponse = await graph.query("queryService").params({ const res: AxiosResponse = await graphql.query("queryService").params({
serviceId, serviceId,
}); });
if (!res.data.errors) { if (!res.data.errors) {
this.currentService = res.data.data.service || {}; if (isRelation) {
this.setCurrentDestService(res.data.data.service);
this.destServices = [res.data.data.service];
return res.data;
}
this.setCurrentService(res.data.data.service);
this.services = [res.data.data.service]; this.services = [res.data.data.service];
} }
return res.data; return res.data;
}, },
async getInstance(instanceId: string) { async getInstance(instanceId: string, isRelation?: boolean) {
if (!instanceId) { if (!instanceId) {
return; return;
} }
const res: AxiosResponse = await graph.query("queryInstance").params({ const res: AxiosResponse = await graphql.query("queryInstance").params({
instanceId, instanceId,
}); });
if (!res.data.errors) { if (!res.data.errors) {
if (isRelation) {
this.currentDestPod = res.data.data.instance || null;
this.destPods = [res.data.data.instance];
return;
}
this.currentPod = res.data.data.instance || null; this.currentPod = res.data.data.instance || null;
this.pods = [res.data.data.instance];
} }
return res.data; return res.data;
}, },
async getEndpoint(endpointId: string) { async getEndpoint(endpointId: string, isRelation?: string) {
if (!endpointId) { if (!endpointId) {
return; return;
} }
const res: AxiosResponse = await graph.query("queryEndpoint").params({ const res: AxiosResponse = await graphql.query("queryEndpoint").params({
endpointId, endpointId,
}); });
if (!res.data.errors) { if (!res.data.errors) {
if (isRelation) {
this.currentDestPod = res.data.data.endpoint || null;
this.destPods = [res.data.data.endpoint];
return;
}
this.currentPod = res.data.data.endpoint || null; this.currentPod = res.data.data.endpoint || null;
this.pods = [res.data.data.endpoint];
} }
return res.data; return res.data;

View File

@ -0,0 +1,460 @@
/**
* 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 { defineStore } from "pinia";
import { store } from "@/store";
import { Service } from "@/types/selector";
import { Node, Call } from "@/types/topology";
import graphql from "@/graphql";
import { useSelectorStore } from "@/store/modules/selectors";
import { useAppStoreWithOut } from "@/store/modules/app";
import { AxiosResponse } from "axios";
import query from "@/graphql/fetch";
interface MetricVal {
[key: string]: { values: { id: string; value: unknown }[] };
}
interface TopologyState {
node: Nullable<Node>;
call: Nullable<Call>;
calls: Call[];
nodes: Node[];
nodeMetrics: MetricVal;
linkServerMetrics: MetricVal;
linkClientMetrics: MetricVal;
defaultDepth: string;
}
export const topologyStore = defineStore({
id: "topology",
state: (): TopologyState => ({
calls: [],
nodes: [],
node: null,
call: null,
nodeMetrics: {},
linkServerMetrics: {},
linkClientMetrics: {},
defaultDepth: "2",
}),
actions: {
setNode(node: Node) {
this.node = node;
},
setLink(link: Call) {
this.call = link;
},
setInstanceTopology(data: { nodes: Node[]; calls: Call[] }) {
for (const call of data.calls) {
for (const node of data.nodes) {
if (call.source === node.id) {
call.sourceObj = node;
}
if (call.target === node.id) {
call.targetObj = node;
}
}
call.value = call.value || 1;
}
this.calls = data.calls;
this.nodes = data.nodes;
},
setTopology(data: { nodes: Node[]; calls: Call[] }) {
const obj = {} as any;
const services = useSelectorStore().services;
const nodes = data.nodes.reduce((prev: Node[], next: Node) => {
if (!obj[next.id]) {
obj[next.id] = true;
const s = services.filter((d: Service) => d.id === next.id)[0] || {};
next.layer = s.layers ? s.layers[0] : null;
prev.push(next);
}
return prev;
}, []);
const calls = data.calls.reduce((prev: Call[], next: Call) => {
if (!obj[next.id]) {
obj[next.id] = true;
next.value = next.value || 1;
for (const node of data.nodes) {
if (next.source === node.id) {
next.sourceObj = node;
}
if (next.target === node.id) {
next.targetObj = node;
}
}
next.value = next.value || 1;
prev.push(next);
}
return prev;
}, []);
this.calls = calls;
this.nodes = nodes;
},
setNodeMetrics(m: { id: string; value: unknown }[]) {
this.nodeMetrics = m;
},
setLinkServerMetrics(m: { id: string; value: unknown }[]) {
this.linkServerMetrics = m;
},
setLinkClientMetrics(m: { id: string; value: unknown }[]) {
this.linkClientMetrics = m;
},
setDefaultDepth(val: number) {
this.defaultDepth = val;
},
async getDepthServiceTopology(serviceIds: string[], depth: number) {
const res = await this.getServicesTopology(serviceIds);
if (depth > 1) {
const ids = res.nodes
.map((item: Node) => item.id)
.filter((d: string) => !serviceIds.includes(d));
if (!ids.length) {
this.setTopology(res);
return;
}
const json = await this.getServicesTopology(ids);
if (depth > 2) {
const pods = json.nodes
.map((item: Node) => item.id)
.filter((d: string) => ![...ids, ...serviceIds].includes(d));
if (!pods.length) {
const nodes = [...res.nodes, ...json.nodes];
const calls = [...res.calls, ...json.calls];
this.setTopology({ nodes, calls });
return;
}
const topo = await this.getServicesTopology(pods);
if (depth > 3) {
const services = topo.nodes
.map((item: Node) => item.id)
.filter(
(d: string) => ![...ids, ...pods, ...serviceIds].includes(d)
);
if (!services.length) {
const nodes = [...res.nodes, ...json.nodes, ...topo.nodes];
const calls = [...res.calls, ...json.calls, ...topo.calls];
this.setTopology({ nodes, calls });
return;
}
const data = await this.getServicesTopology(services);
if (depth > 4) {
const nodeIds = data.nodes
.map((item: Node) => item.id)
.filter(
(d: string) =>
![...services, ...ids, ...pods, ...serviceIds].includes(d)
);
if (!nodeIds.length) {
const nodes = [
...res.nodes,
...json.nodes,
...topo.nodes,
...data.nodes,
];
const calls = [
...res.calls,
...json.calls,
...topo.calls,
...data.calls,
];
this.setTopology({ nodes, calls });
return;
}
const toposObj = await this.getServicesTopology(nodeIds);
const nodes = [
...res.nodes,
...json.nodes,
...topo.nodes,
...data.nodes,
...toposObj.nodes,
];
const calls = [
...res.calls,
...json.calls,
...topo.calls,
...data.calls,
...toposObj.calls,
];
this.setTopology({ nodes, calls });
} else {
const nodes = [
...res.nodes,
...json.nodes,
...topo.nodes,
...data.nodes,
];
const calls = [
...res.calls,
...json.calls,
...topo.calls,
...data.calls,
];
this.setTopology({ nodes, calls });
}
} else {
const nodes = [...res.nodes, ...json.nodes, ...topo.nodes];
const calls = [...res.calls, ...json.calls, ...topo.calls];
this.setTopology({ nodes, calls });
}
} else {
this.setTopology({
nodes: [...res.nodes, ...json.nodes],
calls: [...res.calls, ...json.calls],
});
}
} else {
this.setTopology(res);
}
},
async getServicesTopology(serviceIds: string[]) {
const duration = useAppStoreWithOut().durationTime;
const res: AxiosResponse = await graphql
.query("getServicesTopology")
.params({
serviceIds,
duration,
});
if (res.data.errors) {
return res.data;
}
return res.data.data.topology;
},
async getInstanceTopology() {
const serverServiceId = useSelectorStore().currentService.id;
const clientServiceId = useSelectorStore().currentDestService.id;
const duration = useAppStoreWithOut().durationTime;
const res: AxiosResponse = await graphql
.query("getInstanceTopology")
.params({
clientServiceId,
serverServiceId,
duration,
});
if (!res.data.errors) {
this.setInstanceTopology(res.data.data.topology);
}
return res.data;
},
async updateEndpointTopology(endpointIds: string[], depth: number) {
const res = await this.getEndpointTopology(endpointIds);
if (depth > 1) {
const ids = res.nodes
.map((item: Node) => item.id)
.filter((d: string) => !endpointIds.includes(d));
if (!ids.length) {
this.setTopology(res);
return;
}
const json = await this.getEndpointTopology(ids);
if (depth > 2) {
const pods = json.nodes
.map((item: Node) => item.id)
.filter((d: string) => ![...ids, ...endpointIds].includes(d));
if (!pods.length) {
const nodes = [...res.nodes, ...json.nodes];
const calls = [...res.calls, ...json.calls];
this.setTopology({ nodes, calls });
return;
}
const topo = await this.getEndpointTopology(pods);
if (depth > 3) {
const endpoints = topo.nodes
.map((item: Node) => item.id)
.filter(
(d: string) => ![...ids, ...pods, ...endpointIds].includes(d)
);
if (!endpoints.length) {
const nodes = [...res.nodes, ...json.nodes, ...topo.nodes];
const calls = [...res.calls, ...json.calls, ...topo.calls];
this.setTopology({ nodes, calls });
return;
}
const data = await this.getEndpointTopology(endpoints);
if (depth > 4) {
const nodeIds = data.nodes
.map((item: Node) => item.id)
.filter(
(d: string) =>
![...endpoints, ...ids, ...pods, ...endpointIds].includes(d)
);
if (!nodeIds.length) {
const nodes = [
...res.nodes,
...json.nodes,
...topo.nodes,
...data.nodes,
];
const calls = [
...res.calls,
...json.calls,
...topo.calls,
...data.calls,
];
this.setTopology({ nodes, calls });
return;
}
const toposObj = await this.getEndpointTopology(nodeIds);
const nodes = [
...res.nodes,
...json.nodes,
...topo.nodes,
...data.nodes,
...toposObj.nodes,
];
const calls = [
...res.calls,
...json.calls,
...topo.calls,
...data.calls,
...toposObj.calls,
];
this.setTopology({ nodes, calls });
} else {
const nodes = [
...res.nodes,
...json.nodes,
...topo.nodes,
...data.nodes,
];
const calls = [
...res.calls,
...json.calls,
...topo.calls,
...data.calls,
];
this.setTopology({ nodes, calls });
}
} else {
const nodes = [...res.nodes, ...json.nodes, ...topo.nodes];
const calls = [...res.calls, ...json.calls, ...topo.calls];
this.setTopology({ nodes, calls });
}
} else {
this.setTopology({
nodes: [...res.nodes, ...json.nodes],
calls: [...res.calls, ...json.calls],
});
}
} else {
this.setTopology(res);
}
},
async getEndpointTopology(endpointIds: string[]) {
const duration = useAppStoreWithOut().durationTime;
const variables = ["$duration: Duration!"];
const fragment = endpointIds.map((id: string, index: number) => {
return `endpointTopology${index}: getEndpointDependencies(endpointId: "${id}", duration: $duration) {
nodes {
id
name
serviceId
serviceName
type
isReal
}
calls {
id
source
target
detectPoints
}
}`;
});
const queryStr = `query queryData(${variables}) {${fragment}}`;
const conditions = { duration };
const res: AxiosResponse = await query({ queryStr, conditions });
if (res.data.errors) {
return res.data;
}
const topo = res.data.data;
const calls = [] as any;
const nodes = [] as any;
for (const key of Object.keys(topo)) {
calls.push(...topo[key].calls);
nodes.push(...topo[key].nodes);
}
// this.setTopology({ calls, nodes });
return { calls, nodes };
},
async getNodeMetrics(param: {
queryStr: string;
conditions: { [key: string]: unknown };
}) {
const res: AxiosResponse = await query(param);
if (res.data.errors) {
return res.data;
}
this.setNodeMetrics(res.data.data);
return res.data;
},
async getLegendMetrics(param: {
queryStr: string;
conditions: { [key: string]: unknown };
}) {
const res: AxiosResponse = await query(param);
if (res.data.errors) {
return res.data;
}
const data = res.data.data;
const metrics = Object.keys(data);
this.nodes = this.nodes.map((d: Node | any) => {
for (const m of metrics) {
for (const val of data[m].values) {
if (d.id === val.id) {
d[m] = val.value;
}
}
}
return d;
});
return res.data;
},
async getCallServerMetrics(param: {
queryStr: string;
conditions: { [key: string]: unknown };
}) {
const res: AxiosResponse = await query(param);
if (res.data.errors) {
return res.data;
}
this.setLinkServerMetrics(res.data.data);
return res.data;
},
async getCallClientMetrics(param: {
queryStr: string;
conditions: { [key: string]: unknown };
}) {
const res: AxiosResponse = await query(param);
if (res.data.errors) {
return res.data;
}
this.setLinkClientMetrics(res.data.data);
return res.data;
},
},
});
export function useTopologyStore(): any {
return topologyStore(store);
}

View File

@ -95,6 +95,10 @@
background-color: #a7aebb; background-color: #a7aebb;
} }
.mb-5 {
margin-bottom: 5px;
}
.ml-5 { .ml-5 {
margin-left: 5px; margin-left: 5px;
} }
@ -129,11 +133,19 @@
@keyframes loading { @keyframes loading {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);
transform: rotate(0deg);
} }
to { to {
transform: rotate(1turn); transform: rotate(1turn);
transform: rotate(1turn); }
}
.dark-dialog {
background-color: #333840;
color: #ddd;
.el-loading-mask {
background-color: #333840;
color: #ddd;
} }
} }

View File

@ -55,7 +55,8 @@ export type GraphConfig =
| TableConfig | TableConfig
| EndpointListConfig | EndpointListConfig
| ServiceListConfig | ServiceListConfig
| InstanceListConfig; | InstanceListConfig
| TopologyConfig;
export interface BarConfig { export interface BarConfig {
type?: string; type?: string;
showBackground?: boolean; showBackground?: boolean;
@ -110,3 +111,13 @@ export interface EndpointListConfig {
dashboardName: string; dashboardName: string;
fontSize: number; fontSize: number;
} }
export interface TopologyConfig {
type?: string;
backgroundColor?: string;
fontColor?: string;
iconTheme?: boolean;
content?: string;
fontSize?: number;
depth?: string;
}

View File

@ -26,6 +26,8 @@ import type {
declare module "three"; declare module "three";
declare module "three-orbit-controls"; declare module "three-orbit-controls";
declare module "element-plus"; declare module "element-plus";
declare module "d3-tip";
declare module "d3";
declare global { declare global {
const __APP_INFO__: { const __APP_INFO__: {
pkg: { pkg: {

34
src/types/topology.d.ts vendored Normal file
View File

@ -0,0 +1,34 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Call {
source: string | any;
target: string | any;
id: string;
detectPoints: string[];
type?: string;
sourceObj?: any;
targetObj?: any;
value?: number;
}
export interface Node {
id: string;
name: string;
type: string;
isReal: boolean;
layer?: string;
serviceName?: string;
}

View File

@ -16,7 +16,13 @@
*/ */
import * as echarts from "echarts/core"; import * as echarts from "echarts/core";
import { BarChart, LineChart, PieChart, HeatmapChart } from "echarts/charts"; import {
BarChart,
LineChart,
PieChart,
HeatmapChart,
SankeyChart,
} from "echarts/charts";
import { import {
TitleComponent, TitleComponent,
@ -39,6 +45,7 @@ echarts.use([
LineChart, LineChart,
PieChart, PieChart,
HeatmapChart, HeatmapChart,
SankeyChart,
SVGRenderer, SVGRenderer,
DataZoomComponent, DataZoomComponent,
VisualMapComponent, VisualMapComponent,

View File

@ -14,7 +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.
*/ */
import graph from "@/graph"; import graphql from "@/graphql";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
const getLocalTime = (utc: string, time: Date): Date => { const getLocalTime = (utc: string, time: Date): Date => {
@ -38,7 +38,9 @@ const setTimezoneOffset = () => {
export const queryOAPTimeInfo = async (): Promise<void> => { export const queryOAPTimeInfo = async (): Promise<void> => {
let utc = window.localStorage.getItem("utc"); let utc = window.localStorage.getItem("utc");
if (!utc) { if (!utc) {
const res: AxiosResponse = await graph.query("queryOAPTimeInfo").params({}); const res: AxiosResponse = await graphql
.query("queryOAPTimeInfo")
.params({});
if ( if (
!res.data || !res.data ||
!res.data.data || !res.data.data ||

View File

@ -22,7 +22,7 @@ limitations under the License. -->
<span class="label">{{ t("language") }}</span> <span class="label">{{ t("language") }}</span>
<el-switch <el-switch
v-model="lang" v-model="lang"
:change="setLang" @change="setLang"
active-text="En" active-text="En"
inactive-text="Zh" inactive-text="Zh"
style="height: 25px" style="height: 25px"

View File

@ -23,7 +23,17 @@ limitations under the License. -->
:destroy-on-close="true" :destroy-on-close="true"
@closed="dashboardStore.setConfigPanel(false)" @closed="dashboardStore.setConfigPanel(false)"
> >
<config-edit /> <TopologyConfig v-if="dashboardStore.selectedGrid.type === 'Topology'" />
<Widget v-else />
</el-dialog>
<el-dialog
v-model="dashboardStore.showTopology"
:destroy-on-close="true"
fullscreen
@closed="dashboardStore.setTopology(false)"
custom-class="dark-dialog"
>
<Topology />
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
@ -32,7 +42,9 @@ import { useI18n } from "vue-i18n";
import GridLayout from "./panel/Layout.vue"; import GridLayout from "./panel/Layout.vue";
// import { LayoutConfig } from "@/types/dashboard"; // import { LayoutConfig } from "@/types/dashboard";
import Tool from "./panel/Tool.vue"; import Tool from "./panel/Tool.vue";
import ConfigEdit from "./configuration/ConfigEdit.vue"; import Widget from "./configuration/Widget.vue";
import TopologyConfig from "./configuration/Topology.vue";
import Topology from "./related/topology/Index.vue";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";

View File

@ -39,14 +39,14 @@ limitations under the License. -->
<el-table-column prop="date" label="Date" /> <el-table-column prop="date" label="Date" />
<el-table-column label="Operations"> <el-table-column label="Operations">
<template #default="scope"> <template #default="scope">
<el-button size="mini" @click="handleEdit(scope.$index, scope.row)"> <el-button size="small" @click="handleEdit(scope.$index, scope.row)">
{{ t("view") }} {{ t("view") }}
</el-button> </el-button>
<el-button size="mini" @click="handleEdit(scope.$index, scope.row)"> <el-button size="small" @click="handleEdit(scope.$index, scope.row)">
{{ t("edit") }} {{ t("edit") }}
</el-button> </el-button>
<el-button <el-button
size="mini" size="small"
type="danger" type="danger"
@click="handleDelete(scope.$index, scope.row)" @click="handleDelete(scope.$index, scope.row)"
> >

View File

@ -18,7 +18,7 @@ limitations under the License. -->
<div class="item"> <div class="item">
<div class="label">{{ t("name") }}</div> <div class="label">{{ t("name") }}</div>
<el-input <el-input
size="small" size="default"
v-model="states.name" v-model="states.name"
placeholder="Please input name" placeholder="Please input name"
/> />
@ -28,7 +28,6 @@ limitations under the License. -->
<Selector <Selector
v-model="states.selectedLayer" v-model="states.selectedLayer"
:options="states.layers" :options="states.layers"
size="small"
placeholder="Select a layer" placeholder="Select a layer"
@change="changeLayer" @change="changeLayer"
class="selectors" class="selectors"
@ -39,14 +38,13 @@ limitations under the License. -->
<Selector <Selector
v-model="states.entity" v-model="states.entity"
:options="EntityType" :options="EntityType"
size="small"
placeholder="Select a entity" placeholder="Select a entity"
@change="changeEntity" @change="changeEntity"
class="selectors" class="selectors"
/> />
</div> </div>
<div class="btn"> <div class="btn">
<el-button class="create" size="small" type="primary" @click="onCreate"> <el-button class="create" size="default" type="primary" @click="onCreate">
{{ t("create") }} {{ t("create") }}
</el-button> </el-button>
</div> </div>

View File

@ -20,6 +20,7 @@ limitations under the License. -->
placeholder="Please input dashboard name" placeholder="Please input dashboard name"
@change="changeDashboard" @change="changeDashboard"
class="selectors" class="selectors"
size="small"
/> />
</div> </div>
<div>{{ t("metrics") }}</div> <div>{{ t("metrics") }}</div>
@ -31,7 +32,7 @@ limitations under the License. -->
<Selector <Selector
:value="metric" :value="metric"
:options="states.metricList" :options="states.metricList"
size="mini" size="small"
placeholder="Select a metric" placeholder="Select a metric"
@change="changeMetrics(index, $event)" @change="changeMetrics(index, $event)"
class="selectors" class="selectors"
@ -39,7 +40,7 @@ limitations under the License. -->
<Selector <Selector
:value="states.metricTypes[index]" :value="states.metricTypes[index]"
:options="states.metricTypeList[index]" :options="states.metricTypeList[index]"
size="mini" size="small"
:disabled=" :disabled="
dashboardStore.selectedGrid.graph.type && !states.isTable && index !== 0 dashboardStore.selectedGrid.graph.type && !states.isTable && index !== 0
" "

View File

@ -18,7 +18,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="state.unit" v-model="state.unit"
size="mini" size="small"
placeholder="Please input Unit" placeholder="Please input Unit"
@change="changeStandardOpt({ unit: state.unit })" @change="changeStandardOpt({ unit: state.unit })"
/> />
@ -28,7 +28,7 @@ limitations under the License. -->
<Selector <Selector
:value="state.sortOrder" :value="state.sortOrder"
:options="SortOrder" :options="SortOrder"
size="mini" size="small"
placeholder="Select a sort order" placeholder="Select a sort order"
class="selector" class="selector"
@change="changeStandardOpt({ sortOrder: state.sortOrder })" @change="changeStandardOpt({ sortOrder: state.sortOrder })"
@ -39,7 +39,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="state.max" v-model="state.max"
size="mini" size="small"
placeholder="auto" placeholder="auto"
@change="changeStandardOpt({ max: state.max })" @change="changeStandardOpt({ max: state.max })"
/> />
@ -49,7 +49,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="state.min" v-model="state.min"
size="mini" size="small"
placeholder="auto" placeholder="auto"
@change="changeStandardOpt({ min: state.min })" @change="changeStandardOpt({ min: state.min })"
/> />
@ -59,7 +59,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="state.plus" v-model="state.plus"
size="mini" size="small"
placeholder="none" placeholder="none"
@change="changeStandardOpt({ plus: state.plus })" @change="changeStandardOpt({ plus: state.plus })"
/> />
@ -69,7 +69,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="state.minus" v-model="state.minus"
size="mini" size="small"
placeholder="none" placeholder="none"
@change="changeStandardOpt({ minus: state.minus })" @change="changeStandardOpt({ minus: state.minus })"
/> />
@ -79,7 +79,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="state.multiply" v-model="state.multiply"
size="mini" size="small"
placeholder="none" placeholder="none"
@change="changeStandardOpt({ multiply: state.multiply })" @change="changeStandardOpt({ multiply: state.multiply })"
/> />
@ -89,7 +89,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="state.divide" v-model="state.divide"
size="mini" size="small"
placeholder="none" placeholder="none"
@change="changeStandardOpt({ divide: state.divide })" @change="changeStandardOpt({ divide: state.divide })"
/> />
@ -99,7 +99,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="state.milliseconds" v-model="state.milliseconds"
size="mini" size="small"
placeholder="none" placeholder="none"
@change="changeStandardOpt({ milliseconds: state.milliseconds })" @change="changeStandardOpt({ milliseconds: state.milliseconds })"
/> />
@ -109,7 +109,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="state.seconds" v-model="state.seconds"
size="mini" size="small"
placeholder="none" placeholder="none"
@change="changeStandardOpt({ seconds: state.seconds })" @change="changeStandardOpt({ seconds: state.seconds })"
/> />

View File

@ -0,0 +1,52 @@
<!-- 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>
<WidgetOptions />
<TopologyOptions />
<div class="footer">
<el-button size="small">
{{ 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 WidgetOptions from "./WidgetOptions.vue";
import TopologyOptions from "./graph-styles/TopologyItem.vue";
import { useDashboardStore } from "@/store/modules/dashboard";
const { t } = useI18n();
const dashboardStore = useDashboardStore();
function applyConfig() {
dashboardStore.setConfigs(dashboardStore.selectedGrid);
dashboardStore.setConfigPanel(false);
}
</script>
<style lang="scss" scoped>
.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

@ -60,10 +60,10 @@ limitations under the License. -->
</el-collapse> </el-collapse>
</div> </div>
<div class="footer"> <div class="footer">
<el-button size="mini"> <el-button size="small">
{{ t("cancel") }} {{ t("cancel") }}
</el-button> </el-button>
<el-button size="mini" type="primary" @click="applyConfig"> <el-button size="small" type="primary" @click="applyConfig">
{{ t("apply") }} {{ t("apply") }}
</el-button> </el-button>
</div> </div>

View File

@ -18,7 +18,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="title" v-model="title"
size="mini" size="small"
placeholder="Please input title" placeholder="Please input title"
@change="updateWidgetConfig({ title })" @change="updateWidgetConfig({ title })"
/> />
@ -28,7 +28,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="tips" v-model="tips"
size="mini" size="small"
placeholder="Please input tips" placeholder="Please input tips"
@change="updateWidgetConfig({ tips })" @change="updateWidgetConfig({ tips })"
/> />

View File

@ -27,7 +27,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="tableHeaderCol1" v-model="tableHeaderCol1"
size="mini" size="small"
placeholder="none" placeholder="none"
@change="updateConfig({ tableHeaderCol1 })" @change="updateConfig({ tableHeaderCol1 })"
/> />
@ -37,7 +37,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="tableHeaderCol2" v-model="tableHeaderCol2"
size="mini" size="small"
placeholder="none" placeholder="none"
@change="updateConfig({ tableHeaderCol2 })" @change="updateConfig({ tableHeaderCol2 })"
/> />

View File

@ -18,7 +18,7 @@ limitations under the License. -->
<el-input <el-input
class="input" class="input"
v-model="topN" v-model="topN"
size="mini" size="small"
placeholder="none" placeholder="none"
type="number" type="number"
:min="1" :min="1"

View File

@ -0,0 +1,145 @@
<!-- 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("backgroundColors") }}</span>
<Selector
:value="backgroundColor"
:options="colors"
size="small"
placeholder="Select a color"
class="input"
@change="changeConfig({ backgroundColor: $event[0].value })"
/>
</div>
<div class="item">
<span class="label">{{ t("fontSize") }}</span>
<el-slider
class="slider"
v-model="fontSize"
show-input
input-size="small"
:min="12"
:max="30"
:step="1"
@change="changeConfig({ fontSize })"
/>
</div>
<div class="item">
<span class="label">{{ t("fontColors") }}</span>
<Selector
:value="fontColor"
:options="colors"
size="small"
placeholder="Select a color"
class="input"
@change="changeConfig({ fontColor: $event[0].value })"
/>
</div>
<div class="item">
<span class="label">{{ t("iconTheme") }}</span>
<el-switch
v-model="iconTheme"
active-text="Light"
inactive-text="Dark"
@change="changeConfig({ iconTheme })"
/>
</div>
<div class="item">
<span class="label">{{ t("content") }}</span>
<el-input
class="input"
v-model="content"
size="small"
@change="changeConfig({ content })"
/>
</div>
<div class="item">
<span class="label">{{ t("defaultDepth") }}</span>
<Selector
class="input"
size="small"
:value="depth"
:options="DepthList"
@change="changeDepth($event)"
/>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { ref } from "vue";
import { useDashboardStore } from "@/store/modules/dashboard";
import { DepthList } from "../../data";
import { Option } from "@/types/app";
import { useTopologyStore } from "@/store/modules/topology";
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const topologyStore = useTopologyStore();
const { selectedGrid } = dashboardStore;
const iconTheme = ref(selectedGrid.graph.iconTheme || true);
const backgroundColor = ref(selectedGrid.graph.backgroundColor || "green");
const fontColor = ref(selectedGrid.graph.fontColor || "white");
const content = ref<string>(selectedGrid.graph.content);
const fontSize = ref<number>(selectedGrid.graph.fontSize);
const depth = ref<string>(selectedGrid.graph.depth || "2");
const colors = [
{
label: "Green",
value: "green",
},
{ label: "Blue", value: "blue" },
{ label: "Red", value: "red" },
{ label: "Grey", value: "grey" },
{ label: "White", value: "white" },
{ label: "Black", value: "black" },
{ label: "Orange", value: "orange" },
];
topologyStore.setDefaultDepth(depth.value);
function changeConfig(param: { [key: string]: unknown }) {
const { selectedGrid } = dashboardStore;
const graph = {
...selectedGrid.graph,
...param,
};
dashboardStore.selectWidget({ ...selectedGrid, graph });
}
function changeDepth(opt: Option[]) {
const val = opt[0].value;
topologyStore.setDefaultDepth(val);
changeConfig({ depth: val });
}
</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;
}
</style>

View File

@ -0,0 +1,143 @@
<!-- 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="topology">
<div class="header flex-h">
<div>{{ data.widget?.title || "" }}</div>
<div>
<el-tooltip :content="data.widget?.tips">
<span>
<Icon
iconName="info_outline"
size="sm"
class="operation"
v-show="data.widget?.tips"
/>
</span>
</el-tooltip>
<el-popover placement="bottom" trigger="click" :width="100">
<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>
<div
class="body"
@click="ViewTopology"
:style="{ backgroundColor: Colors[data.graph.backgroundColor] }"
>
<Icon
:iconName="data.graph.iconTheme ? 'topology-light' : 'topology-dark'"
size="middle"
/>
<div
:style="{
color: Colors[data.graph.fontColor],
fontSize: data.graph.fontSize + 'px',
}"
>
{{ data.graph.content }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import { Colors } from "../data";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<any>,
default: () => ({ graph: {} }),
},
activeIndex: { type: String, default: "" },
});
const { t } = useI18n();
const dashboardStore = useDashboardStore();
function editConfig() {
dashboardStore.setConfigPanel(true);
dashboardStore.selectWidget(props.data);
}
function ViewTopology() {
dashboardStore.setTopology(true);
}
function removeTopo() {
dashboardStore.removeControls(props.data);
}
</script>
<style lang="scss" scoped>
.topology {
font-size: 12px;
height: 100%;
}
.header {
height: 30px;
padding: 5px;
width: 100%;
border-bottom: 1px solid #eee;
justify-content: space-between;
}
.operation {
cursor: pointer;
}
.tools {
padding: 5px 0;
color: #999;
cursor: pointer;
position: relative;
text-align: center;
&:hover {
color: #409eff;
background-color: #eee;
}
}
.body {
text-align: center;
width: 100%;
height: calc(100% - 30px);
cursor: pointer;
box-sizing: border-box;
color: #333;
display: -webkit-box;
-webkit-box-orient: horizontal;
-webkit-box-pack: center;
-webkit-box-align: center;
}
.no-data {
font-size: 14px;
color: #888;
width: 100%;
text-align: center;
padding-top: 20px;
}
</style>

View File

@ -18,20 +18,20 @@ limitations under the License. -->
<div>{{ data.widget?.title || "" }}</div> <div>{{ data.widget?.title || "" }}</div>
<div> <div>
<el-tooltip :content="data.widget?.tips"> <el-tooltip :content="data.widget?.tips">
<span>
<Icon <Icon
iconName="info_outline" iconName="info_outline"
size="sm" size="sm"
class="operation" class="operation"
v-show="data.widget?.tips" v-show="data.widget?.tips"
/> />
</span>
</el-tooltip> </el-tooltip>
<el-popover <el-popover placement="bottom" trigger="click" :width="100">
placement="bottom"
trigger="click"
:style="{ width: '100px' }"
>
<template #reference> <template #reference>
<span>
<Icon iconName="ellipsis_v" size="middle" class="operation" /> <Icon iconName="ellipsis_v" size="middle" class="operation" />
</span>
</template> </template>
<div class="tools" @click="editConfig"> <div class="tools" @click="editConfig">
<span>{{ t("edit") }}</span> <span>{{ t("edit") }}</span>
@ -99,6 +99,7 @@ export default defineComponent({
async function queryMetrics() { async function queryMetrics() {
const params = await useQueryProcessor(props.data); const params = await useQueryProcessor(props.data);
if (!params) { if (!params) {
state.source = {}; state.source = {};
return; return;
@ -137,15 +138,18 @@ export default defineComponent({
} }
); );
watch( watch(
() => selectorStore.currentService, () => [selectorStore.currentService, selectorStore.currentDestService],
() => { () => {
if (dashboardStore.entity === EntityType[0].value) { if (
dashboardStore.entity === EntityType[0].value ||
dashboardStore.entity === EntityType[4].value
) {
queryMetrics(); queryMetrics();
} }
} }
); );
watch( watch(
() => selectorStore.currentPod, () => [selectorStore.currentPod, selectorStore.currentDestPod],
() => { () => {
if (dashboardStore.entity === EntityType[0].value) { if (dashboardStore.entity === EntityType[0].value) {
return; return;

View File

@ -142,7 +142,7 @@ export enum MetricCatalog {
export const EntityType = [ export const EntityType = [
{ value: "Service", label: "Service", key: 1 }, { value: "Service", label: "Service", key: 1 },
{ value: "All", label: "All", key: 10 }, { value: "All", label: "All", key: 10 },
{ value: "Endpoint", label: "Service Endpoint", key: 3 }, { value: "Endpoint", label: "Endpoint", key: 3 },
{ value: "ServiceInstance", label: "Service Instance", key: 3 }, { value: "ServiceInstance", label: "Service Instance", key: 3 },
{ value: "ServiceRelation", label: "Service Relation", key: 2 }, { value: "ServiceRelation", label: "Service Relation", key: 2 },
{ {
@ -152,6 +152,7 @@ export const EntityType = [
}, },
{ value: "EndpointRelation", label: "Endpoint Relation", key: 4 }, { value: "EndpointRelation", label: "Endpoint Relation", key: 4 },
]; ];
export const hasTopology = ["All", "Service", "ServiceRelation", "Endpoint"];
export const TableEntity: any = { export const TableEntity: any = {
InstanceList: EntityType[3].value, InstanceList: EntityType[3].value,
EndpointList: EntityType[2].value, EndpointList: EntityType[2].value,
@ -165,8 +166,40 @@ export const ToolIcons = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" }, { name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" }, { name: "all_inbox", content: "Add Tab", id: "addTab" },
// { name: "insert_image", content: "Add Image", id: "addImage" }, // { name: "insert_image", content: "Add Image", id: "addImage" },
{ name: "save_alt", content: "Export", id: "export" }, // { name: "save_alt", content: "Export", id: "export" },
{ name: "folder_open", content: "Import", id: "import" }, // { name: "folder_open", content: "Import", id: "import" },
{ name: "settings", content: "Settings", id: "settings" }, // { name: "settings", content: "Settings", id: "settings" },
{ name: "save", content: "Apply", id: "applay" }, { name: "device_hub", content: "Add Topology", id: "topology" },
// { name: "save", content: "Apply", id: "apply" },
]; ];
export const ScopeType = [
{ value: "Service", label: "Service", key: 1 },
{ value: "Endpoint", label: "Endpoint", key: 3 },
{ value: "ServiceInstance", label: "Service Instance", key: 3 },
];
export const LegendConditions = [
{ label: "&&", value: "and" },
{ label: "||", value: "or" },
];
export const MetricConditions = [
{ label: ">", value: ">" },
{ label: "<", value: "<" },
];
export enum LegendOpt {
NAME = "name",
VALUE = "value",
CONDITION = "condition",
}
export const DepthList = ["1", "2", "3", "4", "5"].map((item: string) => ({
value: item,
label: item,
}));
export const Colors: any = {
green: "#67C23A",
blue: "#409EFF",
red: "#F56C6C",
grey: "#909399",
white: "#fff",
black: "#000",
orange: "#E6A23C",
};

View File

@ -42,10 +42,11 @@ import { useDashboardStore } from "@/store/modules/dashboard";
import { LayoutConfig } from "@/types/dashboard"; import { LayoutConfig } from "@/types/dashboard";
import Widget from "../controls/Widget.vue"; import Widget from "../controls/Widget.vue";
import Tab from "../controls/Tab.vue"; import Tab from "../controls/Tab.vue";
import Topology from "../controls/Topology.vue";
export default defineComponent({ export default defineComponent({
name: "Layout", name: "Layout",
components: { Widget, Tab }, components: { Widget, Tab, Topology },
setup() { setup() {
const dashboardStore = useDashboardStore(); const dashboardStore = useDashboardStore();
function layoutUpdatedEvent(newLayout: LayoutConfig[]) { function layoutUpdatedEvent(newLayout: LayoutConfig[]) {

View File

@ -20,7 +20,7 @@ limitations under the License. -->
<Selector <Selector
v-model="states.currentService" v-model="states.currentService"
:options="selectorStore.services" :options="selectorStore.services"
size="mini" size="small"
placeholder="Select a service" placeholder="Select a service"
@change="changeService" @change="changeService"
class="selectors" class="selectors"
@ -29,7 +29,7 @@ limitations under the License. -->
<div class="selectors-item" v-if="states.key === 3 || states.key === 4"> <div class="selectors-item" v-if="states.key === 3 || states.key === 4">
<span class="label"> <span class="label">
{{ {{
dashboardStore.entity === "Endpoint" ["EndpointRelation", "Endpoint"].includes(dashboardStore.entity)
? "$Endpoint" ? "$Endpoint"
: "$ServiceInstance" : "$ServiceInstance"
}} }}
@ -37,48 +37,58 @@ limitations under the License. -->
<Selector <Selector
v-model="states.currentPod" v-model="states.currentPod"
:options="selectorStore.pods" :options="selectorStore.pods"
size="mini" size="small"
placeholder="Select a data" placeholder="Select a data"
@change="changePods" @change="changePods"
class="selectorPod" class="selectorPod"
/> />
</div> </div>
<div class="selectors-item" v-if="states.key === 2"> <div class="selectors-item" v-if="states.key === 2 || states.key === 4">
<span class="label">$DestinationService</span> <span class="label">$DestinationService</span>
<Selector <Selector
v-model="selectorStore.currentDestService" v-model="states.currentDestService"
:options="selectorStore.services" :options="selectorStore.destServices"
size="mini" size="small"
placeholder="Select a service" placeholder="Select a service"
@change="changeService" @change="changeDestService"
class="selectors" class="selectors"
/> />
</div> </div>
<div class="selectors-item" v-if="states.key === 4"> <div class="selectors-item" v-if="states.key === 4">
<span class="label">$DestinationServiceInstance</span> <span class="label">
{{
dashboardStore.entity === "EndpointRelation"
? "$DestinationEndpoint"
: "$DestinationServiceInstance"
}}</span
>
<Selector <Selector
v-model="states.currentPod" v-model="states.currentDestPod"
:options="selectorStore.pods" :options="selectorStore.destPods"
size="mini" size="small"
placeholder="Select a data" placeholder="Select a data"
@change="changePods" @change="changePods"
class="selectors" class="selectorPod"
:borderRadius="4"
/> />
</div> </div>
</div> </div>
<div class="tool-icons"> <div class="tool-icons">
<el-tooltip <span
@click="clickIcons(t)"
v-for="(t, index) in ToolIcons" v-for="(t, index) in ToolIcons"
:key="index" :key="index"
class="item" :title="t.content"
:content="t.content"
placement="top"
> >
<span class="icon-btn" @click="clickIcons(t)"> <Icon
<Icon size="sm" :iconName="t.name" /> class="icon-btn"
size="sm"
:iconName="t.name"
v-if="
t.id !== 'topology' ||
(t.id === 'topology' && hasTopology.includes(dashboardStore.entity))
"
/>
</span> </span>
</el-tooltip>
</div> </div>
</div> </div>
</template> </template>
@ -88,7 +98,7 @@ import { reactive, watch } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { useDashboardStore } from "@/store/modules/dashboard"; import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import { EntityType, ToolIcons } from "../data"; import { EntityType, ToolIcons, hasTopology } from "../data";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { Option } from "@/types/app"; import { Option } from "@/types/app";
@ -105,12 +115,16 @@ const states = reactive<{
key: number; key: number;
currentService: string; currentService: string;
currentPod: string; currentPod: string;
currentDestService: string;
currentDestPod: string;
}>({ }>({
destService: "", destService: "",
destPod: "", destPod: "",
key: (type && type.key) || 0, key: (type && type.key) || 0,
currentService: "", currentService: "",
currentPod: "", currentPod: "",
currentDestService: "",
currentDestPod: "",
}); });
dashboardStore.setLayer(String(params.layerId)); dashboardStore.setLayer(String(params.layerId));
@ -127,56 +141,136 @@ function initSelector() {
} }
async function setSelector() { async function setSelector() {
if (params.podId) { if (
await selectorStore.getService(String(params.serviceId)); [
states.currentService = selectorStore.currentService.value; EntityType[2].value,
await fetchPods(String(params.entity), false); EntityType[3].value,
const currentPod = selectorStore.pods.filter( EntityType[5].value,
(d: { id: string }) => d.id === String(params.podId) EntityType[6].value,
)[0]; ].includes(String(params.entity))
selectorStore.setCurrentPod(currentPod); ) {
states.currentPod = currentPod.label; setSourceSelector();
if (
[EntityType[2].value, EntityType[3].value].includes(String(params.entity))
) {
return; return;
} }
// entity=Service with serviceId setDestSelector();
return;
}
// entity=Service/ServiceRelation
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;
} }
const currentService = selectorStore.services.filter( let currentService, currentDestService;
(d: { id: string }) => d.id === String(params.serviceId) for (const d of selectorStore.services) {
)[0]; if (d.id === String(params.serviceId)) {
currentService = d;
}
if (d.id === String(params.destServiceId)) {
currentDestService = d;
}
}
selectorStore.setCurrentService(currentService); selectorStore.setCurrentService(currentService);
selectorStore.setCurrentDestService(currentDestService);
states.currentService = selectorStore.currentService.value; states.currentService = selectorStore.currentService.value;
states.currentDestService = selectorStore.currentDestService.value;
}
async function setSourceSelector() {
await selectorStore.getService(String(params.serviceId));
states.currentService = selectorStore.currentService.value;
const e = String(params.entity).split("Relation")[0];
await fetchPods(e, selectorStore.currentService.id, false);
if (!(selectorStore.pods.length && selectorStore.pods[0])) {
selectorStore.setCurrentPod(null);
states.currentPod = "";
return;
}
const pod = params.podId || selectorStore.pods[0].id;
const currentPod = selectorStore.pods.filter(
(d: { id: string }) => d.id === pod
)[0];
if (currentPod) {
selectorStore.setCurrentPod(currentPod);
states.currentPod = currentPod.label;
}
}
async function setDestSelector() {
await selectorStore.getService(String(params.destServiceId), true);
states.currentDestService = selectorStore.currentDestService.value;
await fetchPods(
String(params.entity),
selectorStore.currentDestService.id,
false
);
if (!(selectorStore.destPods.length && selectorStore.destPods[0])) {
selectorStore.setCurrentDestPod(null);
states.currentDestPod = "";
return;
}
const destPod = params.destPodId || selectorStore.destPods[0].id;
const currentDestPod = selectorStore.destPods.filter(
(d: { id: string }) => d.id === destPod
)[0];
if (currentDestPod) {
selectorStore.setCurrentDestPod(currentDestPod);
states.currentDestPod = currentDestPod.label;
}
} }
async function getServices() { async function getServices() {
if (!dashboardStore.layerId) { if (!dashboardStore.layerId) {
return; return;
} }
if (dashboardStore.entity === EntityType[1].value) {
return;
}
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;
} }
if (dashboardStore.entity === EntityType[1].value) {
return;
}
selectorStore.setCurrentService( selectorStore.setCurrentService(
selectorStore.services.length ? selectorStore.services[0] : null selectorStore.services.length ? selectorStore.services[0] : null
); );
selectorStore.setCurrentDestService(
selectorStore.services.length ? selectorStore.services[1] : null
);
states.currentService = selectorStore.currentService.value; states.currentService = selectorStore.currentService.value;
fetchPods(dashboardStore.entity, true); states.currentDestService = selectorStore.currentDestService.value;
const e = dashboardStore.entity.split("Relation")[0];
if (
[EntityType[2].value, EntityType[3].value].includes(dashboardStore.entity)
) {
fetchPods(e, selectorStore.currentService.id, true);
}
if (
[EntityType[5].value, EntityType[6].value].includes(dashboardStore.entity)
) {
fetchPods(dashboardStore.entity, selectorStore.currentDestService.id, true);
}
} }
async function changeService(service: Service[]) { async function changeService(service: Service[]) {
if (service[0]) { if (service[0]) {
states.currentService = service[0].value; states.currentService = service[0].value;
selectorStore.setCurrentService(service[0]); selectorStore.setCurrentService(service[0]);
fetchPods(dashboardStore.entity, true); fetchPods(dashboardStore.entity, selectorStore.currentService.id, true);
} else { } else {
selectorStore.setCurrentService(""); selectorStore.setCurrentService(null);
}
}
function changeDestService(service: Service[]) {
if (service[0]) {
states.currentDestService = service[0].value;
selectorStore.setCurrentDestService(service[0]);
} else {
selectorStore.setCurrentDestService(null);
} }
} }
@ -199,6 +293,9 @@ function clickIcons(t: { id: string; content: string; name: string }) {
case "addImage": case "addImage":
dashboardStore.addControl("Image"); dashboardStore.addControl("Image");
break; break;
case "topology":
dashboardStore.addControl("Topology");
break;
case "settings": case "settings":
dashboardStore.setConfigPanel(true); dashboardStore.setConfigPanel(true);
break; break;
@ -207,11 +304,11 @@ function clickIcons(t: { id: string; content: string; name: string }) {
} }
} }
async function fetchPods(type: string, setPod: boolean) { async function fetchPods(type: string, serviceId: string, setPod: boolean) {
let resp; let resp;
switch (type) { switch (type) {
case "Endpoint": case EntityType[2].value:
resp = await selectorStore.getEndpoints(); resp = await selectorStore.getEndpoints({ serviceId });
if (setPod) { if (setPod) {
selectorStore.setCurrentPod( selectorStore.setCurrentPod(
selectorStore.pods.length ? selectorStore.pods[0] : null selectorStore.pods.length ? selectorStore.pods[0] : null
@ -219,8 +316,8 @@ async function fetchPods(type: string, setPod: boolean) {
states.currentPod = selectorStore.currentPod.label; states.currentPod = selectorStore.currentPod.label;
} }
break; break;
case "ServiceInstance": case EntityType[3].value:
resp = await selectorStore.getServiceInstances(); resp = await selectorStore.getServiceInstances({ serviceId });
if (setPod) { if (setPod) {
selectorStore.setCurrentPod( selectorStore.setCurrentPod(
selectorStore.pods.length ? selectorStore.pods[0] : null selectorStore.pods.length ? selectorStore.pods[0] : null
@ -228,6 +325,27 @@ async function fetchPods(type: string, setPod: boolean) {
states.currentPod = selectorStore.currentPod.label; states.currentPod = selectorStore.currentPod.label;
} }
break; break;
case EntityType[6].value:
resp = await selectorStore.getEndpoints({ serviceId, isRelation: true });
if (setPod) {
selectorStore.setCurrentDestPod(
selectorStore.destPods.length ? selectorStore.destPods[0] : null
);
states.currentDestPod = selectorStore.currentDestPod.label;
}
break;
case EntityType[5].value:
resp = await selectorStore.getServiceInstances({
serviceId,
isRelation: true,
});
if (setPod) {
selectorStore.setCurrentDestPod(
selectorStore.destPods.length ? selectorStore.destPods[0] : null
);
states.currentDestPod = selectorStore.currentDestPod.label;
}
break;
default: default:
resp = {}; resp = {};
} }
@ -258,9 +376,13 @@ watch(
padding: 4px 2px; padding: 4px 2px;
} }
.tool-icons {
margin-top: 2px;
}
.icon-btn { .icon-btn {
display: inline-block; display: inline-block;
padding: 0 5px; padding: 3px;
text-align: center; text-align: center;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 3px; border-radius: 3px;
@ -268,9 +390,6 @@ watch(
cursor: pointer; cursor: pointer;
background-color: #eee; background-color: #eee;
color: #666; color: #666;
}
.item {
font-size: 12px; font-size: 12px;
} }

View File

@ -0,0 +1,30 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<PodTopology v-if="isSankey" />
<Graph v-else />
</template>
<script lang="ts" setup>
import { ref } from "vue";
import Graph from "./components/Graph.vue";
import PodTopology from "./components/PodTopology.vue";
import { EntityType } from "../../data";
import { useDashboardStore } from "@/store/modules/dashboard";
const dashboardStore = useDashboardStore();
const isSankey = ref<boolean>(
[EntityType[2].value, EntityType[4].value].includes(dashboardStore.entity)
);
</script>

View File

@ -0,0 +1,586 @@
<!-- 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
ref="chart"
class="micro-topo-chart"
v-loading="loading"
:style="`height: ${height}px`"
>
<div class="setting" v-show="showSetting">
<Settings @update="updateSettings" @updateNodes="freshNodes" />
</div>
<div class="tool">
<span class="label">{{ t("currentDepth") }}</span>
<Selector
class="inputs"
:value="depth"
:options="DepthList"
placeholder="Select a option"
@change="changeDepth"
/>
<span class="switch-icon ml-5" title="Settings" @click="setConfig">
<Icon size="middle" iconName="settings" />
</span>
<span
class="switch-icon ml-5"
title="Back to overview topology"
@click="backToTopology"
>
<Icon size="middle" iconName="keyboard_backspace" />
</span>
</div>
<div
class="operations-list"
v-if="topologyStore.node"
:style="{
top: operationsPos.y + 'px',
left: operationsPos.x + 'px',
}"
>
<span
v-for="(item, index) of items"
:key="index"
@click="item.func(item.dashboard)"
>
{{ item.title }}
</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, reactive } from "vue";
import { useI18n } from "vue-i18n";
import * as d3 from "d3";
import d3tip from "d3-tip";
import zoom from "../utils/zoom";
import { simulationInit, simulationSkip } from "../utils/simulation";
import nodeElement from "../utils/nodeElement";
import { linkElement, anchorElement, arrowMarker } from "../utils/linkElement";
import topoLegend from "../utils/legend";
import { Node, Call } from "@/types/topology";
import { useSelectorStore } from "@/store/modules/selectors";
import { useTopologyStore } from "@/store/modules/topology";
import { useDashboardStore } from "@/store/modules/dashboard";
import { EntityType, DepthList } from "../../../data";
import router from "@/router";
import { ElMessage } from "element-plus";
import Settings from "./Settings.vue";
import { Option } from "@/types/app";
import { Service } from "@/types/selector";
/*global Nullable */
const { t } = useI18n();
const selectorStore = useSelectorStore();
const topologyStore = useTopologyStore();
const dashboardStore = useDashboardStore();
const height = ref<number>(document.body.clientHeight - 90);
const width = ref<number>(document.body.clientWidth - 40);
const loading = ref<boolean>(false);
const simulation = ref<any>(null);
const svg = ref<Nullable<any>>(null);
const chart = ref<Nullable<HTMLDivElement>>(null);
const tip = ref<any>(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 legend = ref<any>(null);
const showSetting = ref<boolean>(false);
const settings = ref<any>({});
const operationsPos = reactive<{ x: number; y: number }>({ x: NaN, y: NaN });
const items = ref<
{ id: string; title: string; func: any; dashboard?: string }[]
>([
{ id: "inspect", title: "Inspect", func: handleInspect },
{ id: "alarm", title: "Alarm", func: handleGoAlarm },
]);
const depth = ref<string>(topologyStore.defaultDepth);
onMounted(async () => {
loading.value = true;
const resp = await getTopology();
loading.value = false;
if (resp && resp.errors) {
ElMessage.error(resp.errors);
}
window.addEventListener("resize", resize);
svg.value = d3
.select(chart.value)
.append("svg")
.attr("class", "topo-svg")
.attr("height", height.value)
.attr("width", width.value);
await init();
update();
});
async function init() {
tip.value = (d3tip as any)().attr("class", "d3-tip").offset([-8, 0]);
graph.value = svg.value.append("g").attr("class", "topo-svg-graph");
graph.value.call(tip.value);
simulation.value = simulationInit(
d3,
topologyStore.nodes,
topologyStore.calls,
ticked
);
node.value = graph.value.append("g").selectAll(".topo-node");
link.value = graph.value.append("g").selectAll(".topo-line");
anchor.value = graph.value.append("g").selectAll(".topo-line-anchor");
arrow.value = graph.value.append("g").selectAll(".topo-line-arrow");
svg.value.call(zoom(d3, graph.value));
// legend
legend.value = graph.value.append("g").attr("class", "topo-legend");
topoLegend(legend.value, height.value, width.value, settings.value.legend);
svg.value.on("click", (event: any) => {
event.stopPropagation();
event.preventDefault();
topologyStore.setNode(null);
topologyStore.setLink(null);
});
}
function ticked() {
link.value.attr(
"d",
(d: Call | any) =>
`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
} ${d.target.x} ${d.target.y}`
);
anchor.value.attr(
"transform",
(d: Call | any) =>
`translate(${(d.source.x + d.target.x) / 2}, ${
(d.target.y + d.source.y) / 2 - d.loopFactor * 45
})`
);
node.value.attr(
"transform",
(d: Node | any) => `translate(${d.x - 22},${d.y - 22})`
);
}
function dragstart(d: any) {
node.value._groups[0].forEach((g: any) => {
g.__data__.fx = g.__data__.x;
g.__data__.fy = g.__data__.y;
});
if (!d.active) {
simulation.value.alphaTarget(0.1).restart();
}
d.subject.fx = d.subject.x;
d.subject.fy = d.subject.y;
d.sourceEvent.stopPropagation();
}
function dragged(d: any) {
d.subject.fx = d.x;
d.subject.fy = d.y;
}
function dragended(d: any) {
if (!d.active) {
simulation.value.alphaTarget(0);
}
}
function handleNodeClick(d: Node & { x: number; y: number }) {
topologyStore.setNode(d);
topologyStore.setLink(null);
operationsPos.x = d.x;
operationsPos.y = d.y + 30;
if (d.layer === String(dashboardStore.layerId)) {
return;
}
items.value = [
{ id: "inspect", title: "Inspect", func: handleInspect },
{ id: "alarm", title: "Alarm", func: handleGoAlarm },
];
}
function handleLinkClick(event: any, d: Call) {
if (
d.source.layer !== dashboardStore.layerId ||
d.target.layer !== dashboardStore.layerId
) {
return;
}
event.stopPropagation();
topologyStore.setNode(null);
topologyStore.setLink(d);
if (!settings.value.linkDashboard) {
return;
}
const e =
dashboardStore.entity === EntityType[1].value
? EntityType[0].value
: dashboardStore.entity;
const path = `/dashboard/${dashboardStore.layerId}/${e}Relation/${d.source.id}/${d.target.id}/${settings.value.linkDashboard}`;
const routeUrl = router.resolve({ path });
window.open(routeUrl.href, "_blank");
}
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 html = nodeMetrics.map((m) => {
const metric =
topologyStore.nodeMetrics[m].values.filter(
(val: { id: string; value: unknown }) => val.id === data.id
)[0] || {};
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</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 linkServerMetrics: string[] =
settings.value.linkServerMetrics || [];
const htmlServer = linkServerMetrics.map((m) => {
const metric = topologyStore.linkServerMetrics[m].values.filter(
(val: { id: string; value: unknown }) => val.id === data.id
)[0];
if (metric) {
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</div>`;
}
});
const htmlClient = linkClientMetrics.map((m) => {
const metric = topologyStore.linkClientMetrics[m].values.filter(
(val: { id: string; value: unknown }) => val.id === data.id
)[0];
if (metric) {
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
return ` <div class="mb-5"><span class="grey">${m}: </span>${val}</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() {
svg.value.selectAll(".topo-svg-graph").remove();
const id = topologyStore.node.id;
topologyStore.setNode(null);
topologyStore.setLink(null);
loading.value = true;
const resp = await topologyStore.getServicesTopology([id]);
loading.value = false;
if (resp.errors) {
ElMessage.error(resp.errors);
}
await init();
update();
}
function handleGoEndpoint(name: string) {
const path = `/dashboard/${dashboardStore.layerId}/Endpoint/${topologyStore.node.id}/${name}`;
const routeUrl = router.resolve({ path });
window.open(routeUrl.href, "_blank");
}
function handleGoInstance(name: string) {
const path = `/dashboard/${dashboardStore.layerId}/ServiceInstance/${topologyStore.node.id}/${name}`;
const routeUrl = router.resolve({ path });
window.open(routeUrl.href, "_blank");
}
function handleGoDashboard(name: string) {
const path = `/dashboard/${dashboardStore.layerId}/Service/${topologyStore.node.id}/${name}`;
const routeUrl = router.resolve({ path });
window.open(routeUrl.href, "_blank");
}
function handleGoAlarm() {
const path = `/alarm`;
const routeUrl = router.resolve({ path });
window.open(routeUrl.href, "_blank");
}
async function backToTopology() {
svg.value.selectAll(".topo-svg-graph").remove();
loading.value = true;
const resp = await getTopology();
loading.value = false;
if (resp.errors) {
ElMessage.error(resp.errors);
}
await init();
update();
topologyStore.setNode(null);
topologyStore.setLink(null);
}
async function getTopology() {
const ids = selectorStore.services.map((d: Service) => d.id);
const serviceIds =
dashboardStore.entity === EntityType[0].value
? [selectorStore.currentService.id]
: ids;
const resp = await topologyStore.getDepthServiceTopology(
serviceIds,
Number(depth.value)
);
return resp;
}
function setConfig() {
showSetting.value = !showSetting.value;
}
function resize() {
height.value = document.body.clientHeight - 90;
width.value = document.body.clientWidth - 40;
svg.value.attr("height", height.value).attr("width", width.value);
}
function updateSettings(config: any) {
items.value = [
{ id: "inspect", title: "Inspect", func: handleInspect },
{ id: "alarm", title: "Alarm", func: handleGoAlarm },
];
settings.value = config;
for (const item of config.nodeDashboard) {
if (item.scope === EntityType[0].value) {
items.value.push({
id: "dashboard",
title: "Dashboard",
func: handleGoDashboard,
...item,
});
}
if (item.scope === EntityType[2].value) {
items.value.push({
id: "endpoint",
title: "Endpoint",
func: handleGoEndpoint,
...item,
});
}
if (item.scope === EntityType[3].value) {
items.value.push({
id: "instance",
title: "Service Instance",
func: handleGoInstance,
...item,
});
}
}
}
async function freshNodes() {
svg.value.selectAll(".topo-svg-graph").remove();
await init();
update();
}
async function changeDepth(opt: Option[]) {
depth.value = opt[0].value;
await getTopology();
freshNodes();
}
onBeforeUnmount(() => {
window.removeEventListener("resize", resize);
});
</script>
<style lang="scss">
.micro-topo-chart {
position: relative;
.setting {
position: absolute;
top: 70px;
right: 0;
width: 400px;
height: 700px;
background-color: #2b3037;
overflow: auto;
padding: 0 15px;
border-radius: 3px;
color: #ccc;
transition: all 0.5ms linear;
}
.label {
color: #ccc;
display: inline-block;
margin-right: 5px;
}
.operations-list {
position: absolute;
padding: 10px;
color: #333;
cursor: pointer;
background-color: #fff;
border-radius: 3px;
span {
display: block;
height: 30px;
width: 140px;
line-height: 30px;
text-align: center;
}
span:hover {
color: #409eff;
background-color: #eee;
}
}
.tool {
position: absolute;
top: 22px;
right: 0;
}
.switch-icon {
cursor: pointer;
transition: all 0.5ms linear;
background-color: #252a2f99;
color: #ddd;
display: inline-block;
padding: 5px 8px 8px;
border-radius: 3px;
}
.topo-svg {
display: block;
width: 100%;
}
.topo-line {
stroke-linecap: round;
stroke-width: 3px;
stroke-dasharray: 13 7;
fill: none;
animation: topo-dash 0.5s linear infinite;
}
.topo-line-anchor {
cursor: pointer;
}
.topo-text {
font-family: "Lato", "Source Han Sans CN", "Microsoft YaHei", sans-serif;
fill: #ddd;
font-size: 11px;
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 {
from {
stroke-dashoffset: 20;
}
to {
stroke-dashoffset: 0;
}
}
</style>

View File

@ -0,0 +1,274 @@
<!-- 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="tool">
<span v-show="dashboardStore.entity === EntityType[2].value">
<span class="label">{{ t("currentDepth") }}</span>
<Selector
class="inputs"
:value="depth"
:options="DepthList"
placeholder="Select a option"
@change="changeDepth"
/>
</span>
<span class="switch-icon ml-5" title="Settings" @click="setConfig">
<Icon size="middle" iconName="settings" />
</span>
<span
class="switch-icon ml-5"
title="Back to overview topology"
@click="backToTopology"
>
<Icon size="middle" iconName="keyboard_backspace" />
</span>
<div class="settings" v-show="showSettings">
<Settings @update="updateConfig" />
</div>
</div>
<div
class="sankey"
:style="`height:${height}px;width:${width}px;`"
v-loading="loading"
@click="handleClick"
>
<Sankey @click="selectNodeLink" />
</div>
<div
class="operations-list"
v-if="topologyStore.node"
:style="{
top: operationsPos.y + 'px',
left: operationsPos.x + 'px',
}"
>
<i v-for="(item, index) of items" :key="index" @click="item.func">
<span
v-if="
['alarm', 'inspect'].includes(item.id) ||
(item.id === 'dashboard' && settings.nodeDashboard)
"
>
{{ item.title }}
</span>
</i>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { ref, onMounted, reactive } from "vue";
import { Option } from "@/types/app";
import { useTopologyStore } from "@/store/modules/topology";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import { EntityType, DepthList } from "../../../data";
import { ElMessage } from "element-plus";
import Sankey from "./Sankey.vue";
import Settings from "./Settings.vue";
import router from "@/router";
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore();
const topologyStore = useTopologyStore();
const loading = ref<boolean>(false);
const height = ref<number>(document.body.clientHeight - 150);
const width = ref<number>(document.body.clientWidth - 40);
const showSettings = ref<boolean>(false);
const settings = ref<any>({});
const operationsPos = reactive<{ x: number; y: number }>({ x: NaN, y: NaN });
const depth = ref<string>(topologyStore.defaultDepth);
const items = [
{ id: "inspect", title: "Inspect", func: inspect },
{ id: "dashboard", title: "View Dashboard", func: goDashboard },
{ id: "alarm", title: "View Alarm", func: goAlarm },
];
onMounted(async () => {
loadTopology(selectorStore.currentPod && selectorStore.currentPod.id);
});
async function loadTopology(id: string) {
loading.value = true;
const resp = await getTopology(id);
loading.value = false;
if (resp && resp.errors) {
ElMessage.error(resp.errors);
}
}
function inspect() {
const id = topologyStore.node.id;
topologyStore.setNode(null);
topologyStore.setLink(null);
loadTopology(id);
}
function goAlarm() {
const path = `/alarm`;
const routeUrl = router.resolve({ path });
window.open(routeUrl.href, "_blank");
topologyStore.setNode(null);
}
function goDashboard() {
const entity =
dashboardStore.entity === EntityType[2].value
? EntityType[2].value
: EntityType[3].value;
const path = `/dashboard/${dashboardStore.layerId}/${entity}/${topologyStore.node.serviceId}/${topologyStore.node.id}/${settings.value.nodeDashboard}`;
const routeUrl = router.resolve({ path });
window.open(routeUrl.href, "_blank");
topologyStore.setNode(null);
}
function setConfig() {
topologyStore.setNode(null);
showSettings.value = !showSettings.value;
}
function updateConfig(config: any) {
settings.value = config;
}
function backToTopology() {
loadTopology(selectorStore.currentPod.id);
topologyStore.setNode(null);
}
function selectNodeLink(d: any) {
if (d.dataType === "edge") {
topologyStore.setNode(null);
topologyStore.setLink(d.data);
if (!settings.value.linkDashboard) {
return;
}
const { sourceObj, targetObj } = d.data;
const entity =
dashboardStore.entity === EntityType[2].value
? EntityType[6].value
: EntityType[5].value;
const path = `/dashboard/${dashboardStore.layerId}/${entity}/${sourceObj.serviceId}/${sourceObj.id}/${targetObj.serviceId}/${targetObj.id}/${settings.value.linkDashboard}`;
const routeUrl = router.resolve({ path });
window.open(routeUrl.href, "_blank");
return;
}
topologyStore.setNode(d.data);
topologyStore.setLink(null);
operationsPos.x = d.event.event.clientX;
operationsPos.y = d.event.event.clientY;
}
async function changeDepth(opt: Option[]) {
depth.value = opt[0].value;
loadTopology(selectorStore.currentPod.id);
}
async function getTopology(id: string) {
let resp;
switch (dashboardStore.entity) {
case EntityType[2].value:
resp = await topologyStore.updateEndpointTopology(
[id],
Number(depth.value)
);
break;
case EntityType[4].value:
resp = await topologyStore.getInstanceTopology();
break;
}
return resp;
}
function handleClick(event: any) {
if (event.target.nodeName === "svg") {
topologyStore.setNode(null);
topologyStore.setLink(null);
}
}
</script>
<style lang="scss" scoped>
.sankey {
margin-top: 10px;
background-color: #333840;
color: #ddd;
}
.settings {
position: absolute;
top: 40px;
right: 0;
width: 400px;
height: 700px;
background-color: #2b3037;
overflow: auto;
padding: 0 15px;
border-radius: 3px;
color: #ccc;
transition: all 0.5ms linear;
z-index: 99;
text-align: left;
}
.tool {
text-align: right;
margin-top: 10px;
position: relative;
}
.switch-icon {
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
cursor: pointer;
transition: all 0.5ms linear;
background-color: #252a2f99;
color: #ddd;
display: inline-block;
border-radius: 3px;
}
.label {
color: #ccc;
display: inline-block;
margin-right: 5px;
}
.operations-list {
position: absolute;
padding: 10px;
color: #333;
cursor: pointer;
background-color: #fff;
border-radius: 3px;
span {
display: block;
height: 30px;
width: 140px;
line-height: 30px;
text-align: center;
}
span:hover {
color: #409eff;
background-color: #eee;
}
i {
font-style: normal;
}
}
</style>

View File

@ -0,0 +1,131 @@
<!-- 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>
<Graph :option="option" @select="clickChart" />
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { useTopologyStore } from "@/store/modules/topology";
import { Node, Call } from "@/types/topology";
/*global defineEmits */
const emit = defineEmits(["click"]);
const topologyStore = useTopologyStore();
const option = computed(() => getOption());
function getOption() {
return {
tooltip: {
trigger: "item",
},
series: {
type: "sankey",
left: 40,
top: 20,
right: 300,
bottom: 40,
emphasis: { focus: "adjacency" },
data: topologyStore.nodes,
links: topologyStore.calls,
label: {
color: "#fff",
formatter: (param: any) => param.data.name,
},
color: [
"#3fe1da",
"#6be6c1",
"#3fcfdc",
"#626c91",
"#3fbcde",
"#a0a7e6",
"#3fa9e1",
"#96dee8",
"#bf99f8",
],
itemStyle: {
borderWidth: 0,
},
lineStyle: {
color: "source",
opacity: 0.12,
},
tooltip: {
position: "bottom",
formatter: (param: { data: any; dataType: string }) => {
if (param.dataType === "edge") {
return linkTooltip(param.data);
}
return nodeTooltip(param.data);
},
},
},
};
}
function linkTooltip(data: Call) {
const clientMetrics: string[] = Object.keys(topologyStore.linkClientMetrics);
const serverMetrics: string[] = Object.keys(topologyStore.linkServerMetrics);
const htmlServer = serverMetrics.map((m) => {
const metric = topologyStore.linkServerMetrics[m].values.filter(
(val: { id: string; value: unknown }) => val.id === data.id
)[0];
if (metric) {
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
return ` <div><span>${m}: </span>${val}</div>`;
}
});
const htmlClient = clientMetrics.map((m) => {
const metric = topologyStore.linkClientMetrics[m].values.filter(
(val: { id: string; value: unknown }) => val.id === data.id
)[0];
if (metric) {
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
return ` <div><span>${m}: </span>${val}</div>`;
}
});
const html = [
`<div>${data.sourceObj.serviceName} -> ${data.targetObj.serviceName}</div>`,
...htmlServer,
...htmlClient,
].join(" ");
return html;
}
function nodeTooltip(data: Node) {
const nodeMetrics: string[] = Object.keys(topologyStore.nodeMetrics);
const html = nodeMetrics.map((m) => {
const metric =
topologyStore.nodeMetrics[m].values.filter(
(val: { id: string; value: unknown }) => val.id === data.id
)[0] || {};
const val = m.includes("_sla") ? metric.value / 100 : metric.value;
return ` <div><span>${m}: </span>${val}</div>`;
});
return [` <div><span>name: </span>${data.serviceName}</div>`, ...html].join(
" "
);
}
function clickChart(param: any) {
emit("click", param);
}
</script>
<style lang="scss" scoped>
.sankey {
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,402 @@
<!-- 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-settings">
<h5 class="title">{{ t("callSettings") }}</h5>
<div class="label">{{ t("linkDashboard") }}</div>
<el-input
v-model="states.linkDashboard"
placeholder="Please input a dashboard name for calls"
@change="updateSettings"
size="small"
class="inputs"
/>
<div class="label">{{ t("linkServerMetrics") }}</div>
<Selector
class="inputs"
:multiple="true"
:value="states.linkServerMetrics"
:options="states.linkMetricList"
size="small"
placeholder="Select metrics"
@change="changeLinkServerMetrics"
/>
<span v-show="dashboardStore.entity !== EntityType[2].value">
<div class="label">
{{ t("linkClientMetrics") }}
</div>
<Selector
class="inputs"
:multiple="true"
:value="states.linkClientMetrics"
:options="states.linkMetricList"
size="small"
placeholder="Select metrics"
@change="changeLinkClientMetrics"
/>
</span>
</div>
<div class="node-settings">
<h5 class="title">{{ t("nodeSettings") }}</h5>
<div class="label">{{ t("nodeDashboard") }}</div>
<el-input
v-show="!isServer"
v-model="states.nodeDashboard"
placeholder="Please input a dashboard name for nodes"
@change="updateSettings"
size="small"
class="inputs"
/>
<div
v-show="isServer"
v-for="(item, index) in items"
:key="index"
class="metric-item"
>
<Selector
:value="item.scope"
:options="ScopeType"
size="small"
placeholder="Select a scope"
@change="changeScope(index, $event)"
class="item mr-5"
/>
<el-input
v-model="item.dashboard"
placeholder="Please input a dashboard name for nodes"
@change="updateNodeDashboards(index, $event)"
size="small"
class="item mr-5"
/>
<span>
<Icon
class="cp mr-5"
v-show="items.length > 1"
iconName="remove_circle_outline"
size="middle"
@click="deleteItem(index)"
/>
<Icon
class="cp"
v-show="index === items.length - 1 && items.length < 5"
iconName="add_circle_outlinecontrol_point"
size="middle"
@click="addItem"
/>
</span>
</div>
<div class="label">{{ t("nodeMetrics") }}</div>
<Selector
class="inputs"
:multiple="true"
:value="states.nodeMetrics"
:options="states.nodeMetricList"
size="small"
placeholder="Select metrics"
@change="changeNodeMetrics"
/>
</div>
<div class="legend-settings" v-show="isServer">
<h5 class="title">{{ t("legendSettings") }}</h5>
<div class="label">{{ t("conditions") }}</div>
<div v-for="(metric, index) of legend.metric" :key="metric.name + index">
<Selector
class="item"
:value="metric.name"
:options="states.nodeMetricList"
size="small"
placeholder="Select a metric"
@change="changeLegend(LegendOpt.NAME, $event, index)"
/>
<Selector
class="input-small"
:value="metric.condition"
:options="MetricConditions"
size="small"
placeholder="Select a condition"
@change="changeLegend(LegendOpt.CONDITION, $event, index)"
/>
<el-input
v-model="metric.value"
placeholder="Please input a value"
@change="changeLegend(LegendOpt.VALUE, $event, index)"
size="small"
class="item"
/>
<span>
<Icon
class="cp delete"
iconName="remove_circle_outline"
size="middle"
@click="deleteMetric(index)"
v-show="legend.metric.length > 1"
/>
<Icon
class="cp"
iconName="add_circle_outlinecontrol_point"
size="middle"
v-show="
index === legend.metric.length - 1 && legend.metric.length < 5
"
@click="addMetric"
/>
</span>
<div v-show="index !== legend.metric.length - 1">&&</div>
</div>
<!-- <div class="label">{{ t("conditions") }}</div>
<Selector
class="inputs"
:value="legend.condition"
:options="LegendConditions"
size="small"
placeholder="Select a condition"
@change="changeCondition"
/> -->
<el-button
@click="setLegend"
class="legend-btn"
size="small"
type="primary"
>
{{ t("setLegend") }}
</el-button>
</div>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useTopologyStore } from "@/store/modules/topology";
import { ElMessage } from "element-plus";
import { MetricCatalog, ScopeType, MetricConditions } from "../../../data";
import { Option } from "@/types/app";
import { useQueryTopologyMetrics } from "@/hooks/useProcessor";
import { Node, Call } from "@/types/topology";
import { EntityType, LegendOpt } from "../../../data";
/*global defineEmits */
const emit = defineEmits(["update", "updateNodes"]);
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const topologyStore = useTopologyStore();
const items = reactive<
{
scope: string;
dashboard: string;
}[]
>([{ scope: "", dashboard: "" }]);
const states = reactive<{
linkDashboard: string;
nodeDashboard: {
scope: string;
dashboard: string;
}[];
linkServerMetrics: string[];
linkClientMetrics: string[];
nodeMetrics: string[];
nodeMetricList: Option[];
linkMetricList: Option[];
}>({
linkDashboard: "",
nodeDashboard: [],
linkServerMetrics: [],
linkClientMetrics: [],
nodeMetrics: [],
nodeMetricList: [],
linkMetricList: [],
});
const isServer = [EntityType[0].value, EntityType[1].value].includes(
dashboardStore.entity
);
const legend = reactive<{
metric: { name: string; condition: string; value: string }[];
}>({ metric: [{ name: "", condition: "", value: "" }] });
getMetricList();
async function getMetricList() {
const json = await dashboardStore.fetchMetricList();
if (json.errors) {
ElMessage.error(json.errors);
return;
}
const entity =
dashboardStore.entity === EntityType[1].value
? EntityType[0].value
: dashboardStore.entity === EntityType[4].value
? EntityType[3].value
: dashboardStore.entity;
states.nodeMetricList = (json.data.metrics || []).filter(
(d: { catalog: string }) => entity === (MetricCatalog as any)[d.catalog]
);
const e =
dashboardStore.entity === EntityType[1].value
? EntityType[0].value
: dashboardStore.entity === EntityType[4].value
? EntityType[3].value
: dashboardStore.entity;
states.linkMetricList = (json.data.metrics || []).filter(
(d: { catalog: string }) =>
e + "Relation" === (MetricCatalog as any)[d.catalog]
);
}
async function setLegend() {
const metrics = legend.metric.filter(
(d: any) => d.name && d.value && d.condition
);
const names = metrics.map((d: any) => d.name);
emit("update", {
linkDashboard: states.linkDashboard,
nodeDashboard: isServer
? items.filter((d: { scope: string; dashboard: string }) => d.dashboard)
: states.nodeDashboard,
linkServerMetrics: states.linkServerMetrics,
linkClientMetrics: states.linkClientMetrics,
nodeMetrics: states.nodeMetrics,
legend: metrics,
});
const ids = topologyStore.nodes.map((d: Node) => d.id);
const param = await useQueryTopologyMetrics(names, ids);
const res = await topologyStore.getLegendMetrics(param);
if (res.errors) {
ElMessage.error(res.errors);
}
emit("updateNodes");
}
function changeLegend(type: string, opt: any, index: number) {
(legend.metric[index] as any)[type] = opt[0].value || opt;
}
function changeScope(index: number, opt: Option[]) {
items[index].scope = opt[0].value;
items[index].dashboard = "";
}
function updateNodeDashboards(index: number, content: string) {
items[index].dashboard = content;
updateSettings();
}
function addItem() {
items.push({ scope: "", dashboard: "" });
}
function deleteItem(index: number) {
items.splice(index, 1);
updateSettings();
}
function updateSettings() {
emit("update", {
linkDashboard: states.linkDashboard,
nodeDashboard: isServer
? items.filter((d: { scope: string; dashboard: string }) => d.dashboard)
: states.nodeDashboard,
linkServerMetrics: states.linkServerMetrics,
linkClientMetrics: states.linkClientMetrics,
nodeMetrics: states.nodeMetrics,
legend: legend.metric,
});
}
async function changeLinkServerMetrics(options: Option[]) {
states.linkServerMetrics = options.map((d: Option) => d.value);
updateSettings();
if (!states.linkServerMetrics.length) {
topologyStore.setLinkServerMetrics({});
return;
}
const idsS = topologyStore.calls
.filter((i: Call) => i.detectPoints.includes("SERVER"))
.map((b: Call) => b.id);
const param = await useQueryTopologyMetrics(states.linkServerMetrics, idsS);
const res = await topologyStore.getCallServerMetrics(param);
if (res.errors) {
ElMessage.error(res.errors);
}
}
async function changeLinkClientMetrics(options: Option[]) {
states.linkClientMetrics = options.map((d: Option) => d.value);
updateSettings();
if (!states.linkClientMetrics.length) {
topologyStore.setLinkClientMetrics({});
return;
}
const idsC = topologyStore.calls
.filter((i: Call) => i.detectPoints.includes("CLIENT"))
.map((b: Call) => b.id);
const param = await useQueryTopologyMetrics(states.linkClientMetrics, idsC);
const res = await topologyStore.getCallClientMetrics(param);
if (res.errors) {
ElMessage.error(res.errors);
}
}
async function changeNodeMetrics(options: Option[]) {
states.nodeMetrics = options.map((d: Option) => d.value);
updateSettings();
if (!states.nodeMetrics.length) {
topologyStore.setNodeMetrics({});
return;
}
const ids = topologyStore.nodes.map((d: Node) => d.id);
const param = await useQueryTopologyMetrics(states.nodeMetrics, ids);
const res = await topologyStore.getNodeMetrics(param);
if (res.errors) {
ElMessage.error(res.errors);
}
}
function deleteMetric(index: number) {
legend.metric.splice(index, 1);
}
function addMetric() {
legend.metric.push({ name: "", condition: "", value: "" });
}
</script>
<style lang="scss" scoped>
.link-settings {
margin-bottom: 20px;
}
.inputs {
margin-top: 8px;
width: 370px;
}
.item {
width: 130px;
margin-top: 5px;
}
.input-small {
width: 45px;
margin: 0 3px;
}
.title {
margin-bottom: 0;
}
.label {
font-size: 12px;
margin-top: 10px;
}
.legend-btn {
margin: 20px 0;
cursor: pointer;
}
.delete {
margin: 0 3px;
}
</style>

View File

@ -0,0 +1,53 @@
/**
* 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";
export default function topoLegend(
graph: any,
clientHeight: number,
clientWidth: number,
config: any
) {
for (const item of ["CUBE", "CUBEERROR"]) {
graph
.append("image")
.attr("width", 30)
.attr("height", 30)
.attr("x", clientWidth - (item === "CUBEERROR" ? 340 : 440))
.attr("y", clientHeight - 50)
.attr("xlink:href", () =>
item === "CUBEERROR" ? icons.CUBEERROR : icons.CUBE
);
graph
.append("text")
.attr("x", clientWidth - (item === "CUBEERROR" ? 310 : 410))
.attr("y", clientHeight - 30)
.text(() => {
const l = config || [];
const str = l
.map((d: any) => `${d.name} ${d.condition} ${d.value}`)
.join(" and ");
return item === "CUBEERROR"
? config
? `Unhealthy (${str})`
: "Unhealthy"
: "Healthy";
})
.style("fill", "#efeff1")
.style("font-size", "11px");
}
}

View File

@ -0,0 +1,60 @@
/**
* 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

@ -0,0 +1,96 @@
/**
* 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 { 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) {
const val = l.name.includes("_sla") ? d[l.name] / 100 : d[l.name];
if (l.condition === "<") {
c = c && val < Number(l.value);
} else {
c = c && val > 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

@ -0,0 +1,56 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const simulationInit = (
d3: any,
dataNodes: any,
dataLinks: any,
ticked: any
) => {
const simulation = d3
.forceSimulation(dataNodes)
.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(dataLinks).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

@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default (d3: any, graph: any) =>
d3
.zoom()
.scaleExtent([0.3, 10])
.on("zoom", (d: any) => {
graph
.attr("transform", d3.zoomTransform(graph.node()))
.attr(
`translate(${d.transform.x},${d.transform.y})scale(${d.transform.k})`
);
});

View File

@ -30,6 +30,7 @@
"baseUrl": ".", "baseUrl": ".",
"allowJs": true, "allowJs": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"noImplicitThis": false,
"types": [ "types": [
"webpack-env", "webpack-env",
"jest" "jest"