41 Commits

Author SHA1 Message Date
dependabot[bot]
29238f2a54 build(deps): bump uuid, vis-timeline and cypress
Bumps [uuid](https://github.com/uuidjs/uuid) to 14.0.0 and updates ancestor dependencies [uuid](https://github.com/uuidjs/uuid), [vis-timeline](https://github.com/visjs/vis-timeline) and [cypress](https://github.com/cypress-io/cypress). These dependencies need to be updated together.


Updates `uuid` from 8.3.2 to 14.0.0
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v8.3.2...v14.0.0)

Updates `vis-timeline` from 7.7.0 to 8.5.1
- [Release notes](https://github.com/visjs/vis-timeline/releases)
- [Changelog](https://github.com/visjs/vis-timeline/blob/master/HISTORY.md)
- [Commits](https://github.com/visjs/vis-timeline/compare/v7.7.0...v8.5.1)

Updates `cypress` from 13.3.2 to 15.15.0
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.3.2...v15.15.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 14.0.0
  dependency-type: indirect
- dependency-name: vis-timeline
  dependency-version: 8.5.1
  dependency-type: direct:production
- dependency-name: cypress
  dependency-version: 15.15.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 22:43:35 +00:00
dependabot[bot]
0dfb65bad3 build(deps-dev): bump axios from 1.15.0 to 1.16.0 (#551) 2026-05-08 10:18:47 +08:00
Fine0830
99b5083ea5 fix: add templates to optimize log content and update topology tooltips (#550) 2026-04-29 20:58:36 +08:00
Fine0830
2c1e8511e7 feat: support trace v1 in trace single page (#549) 2026-04-27 19:04:47 +08:00
吴晟 Wu Sheng
d39db06b34 Add WeChat and Alipay Mini Program i18n menu entries (#548) 2026-04-21 15:41:06 +08:00
Fine0830
f6c6612af8 fix: add flex-shrink style for labels (#547) 2026-04-18 14:09:25 +08:00
Fine0830
a6be0e0e0d fix: correct metric labels in dashboards (#546) 2026-04-17 22:48:24 +08:00
吴晟 Wu Sheng
d2879e3f37 Add mobile menu icon and i18n labels (#545) 2026-04-17 10:14:36 +08:00
dependabot[bot]
85c76575b2 build(deps-dev): bump follow-redirects from 1.15.11 to 1.16.0 (#544) 2026-04-16 10:36:59 +08:00
Jingyi Qu
54ba66db92 Add the front end for pprof profiling (#541) 2026-04-13 10:38:26 +08:00
dependabot[bot]
ed4841450c build(deps): bump lodash from 4.17.23 to 4.18.1 (#540) 2026-04-12 20:58:02 +08:00
dependabot[bot]
010844c374 build(deps-dev): bump axios from 1.13.4 to 1.15.0 (#543) 2026-04-12 20:54:01 +08:00
dependabot[bot]
ffca69ec66 build(deps-dev): bump vite from 6.4.1 to 6.4.2 (#542) 2026-04-07 13:09:08 +08:00
dependabot[bot]
f6a6afd2e0 build(deps): bump lodash-es from 4.17.23 to 4.18.1 (#539) 2026-04-02 21:25:23 +08:00
吴晟 Wu Sheng
4e8a7576ac Add Envoy AI Gateway i18n menu entries (#538) (#538) 2026-04-01 07:52:49 +08:00
Fine0830
431bcc0891 fix: set the step to SECOND in the duration for Log/Trace/Alarm/Tag (#537) 2026-03-30 12:36:59 +08:00
dependabot[bot]
370bfbc87d build(deps): bump picomatch (#536) 2026-03-28 11:55:55 +08:00
dependabot[bot]
8b004ef316 build(deps-dev): bump flatted from 3.2.7 to 3.4.2 (#535) 2026-03-20 08:55:50 +08:00
peachisai
6538cc401d Add the gen-ai menu (#534) 2026-03-17 09:06:03 +08:00
dependabot[bot]
93f2e70e6c build(deps): bump undici from 7.22.0 to 7.24.1 (#533) 2026-03-14 20:15:46 +08:00
Fine0830
64e2cab386 fix cold stage label (#532) 2026-03-08 20:37:44 +08:00
dependabot[bot]
cf330e6cfd build(deps): bump @tootallnate/once and jsdom (#531) 2026-03-05 10:48:31 +08:00
dependabot[bot]
fc9b68d93d build(deps): bump immutable from 5.0.3 to 5.1.5 (#529) 2026-03-05 10:44:50 +08:00
dependabot[bot]
6a7cdbf9f8 build(deps-dev): bump svgo from 2.8.0 to 2.8.2 (#530) 2026-03-05 10:41:38 +08:00
dependabot[bot]
f31aa90b6a build(deps): bump minimatch (#528) 2026-02-28 10:25:02 +08:00
dependabot[bot]
de6b493bf2 build(deps): bump rollup from 4.40.1 to 4.59.0 (#527) 2026-02-28 10:21:23 +08:00
peachisai
6be09fb26b Add the GenAI icon (#525) 2026-02-25 21:42:45 +08:00
dependabot[bot]
1a511ae1a0 build(deps-dev): bump qs from 6.14.1 to 6.14.2 (#523) 2026-02-25 09:50:02 +08:00
peachisai
b7bcbf1740 Fix incorrect virtual service names (#524) 2026-02-23 22:58:02 +08:00
Fine0830
49a51d2a37 refactor: optimize the pages theme (#522) 2026-02-10 14:45:31 +08:00
Fine0830
3c907950e7 feat: add coldStage to the Duration (#521) 2026-02-10 12:44:14 +08:00
Fine0830
4e3b1bdeae Fix validation guard for router (#520) 2026-02-05 23:01:05 +13:00
dependabot[bot]
41b323400f build(deps): bump qs and @cypress/request (#519) 2026-02-05 17:21:47 +08:00
dependabot[bot]
d90ff89de1 build(deps): bump lodash-es from 4.17.21 to 4.17.23 (#518) 2026-02-05 17:17:56 +08:00
dependabot[bot]
fe767250b9 build(deps): bump lodash from 4.17.21 to 4.17.23 (#517) 2026-01-22 10:47:33 +08:00
Fine0830
0334c28da5 refactor: Implement a common pagination component (#516) 2026-01-06 18:19:35 +08:00
Fine0830
f74a59f757 fix: correct logic for the log pagination (#515) 2026-01-05 18:42:31 +08:00
KitAndrew
a4e9908b43 fix: update current service in store before navigating to dashboard (#514) 2025-11-21 11:02:59 +08:00
dependabot[bot]
851c89925a build(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 (#513) 2025-11-16 18:34:48 +08:00
youjie23
6eaf7fe26d feat: enhance the alarm kernel with recovered status notification capability #13492 (#505) 2025-11-14 10:19:55 +08:00
Fine0830
28c2cbd609 fix: change icons for trace types (#512) 2025-11-10 20:22:00 +08:00
71 changed files with 5170 additions and 3312 deletions

View File

@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
node-version: [20.x, 22.x, 24.x]
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}

5718
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,7 @@
"element-plus": "^2.11.0",
"monaco-editor": "^0.34.1",
"pinia": "^2.0.28",
"vis-timeline": "^7.5.1",
"vis-timeline": "^8.5.1",
"vue": "^3.2.45",
"vue-grid-layout": "^3.0.0-beta1",
"vue-i18n": "^9.14.5",
@@ -49,22 +49,22 @@
"@types/d3-tip": "^3.5.5",
"@types/echarts": "^4.9.12",
"@types/jsdom": "^20.0.1",
"@types/node": "^18.11.12",
"@types/node": "^22.19.10",
"@types/three": "^0.131.0",
"@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"@vitest/coverage-v8": "^3.0.6",
"@vitest/coverage-v8": "^4.0.18",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.2.6",
"@vue/tsconfig": "^0.1.3",
"@vueuse/core": "^9.6.0",
"cypress": "^13.3.2",
"cypress": "^15.15.0",
"eslint": "^8.22.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-vue": "^9.3.0",
"husky": "^8.0.2",
"jsdom": "^20.0.3",
"jsdom": "^28.1.0",
"lint-staged": "^13.2.1",
"mockjs": "^1.1.0",
"npm-run-all": "^4.1.5",
@@ -81,10 +81,10 @@
"typescript": "^5.7.3",
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.3",
"vite": "^6.4.1",
"vite": "^6.4.2",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svg-icons": "^2.0.1",
"vitest": "^3.0.5",
"vitest": "^4.0.18",
"vue-tsc": "^2.2.2"
},
"browserslist": [

View File

@@ -0,0 +1,16 @@
<!-- 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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64" fill="currentColor"><path d="M16.4004 21H14.2461L12.2461 16H5.75391L3.75391 21H1.59961L8 4.99996H10L16.4004 21ZM21 12V21H19V12H21ZM6.55371 14H11.4463L9 7.88473L6.55371 14ZM19.5293 2.3193C19.7058 1.89351 20.2942 1.8935 20.4707 2.3193L20.7236 2.93063C21.1555 3.97343 21.9615 4.80613 22.9746 5.2568L23.6914 5.57613C24.1022 5.75881 24.1022 6.35634 23.6914 6.53902L22.9326 6.87691C21.945 7.31619 21.1534 8.11942 20.7139 9.12789L20.4668 9.69332C20.2863 10.1075 19.7136 10.1075 19.5332 9.69332L19.2861 9.12789C18.8466 8.11941 18.0551 7.31619 17.0674 6.87691L16.3076 6.53902C15.8974 6.35617 15.8974 5.75894 16.3076 5.57613L17.0254 5.2568C18.0384 4.80613 18.8445 3.97343 19.2764 2.93063L19.5293 2.3193Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 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 class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32">
<path d="M896 469.333333h-341.333333c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667h341.333333c25.6 0 42.666667-17.066667 42.666667-42.666667s-17.066667-42.666667-42.666667-42.666667zM341.333333 298.666667h554.666667c25.6 0 42.666667-17.066667 42.666667-42.666667s-17.066667-42.666667-42.666667-42.666667H341.333333c-25.6 0-42.666667 17.066667-42.666666 42.666667s17.066667 42.666667 42.666666 42.666667zM896 725.333333h-341.333333c-25.6 0-42.666667 17.066667-42.666667 42.666667s17.066667 42.666667 42.666667 42.666667h341.333333c25.6 0 42.666667-17.066667 42.666667-42.666667s-17.066667-42.666667-42.666667-42.666667zM213.333333 554.666667h128c25.6 0 42.666667-17.066667 42.666667-42.666667s-17.066667-42.666667-42.666667-42.666667H213.333333c-25.6 0-42.666667-17.066667-42.666666-42.666666V256c0-25.6-17.066667-42.666667-42.666667-42.666667s-42.666667 17.066667-42.666667 42.666667v426.666667c0 72.533333 55.466667 128 128 128h128c25.6 0 42.666667-17.066667 42.666667-42.666667s-17.066667-42.666667-42.666667-42.666667H213.333333c-25.6 0-42.666667-17.066667-42.666666-42.666666v-136.533334c12.8 4.266667 25.6 8.533333 42.666666 8.533334z"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.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="M8.25 2.016h7.5q1.313 0 2.273 0.961t0.961 2.273v13.5q0 1.313-0.961 2.273t-2.273 0.961h-7.5q-1.313 0-2.273-0.961t-0.961-2.273v-13.5q0-1.313 0.961-2.273t2.273-0.961zM8.25 4.031q-0.469 0-0.844 0.375t-0.375 0.844v13.5q0 0.469 0.375 0.844t0.844 0.375h7.5q0.469 0 0.844-0.375t0.375-0.844v-13.5q0-0.469-0.375-0.844t-0.844-0.375h-1.734q-0.094 0.563-0.563 0.891t-1.125 0.328h-0.656q-0.656 0-1.125-0.328t-0.563-0.891h-1.734zM9.375 11.109h1.453l0.844-1.688q0.141-0.281 0.422-0.281 0.328 0 0.422 0.281l1.219 3 0.563-1.125q0.094-0.188 0.281-0.281t0.375-0.094h1.969q0.422 0 0.422 0.422t-0.422 0.422h-1.734l-1.031 2.109q-0.141 0.281-0.469 0.281-0.281 0-0.422-0.281l-1.219-2.953-0.563 1.078q-0.094 0.234-0.281 0.328t-0.422 0.094h-1.734q-0.422 0-0.422-0.422t0.422-0.422zM10.828 17.531h2.344q0.422 0 0.422 0.422t-0.422 0.422h-2.344q-0.422 0-0.422-0.422t0.422-0.422z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -12,4 +12,4 @@ 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="1619507658599" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2073" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M331.840623 793.484755 331.840623 387.450978c0-16.191531-12.457456-28.645338-27.399836-28.645338L222.235197 358.80564c-14.94238 0-27.399836 13.698094-27.399836 28.645338L194.835361 793.484755 331.840623 793.484755 331.840623 793.484755zM506.210956 793.484755 506.210956 213.081861c0-16.192747-12.453808-29.89449-27.401052-29.89449l-82.20559 0c-14.94238 0-27.399836 13.701743-27.399836 29.89449L369.204478 793.484755 506.210956 793.484755 506.210956 793.484755zM680.580073 793.484755 680.580073 536.910048c0-16.191531-12.452591-29.889625-27.399836-29.889625L570.979512 507.020423c-14.947245 0-27.405918 13.698094-27.405918 29.889625L543.573595 793.484755 680.580073 793.484755 680.580073 793.484755zM854.94919 793.484755 854.94919 387.450978c0-16.191531-12.452591-28.645338-27.399836-28.645338l-82.200725 0c-14.947245 0-27.399836 13.698094-27.399836 28.645338L717.948794 793.484755 854.94919 793.484755 854.94919 793.484755zM879.860454 830.84861" p-id="2074" fill="#ffffff"></path></svg>
<svg t="1619507658599" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2073" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M331.840623 793.484755 331.840623 387.450978c0-16.191531-12.457456-28.645338-27.399836-28.645338L222.235197 358.80564c-14.94238 0-27.399836 13.698094-27.399836 28.645338L194.835361 793.484755 331.840623 793.484755 331.840623 793.484755zM506.210956 793.484755 506.210956 213.081861c0-16.192747-12.453808-29.89449-27.401052-29.89449l-82.20559 0c-14.94238 0-27.399836 13.701743-27.399836 29.89449L369.204478 793.484755 506.210956 793.484755 506.210956 793.484755zM680.580073 793.484755 680.580073 536.910048c0-16.191531-12.452591-29.889625-27.399836-29.889625L570.979512 507.020423c-14.947245 0-27.405918 13.698094-27.405918 29.889625L543.573595 793.484755 680.580073 793.484755 680.580073 793.484755zM854.94919 793.484755 854.94919 387.450978c0-16.191531-12.452591-28.645338-27.399836-28.645338l-82.200725 0c-14.947245 0-27.399836 13.698094-27.399836 28.645338L717.948794 793.484755 854.94919 793.484755 854.94919 793.484755zM879.860454 830.84861"></path></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -16,6 +16,7 @@
*/
export enum TimeType {
SECOND_TIME = "SECOND",
MINUTE_TIME = "MINUTE",
HOUR_TIME = "HOUR",
DAY_TIME = "DAY",

View File

@@ -23,6 +23,7 @@ export const Alarm = {
key: id
message
startTime
recoveryTime
scope
name
tags {

View File

@@ -0,0 +1,79 @@
/**
* 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 GetPprofTaskList = {
variable: "$request: PprofTaskListRequest!",
query: `
pprofTaskList: queryPprofTaskList(request: $request) {
errorReason
tasks {
id
serviceId
serviceInstanceIds
createTime
events
duration
dumpPeriod
}
}
`,
};
export const GetPprofTaskProcess = {
variable: "$taskId: String!",
query: `
taskProgress: queryPprofTaskProgress(taskId: $taskId) {
logs {
id
instanceId
instanceName
operationType
operationTime
}
errorInstanceIds
successInstanceIds
}
`,
};
export const CreatePprofTask = {
variable: "$pprofTaskCreationRequest: PprofTaskCreationRequest!",
query: `
task: createPprofTask(pprofTaskCreationRequest: $pprofTaskCreationRequest) {
id
errorReason
code
}
`,
};
export const GetPprofAnalyze = {
variable: "$request: PprofAnalyzationRequest!",
query: `
analysisResult: queryPprofAnalyze(request: $request) {
tree {
elements {
id
parentId
symbol: codeSignature
dumpCount: total
self
}
}
}
`,
};

View File

@@ -27,6 +27,7 @@ import * as event from "./query/event";
import * as ebpf from "./query/ebpf";
import * as demandLog from "./query/demand-log";
import * as asyncProfile from "./query/async-profile";
import * as pprof from "./query/pprof";
const query: { [key: string]: string } = {
...app,
@@ -41,6 +42,7 @@ const query: { [key: string]: string } = {
...ebpf,
...demandLog,
...asyncProfile,
...pprof,
};
class Graphql {
queryData = "";

View File

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

View File

@@ -46,6 +46,7 @@ vi.mock("@/utils/dateFormat", () => ({
describe("useDuration hook", () => {
const mockAppStore = {
utc: false,
coldStageMode: false,
} as unknown as ReturnType<typeof useAppStoreWithOut>;
beforeEach(() => {
@@ -66,7 +67,7 @@ describe("useDuration hook", () => {
setDurationRow(newDuration);
const result = getDurationTime();
expect(result.step).toBe("DAY");
expect(result.step).toBe("SECOND");
});
});
@@ -77,9 +78,10 @@ describe("useDuration hook", () => {
const result = getDurationTime();
expect(result).toEqual({
start: "2023-01-01",
end: "2023-01-01",
step: "HOUR",
start: "2023-01-01 00",
end: "2023-01-01 00",
step: "SECOND",
coldStage: false,
});
});
@@ -152,7 +154,7 @@ describe("useDuration hook", () => {
// Test getDurationTime
const durationTime = getDurationTime();
expect(durationTime.step).toBe("MINUTE");
expect(durationTime.step).toBe("SECOND");
// Test getMaxRange
const maxRange = getMaxRange(5);

View File

@@ -18,6 +18,7 @@ import { useAppStoreWithOut, InitializationDurationRow } from "@/store/modules/a
import type { Duration, DurationTime } from "@/types/app";
import getLocalTime from "@/utils/localtime";
import dateFormatStep from "@/utils/dateFormat";
import { TimeType } from "@/constants/data";
export function useDuration() {
let durationRow: Duration = InitializationDurationRow;
@@ -27,19 +28,24 @@ export function useDuration() {
return {
start: getLocalTime(appStore.utc, durationRow.start),
end: getLocalTime(appStore.utc, durationRow.end),
step: durationRow.step,
step: TimeType.SECOND_TIME,
coldStage: appStore.coldStageMode,
};
}
function getDurationTime(): DurationTime {
const { start, step, end } = getDuration();
const appStore = useAppStoreWithOut();
const { start, end } = getDuration();
const step = TimeType.SECOND_TIME;
return {
start: dateFormatStep(start, step, true),
end: dateFormatStep(end, step, true),
step: step,
step,
coldStage: appStore.coldStageMode,
};
}
function setDurationRow(data: Duration) {
durationRow = data;
const appStore = useAppStoreWithOut();
durationRow = { ...data, coldStage: appStore.coldStageMode, step: TimeType.SECOND_TIME };
}
function getMaxRange(day: number) {
if (day === undefined || day === null) {

View File

@@ -192,7 +192,7 @@ export async function useDashboardQueryProcessor(configList: DashboardWidgetConf
)
) {
for (const item of results) {
let label: string = name;
let label: string = "" as string;
if (item.metric) {
const joined = item.metric.labels
.map((d: { key: string; value: string }) => `${d.key}=${d.value}`)

View File

@@ -58,6 +58,14 @@ export function useTheme() {
// Handle theme change with transition animation
function handleChangeTheme() {
const prefersReducedMotion =
typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) {
applyTheme();
return;
}
const x = themeSwitchRef.value?.offsetLeft ?? 0;
const y = themeSwitchRef.value?.offsetTop ?? 0;
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));

View File

@@ -54,10 +54,10 @@ limitations under the License. -->
<el-switch
v-model="coldStage"
inline-prompt
active-text="Active Data"
inactive-text="Cold Data"
inactive-text="Cold Excluded"
active-text="Cold Only"
@change="changeDataMode"
width="90px"
width="105px"
/>
</span>
<span class="ml-5" ref="themeSwitchRef">
@@ -174,7 +174,20 @@ limitations under the License. -->
async function setTTL() {
await getMetricsTTL();
await getRecordsTTL();
changeDataMode();
// Initialize TTL handling without triggering duration update
if (coldStage.value) {
handleMetricsTTL({
minute: appStore.metricsTTL?.coldMinute ?? NaN,
hour: appStore.metricsTTL?.coldHour ?? NaN,
day: appStore.metricsTTL?.coldDay ?? NaN,
});
} else {
handleMetricsTTL({
minute: appStore.metricsTTL?.minute ?? NaN,
hour: appStore.metricsTTL?.hour ?? NaN,
day: appStore.metricsTTL?.day ?? NaN,
});
}
}
async function getRecordsTTL() {
const resp = await appStore.queryRecordsTTL();

View File

@@ -215,6 +215,7 @@ const msg = {
timeRange: "Time Range",
duration: "Duration",
startTime: "Start Time",
recoveryTime: "Recovery Time",
start: "Start",
spans: "Spans",
spanInfo: "Span Info",
@@ -327,6 +328,7 @@ const msg = {
message: "Message",
tooltipsContent: "Tooltip Content",
alarmDetail: "Alarm Detail",
recoveredAt: "Recovered At",
scope: "Scope",
destService: "Destination Service",
destServiceInstance: "Destination Service Instance",
@@ -393,6 +395,7 @@ const msg = {
errorInstances: "Error Instances",
successInstances: "Success Instances",
profilingEvents: "Async Profiling Events",
pprofEvent: "PProf Event",
execArgs: "Exec Args",
instances: "Instances",
snapshot: "Snapshot",
@@ -405,7 +408,16 @@ const msg = {
maxDuration: "Max Duration",
minutes: "Minutes",
invalidProfilingDurationRange: "Please enter a valid duration between 1 and 900 seconds",
invalidPprofDuration: "Please enter a valid duration in minutes",
invalidPprofDumpPeriod: "Please enter a valid dump period",
pprofDumpPeriod: "Dump Period",
pprofDurationHint: "Duration is required for CPU, BLOCK and MUTEX tasks.",
pprofDumpPeriodBlockHint:
"For BLOCK tasks, dump period is required and represents the blocked nanoseconds sampling rate. 1 samples every event.",
pprofDumpPeriodMutexHint:
"For MUTEX tasks, dump period is required and represents the contention occurrences sampling rate. 1 samples every event.",
taskCreatedSuccessfully: "Task created successfully",
taskCreationFailed: "Task creation failed",
runQuery: "Run Query",
spansTable: "Spans Table",
download: "Download",

View File

@@ -213,6 +213,7 @@ const msg = {
timeRange: "Rango de Tiempo",
duration: "Duración",
startTime: "Hora Inicio",
recoveryTime: "Tiempo Recuperación",
start: "Incio",
spans: "Lapso",
spanInfo: "Info Lapso",
@@ -324,6 +325,7 @@ const msg = {
message: "Mensaje",
tooltipsContent: "Contenido de Información de Herramienta",
alarmDetail: "Detalle Alarma",
recoveredAt: "Recuperado En",
scope: "Alcance",
destService: "Servicio Destinación",
destServiceInstance: "Instancia Servicio Destinación",
@@ -392,6 +394,7 @@ const msg = {
errorInstances: "Error Instances",
successInstances: "Success Instances",
profilingEvents: "Async Profiling Events",
pprofEvent: "Evento PProf",
execArgs: "Exec Args",
instances: "Instances",
snapshot: "Snapshot",
@@ -405,7 +408,16 @@ const msg = {
maxDuration: "Duración Máxima",
minutes: "Minutos",
invalidProfilingDurationRange: "Por favor ingrese una duración válida entre 1 y 900 segundos",
invalidPprofDuration: "Por favor ingrese una duración válida en minutos",
invalidPprofDumpPeriod: "Por favor ingrese un período de volcado válido",
pprofDumpPeriod: "Período de Volcado",
pprofDurationHint: "La duración es obligatoria para tareas CPU, BLOCK y MUTEX.",
pprofDumpPeriodBlockHint:
"Para tareas BLOCK, el período de volcado es obligatorio y representa la tasa de muestreo en nanosegundos bloqueados. 1 muestrea todos los eventos.",
pprofDumpPeriodMutexHint:
"Para tareas MUTEX, el período de volcado es obligatorio y representa la tasa de muestreo por ocurrencias de contención. 1 muestrea todos los eventos.",
taskCreatedSuccessfully: "Tarea creada exitosamente",
taskCreationFailed: "Error al crear la tarea",
runQuery: "Ejecutar Consulta",
spansTable: "Tabla de Lapso",
download: "Descargar",

View File

@@ -21,10 +21,10 @@ const titles = {
"Observe services and relative direct dependencies through telemetry data collected from SkyWalking Agents.",
general_service_services: "Services",
general_service_services_desc: "Observe services through telemetry data collected from SkyWalking Agent.",
general_service_virtual_database: "Visual Database",
general_service_virtual_database: "Virtual Database",
general_service_virtual_database_desc:
"Observe the virtual databases which are conjectured by language agents through various plugins.",
general_service_virtual_cache: "Visual Cache",
general_service_virtual_cache: "Virtual Cache",
general_service_virtual_cache_desc:
"Observe the virtual cache servers which are conjectured by language agents through various plugins.",
general_service_virtual_mq: "Virtual MQ",
@@ -76,6 +76,18 @@ const titles = {
// Browser
browser: "Browser",
browser_desc: "Provide Browser-Side monitoring of Web-App, Versions and Pages, through Apache SkyWalking Client JS.",
// Mobile
mobile: "Mobile",
mobile_desc: "Mobile application monitoring via OpenTelemetry SDKs.",
mobile_ios: "iOS",
mobile_ios_desc:
"iOS and iPadOS app monitoring via OpenTelemetry Swift SDK. Provides HTTP performance, MetricKit daily stats, and crash diagnostics.",
mobile_wechat_mini_program: "WeChat Mini Program",
mobile_wechat_mini_program_desc:
"WeChat Mini Program monitoring via the mini-program-monitor SDK. Launch / render / route / script / package-load perf, request latency percentiles, error counts, per-page breakdowns, plus trace drill-down for outbound HTTP.",
mobile_alipay_mini_program: "Alipay Mini Program",
mobile_alipay_mini_program_desc:
"Alipay Mini Program monitoring via the mini-program-monitor SDK. Lifecycle-based launch / render approximations, request latency percentiles, error counts, per-page breakdowns, plus trace drill-down for outbound HTTP.",
// Gateway
gateway: "Gateway",
gateway_desc:
@@ -144,6 +156,14 @@ const titles = {
data_processing_engine_flink: "Flink",
data_processing_engine_flink_desc:
"Apache Flink is a framework and distributed processing engine for stateful computations over unbounded and bounded data streams. Flink has been designed to run in all common cluster environments, perform computations at in-memory speed and at any scale.",
gen_ai: "Generative AI",
gen_ai_desc:
"Generative AI (GenAI) refers to a category of artificial intelligence that can create new content. Provide monitoring for GenAI providers and model calls.",
virtual_gen_ai: "Virtual GenAI",
virtual_gen_ai_desc:
"Observe the virtual GenAI services and models which are conjectured by language agents through various plugins.",
envoy_ai_gateway: "Envoy AI Gateway",
envoy_ai_gateway_desc: "Provide Envoy AI Gateway monitoring through OpenTelemetry OTLP metrics and access logs.",
};
export default titles;

View File

@@ -77,6 +77,18 @@ const titles = {
// Browser
browser: "Navegador",
browser_desc: "Provide Browser-Side monitoring of Web-App, Versions and Pages, through Apache SkyWalking Client JS.",
// Mobile
mobile: "Mobile",
mobile_desc: "Mobile application monitoring via OpenTelemetry SDKs.",
mobile_ios: "iOS",
mobile_ios_desc:
"iOS and iPadOS app monitoring via OpenTelemetry Swift SDK. Provides HTTP performance, MetricKit daily stats, and crash diagnostics.",
mobile_wechat_mini_program: "WeChat Mini Program",
mobile_wechat_mini_program_desc:
"WeChat Mini Program monitoring via the mini-program-monitor SDK. Launch / render / route / script / package-load perf, request latency percentiles, error counts, per-page breakdowns, plus trace drill-down for outbound HTTP.",
mobile_alipay_mini_program: "Alipay Mini Program",
mobile_alipay_mini_program_desc:
"Alipay Mini Program monitoring via the mini-program-monitor SDK. Lifecycle-based launch / render approximations, request latency percentiles, error counts, per-page breakdowns, plus trace drill-down for outbound HTTP.",
// Gateway
gateway: "Puerta",
gateway_desc:
@@ -146,6 +158,15 @@ const titles = {
data_processing_engine_flink: "Flink",
data_processing_engine_flink_desc:
"Apache Flink is a framework and distributed processing engine for stateful computations over unbounded and bounded data streams. Flink has been designed to run in all common cluster environments, perform computations at in-memory speed and at any scale.",
gen_ai: "IA Generativa",
gen_ai_desc:
"La Inteligencia Artificial Generativa (GenAI) es una categoría de IA capaz de crear contenido nuevo. Permite monitorear proveedores de GenAI e invocaciones a sus modelos.",
virtual_gen_ai: "IA Generativa Virtual",
virtual_gen_ai_desc:
"Monitorea los servicios y modelos de IA generativa virtual detectados por los agentes a través de diversos complementos (plugins).",
envoy_ai_gateway: "Puerta de Enlace de IA Envoy",
envoy_ai_gateway_desc:
"Proporciona monitoreo de Envoy AI Gateway a través de métricas OTLP y logs de acceso de OpenTelemetry.",
};
export default titles;

View File

@@ -68,6 +68,18 @@ const titles = {
// Browser
browser: "Browser",
browser_desc: "通过Apache SkyWalking Client JS提供Web应用程序、版本和页面的浏览器端监控。",
// Mobile
mobile: "移动端",
mobile_desc: "通过 OpenTelemetry SDK 提供移动应用监控。",
mobile_ios: "iOS",
mobile_ios_desc:
"通过 OpenTelemetry Swift SDK 提供 iOS 和 iPadOS 应用监控,包括 HTTP 性能、MetricKit 每日统计和崩溃诊断。",
mobile_wechat_mini_program: "微信小程序",
mobile_wechat_mini_program_desc:
"通过 mini-program-monitor SDK 提供微信小程序监控。包括启动 / 渲染 / 路由 / 脚本 / 分包加载性能、请求延迟分位数、错误计数、按页面细分,以及出站 HTTP 的链路追踪下钻。",
mobile_alipay_mini_program: "支付宝小程序",
mobile_alipay_mini_program_desc:
"通过 mini-program-monitor SDK 提供支付宝小程序监控。包括基于生命周期的启动 / 渲染近似值、请求延迟分位数、错误计数、按页面细分,以及出站 HTTP 的链路追踪下钻。",
// Gateway
gateway: "网关",
gateway_desc: "API网关是位于客户端和后端服务集合之间的API管理工具。",
@@ -126,6 +138,12 @@ const titles = {
data_processing_engine_flink: "Flink",
data_processing_engine_flink_desc:
"Apache Flink 是一个框架和分布式处理引擎用于在无边界和有边界数据流上进行有状态的计算。Flink 能在所有常见集群环境中运行,并能以内存速度和任意规模进行计算。",
gen_ai: "生成式人工智能 (GenAI)",
gen_ai_desc: "提供对 GenAI 供应商及模型调用的性能指标、用量和成本的全面监控。",
virtual_gen_ai: "虚拟 GenAI",
virtual_gen_ai_desc: "由语言探针通过拦截 AI SDK 调用,自动推导出的虚拟 GenAI 逻辑服务与模型视图。",
envoy_ai_gateway: "Envoy AI 网关",
envoy_ai_gateway_desc: "通过 OpenTelemetry OTLP 指标和访问日志提供 Envoy AI 网关监控。",
};
export default titles;

View File

@@ -216,6 +216,7 @@ const msg = {
timeRange: "时间范围",
duration: "持续时间",
startTime: "开始时间",
recoveryTime: "恢复时间",
start: "起始点",
spans: "跨度",
spanInfo: "跨度信息",
@@ -324,6 +325,7 @@ const msg = {
message: "信息",
tooltipsContent: "提示内容",
alarmDetail: "警告详情",
recoveredAt: "恢复于",
scope: "范围",
destService: "终点服务",
destServiceInstance: "终点实例",
@@ -391,6 +393,7 @@ const msg = {
errorInstances: "错误的实例",
successInstances: "成功的实例",
profilingEvents: "异步分析事件",
pprofEvent: "PProf 事件",
execArgs: "String任务扩展",
instances: "实例",
snapshot: "快照",
@@ -403,7 +406,14 @@ const msg = {
maxDuration: "最大时长",
minutes: "分钟",
invalidProfilingDurationRange: "请输入1到900秒之间的有效时长",
invalidPprofDuration: "请输入有效的分钟数",
invalidPprofDumpPeriod: "请输入有效的采样率",
pprofDumpPeriod: "采样率",
pprofDurationHint: "CPU、BLOCK 和 MUTEX 任务必须设置采样时长。",
pprofDumpPeriodBlockHint: "BLOCK 任务必须设置采样率,单位是纳秒。设置为 1 表示采集所有阻塞事件。",
pprofDumpPeriodMutexHint: "MUTEX 任务必须设置采样率,单位是竞争次数。设置为 1 表示采集所有互斥竞争事件。",
taskCreatedSuccessfully: "任务创建成功",
taskCreationFailed: "任务创建失败",
runQuery: "运行查询",
spansTable: "Spans表格",
download: "下载",

View File

@@ -124,16 +124,6 @@ describe("Router Guards", () => {
expect(mockNext).toHaveBeenCalledWith();
});
it("should redirect to NotFound for routes with invalid parameters", () => {
const validationGuard = createValidationGuard();
const to = { path: "/invalid", params: { id: "", name: null } };
const from = { path: "/some-path" };
validationGuard(to, from, mockNext);
expect(mockNext).toHaveBeenCalledWith({ name: "NotFound" });
});
it("should redirect to NotFound for routes with undefined parameters", () => {
const validationGuard = createValidationGuard();
const to = { path: "/invalid", params: { id: undefined } };
@@ -144,14 +134,14 @@ describe("Router Guards", () => {
expect(mockNext).toHaveBeenCalledWith({ name: "NotFound" });
});
it("should handle mixed valid and invalid parameters", () => {
it("should allow empty or null parameters (only undefined is invalid)", () => {
const validationGuard = createValidationGuard();
const to = { path: "/mixed", params: { id: "123", name: "" } };
const to = { path: "/mixed", params: { id: "", name: null } };
const from = { path: "/some-path" };
validationGuard(to, from, mockNext);
expect(mockNext).toHaveBeenCalledWith({ name: "NotFound" });
expect(mockNext).toHaveBeenCalledWith();
});
});

View File

@@ -55,9 +55,7 @@ export function createValidationGuard() {
// Validate route parameters if needed
if (to.params && Object.keys(to.params).length > 0) {
// Add custom validation logic here
const hasValidParams = Object.values(to.params).every(
(param) => param !== undefined && param !== null && param !== "",
);
const hasValidParams = Object.values(to.params).every((param) => param !== undefined);
if (!hasValidParams) {
next({ name: "NotFound" });

View File

@@ -51,6 +51,7 @@ export const ControlsTypes = [
WidgetType.Ebpf,
WidgetType.NetworkProfiling,
WidgetType.AsyncProfiling,
WidgetType.Pprof,
WidgetType.ThirdPartyApp,
WidgetType.ContinuousProfiling,
WidgetType.TaskTimeline,

View File

@@ -82,6 +82,7 @@ describe("App Store", () => {
expect(store.durationRow.start).toBeInstanceOf(Date);
expect(store.durationRow.end).toBeInstanceOf(Date);
expect(store.durationRow.step).toBe(TimeType.MINUTE_TIME);
expect(store.durationRow.coldStage).toBe(false);
});
});
@@ -94,6 +95,7 @@ describe("App Store", () => {
expect(duration.start).toBeInstanceOf(Date);
expect(duration.end).toBeInstanceOf(Date);
expect(duration.step).toBe(TimeType.MINUTE_TIME);
expect(duration.coldStage).toBe(false);
});
it("should return correct duration time", () => {
@@ -104,6 +106,7 @@ describe("App Store", () => {
expect(durationTime.start).toBe("2023-01-01 12:00");
expect(durationTime.end).toBe("2023-01-01 12:00");
expect(durationTime.step).toBe(TimeType.MINUTE_TIME);
expect(durationTime.coldStage).toBe(false);
});
it("should calculate interval unix correctly for MINUTE", () => {
@@ -156,7 +159,7 @@ describe("App Store", () => {
store.setDuration(newDuration);
expect(store.durationRow).toEqual(newDuration);
expect(store.durationRow).toEqual({ ...newDuration, coldStage: false });
});
it("should update duration row correctly", () => {
@@ -169,7 +172,7 @@ describe("App Store", () => {
store.updateDurationRow(newDuration);
expect(store.durationRow).toEqual(newDuration);
expect(store.durationRow).toEqual({ ...newDuration, coldStage: false });
});
it("should set max range correctly", () => {
@@ -231,6 +234,55 @@ describe("App Store", () => {
expect(store.coldStageMode).toBe(true);
});
it("should set duration with coldStage when coldStageMode is enabled", () => {
const store = appStore();
store.setColdStageMode(true);
const newDuration = {
start: new Date("2023-01-01"),
end: new Date("2023-01-02"),
step: "HOUR",
};
store.setDuration(newDuration);
expect(store.durationRow).toEqual({ ...newDuration, coldStage: true });
expect(store.duration.coldStage).toBe(true);
});
it("should update duration row with coldStage when coldStageMode is enabled", () => {
const store = appStore();
store.setColdStageMode(true);
const newDuration = {
start: new Date("2023-02-01"),
end: new Date("2023-02-02"),
step: "DAY",
};
store.updateDurationRow(newDuration);
expect(store.durationRow).toEqual({ ...newDuration, coldStage: true });
expect(store.duration.coldStage).toBe(true);
});
it("should return correct duration time with coldStage when coldStageMode is enabled", () => {
const store = appStore();
store.setColdStageMode(true);
// Need to update duration row after setting cold stage mode
const newDuration = {
start: new Date("2023-01-01"),
end: new Date("2023-01-02"),
step: "HOUR",
};
store.setDuration(newDuration);
const durationTime = store.durationTime;
expect(durationTime.coldStage).toBe(true);
});
it("should set reload timer correctly", () => {
const store = appStore();
const mockTimer = setInterval(() => {

View File

@@ -18,7 +18,9 @@ import { defineStore } from "pinia";
import { store } from "@/store";
import graphql from "@/graphql";
import type { Alarm } from "@/types/alarm";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useDuration } from "@/hooks/useDuration";
const { getDurationTime } = useDuration();
interface AlarmState {
loading: boolean;
@@ -48,10 +50,14 @@ export const alarmStore = defineStore({
return res.data;
},
async getAlarmTagKeys() {
return await graphql.query("queryAlarmTagKeys").params({ duration: useAppStoreWithOut().durationTime });
return await graphql
.query("queryAlarmTagKeys")
.params({ duration: { ...getDurationTime(), coldStage: undefined } });
},
async getAlarmTagValues(tagKey: string) {
return await graphql.query("queryAlarmTagValues").params({ tagKey, duration: useAppStoreWithOut().durationTime });
return await graphql
.query("queryAlarmTagValues")
.params({ tagKey, duration: { ...getDurationTime(), coldStage: undefined } });
},
},
});

View File

@@ -46,6 +46,7 @@ export const InitializationDurationRow = {
start: new Date(new Date().getTime() - 1800000),
end: new Date(),
step: TimeType.MINUTE_TIME,
coldStage: false,
};
export const appStore = defineStore({
@@ -62,7 +63,7 @@ export const appStore = defineStore({
reloadTimer: null,
allMenus: [],
theme: Themes.Dark,
coldStageMode: false,
coldStageMode: InitializationDurationRow.coldStage || false,
maxRange: [],
metricsTTL: null,
recordsTTL: null,
@@ -73,6 +74,7 @@ export const appStore = defineStore({
start: getLocalTime(this.utc, this.durationRow.start),
end: getLocalTime(this.utc, this.durationRow.end),
step: this.durationRow.step,
coldStage: this.durationRow.coldStage,
};
},
durationTime(): DurationTime {
@@ -80,6 +82,7 @@ export const appStore = defineStore({
start: dateFormatStep(this.duration.start, this.duration.step, true),
end: dateFormatStep(this.duration.end, this.duration.step, true),
step: this.duration.step,
coldStage: this.duration.coldStage,
};
},
intervalUnix(): number[] {
@@ -124,10 +127,10 @@ export const appStore = defineStore({
},
actions: {
setDuration(data: Duration): void {
this.durationRow = data;
this.durationRow = { ...data, coldStage: this.coldStageMode };
},
updateDurationRow(data: Duration) {
this.durationRow = data;
this.durationRow = { ...data, coldStage: this.coldStageMode };
},
setMaxRange(times: Date[]) {
this.maxRange = times;

View File

@@ -37,6 +37,8 @@ interface LogState {
}
const { getDurationTime } = useDuration();
export const PageSizeDefault = 21;
export const logStore = defineStore({
id: "log",
state: (): LogState => ({
@@ -45,7 +47,7 @@ export const logStore = defineStore({
endpoints: [{ value: "0", label: "All" }],
conditions: {
queryDuration: getDurationTime(),
paging: { pageNum: 1, pageSize: 15 },
paging: { pageNum: 1, pageSize: PageSizeDefault },
},
supportQueryLogsByKeywords: true,
selectorStore: useSelectorStore(),
@@ -61,7 +63,7 @@ export const logStore = defineStore({
this.logs = [];
this.conditions = {
queryDuration: getDurationTime(),
paging: { pageNum: 1, pageSize: 15 },
paging: { pageNum: 1, pageSize: PageSizeDefault },
};
},
setLogHeaderType(type: string) {
@@ -150,10 +152,14 @@ export const logStore = defineStore({
return response;
},
async getLogTagKeys() {
return await graphql.query("queryLogTagKeys").params({ duration: useAppStoreWithOut().durationTime });
return await graphql
.query("queryLogTagKeys")
.params({ duration: { ...getDurationTime(), coldStage: undefined } });
},
async getLogTagValues(tagKey: string) {
return await graphql.query("queryLogTagValues").params({ tagKey, duration: useAppStoreWithOut().durationTime });
return await graphql
.query("queryLogTagValues")
.params({ tagKey, duration: { ...getDurationTime(), coldStage: undefined } });
},
},
});

138
src/store/modules/pprof.ts Normal file
View File

@@ -0,0 +1,138 @@
/**
* 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 type { PprofTask, PprofTaskCreationRequest, PprofStackElement, PprofTaskProgress } from "@/types/pprof";
import { store } from "@/store";
import graphql from "@/graphql";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import type { Instance } from "@/types/selector";
interface PprofState {
taskList: Array<PprofTask>;
selectedTask: Nullable<PprofTask>;
taskProgress: Nullable<PprofTaskProgress>;
instances: Instance[];
analyzeTrees: PprofStackElement[];
loadingTree: boolean;
loadingTasks: boolean;
}
export const pprofStore = defineStore({
id: "pprof",
state: (): PprofState => ({
taskList: [],
selectedTask: null,
taskProgress: null,
instances: [],
analyzeTrees: [],
loadingTree: false,
loadingTasks: false,
}),
actions: {
setSelectedTask(task: Nullable<PprofTask>) {
this.selectedTask = task || {};
},
setAnalyzeTrees(tree: PprofStackElement[]) {
this.analyzeTrees = tree;
},
async getTaskList() {
const selectorStore = useSelectorStore();
if (!selectorStore.currentService?.id) {
return;
}
this.loadingTasks = true;
const response = await graphql.query("getPprofTaskList").params({
request: {
serviceId: selectorStore.currentService?.id,
limit: 10000,
},
});
this.loadingTasks = false;
if (response.errors) {
return response;
}
this.taskList = response.data.pprofTaskList.tasks || [];
this.selectedTask = this.taskList[0] || {};
this.setAnalyzeTrees([]);
this.setSelectedTask(this.selectedTask);
if (!this.taskList.length) {
return response;
}
return response;
},
async getTaskLogs(param: { taskId: string }) {
const response = await graphql.query("getPprofTaskProcess").params(param);
if (response.errors) {
return response;
}
this.taskProgress = response.data.taskProgress;
return response;
},
async getServiceInstances(param: { serviceId: string; isRelation?: boolean }) {
if (!param.serviceId) {
return null;
}
const response = await graphql.query("queryInstances").params({
serviceId: param.serviceId,
duration: useAppStoreWithOut().durationTime,
});
if (!response.errors) {
this.instances = (response.data.pods || []).map((d: Instance) => {
d.value = d.id || "";
return d;
});
}
return response;
},
async createTask(param: PprofTaskCreationRequest) {
if (!param.serviceId) {
return;
}
const response = await graphql.query("savePprofTask").params({ pprofTaskCreationRequest: param });
if (response.errors) {
return response;
}
this.getTaskList();
return response;
},
async getPprofAnalyze(params: { taskId: string; instanceIds: Array<string> }) {
if (!params.instanceIds.length) {
this.analyzeTrees = [];
return new Promise((resolve) => resolve({}));
}
this.loadingTree = true;
const response = await graphql.query("getPprofAnalyze").params({ request: params });
this.loadingTree = false;
if (response.errors) {
this.analyzeTrees = [];
return response;
}
const { analysisResult } = response.data;
if (!analysisResult) {
this.analyzeTrees = [];
return response;
}
this.analyzeTrees = [analysisResult.tree];
return response;
},
},
});
export function usePprofStore() {
return pprofStore(store);
}

View File

@@ -177,10 +177,10 @@ export const traceStore = defineStore({
return response;
},
async getTraces() {
this.loading = true;
if (this.hasQueryTracesV2Support) {
return this.fetchV2Traces();
}
this.loading = true;
const response = await graphql.query("queryTraces").params({ condition: this.conditions });
if (response.errors) {
this.loading = false;
@@ -193,14 +193,15 @@ export const traceStore = defineStore({
this.loading = false;
return response;
}
this.getTraceSpans({ traceId: response.data.data.traces[0].traceIds[0] });
this.traceList = response.data.data.traces.map((d: Trace) => {
d.traceIds = d.traceIds.map((id: string) => {
return { value: id, label: id };
});
return d;
});
this.setCurrentTrace(response.data.data.traces[0] || {});
const currentTrace = this.traceList[0] || {};
this.setCurrentTrace(currentTrace);
await this.getTraceSpans({ traceId: currentTrace.traceIds?.[0]?.value || "" });
return response;
},
async getTraceSpans(params: { traceId: string }) {
@@ -237,10 +238,14 @@ export const traceStore = defineStore({
return response;
},
async getTagKeys() {
return await graphql.query("queryTraceTagKeys").params({ duration: useAppStoreWithOut().durationTime });
return await graphql
.query("queryTraceTagKeys")
.params({ duration: { ...getDurationTime(), coldStage: undefined } });
},
async getTagValues(tagKey: string) {
return await graphql.query("queryTraceTagValues").params({ tagKey, duration: useAppStoreWithOut().durationTime });
return await graphql
.query("queryTraceTagValues")
.params({ tagKey, duration: { ...getDurationTime(), coldStage: undefined } });
},
async getHasQueryTracesV2Support() {
const response = await graphql.query("queryHasQueryTracesV2Support").params({});
@@ -297,6 +302,27 @@ export const traceStore = defineStore({
this.setCurrentTrace(trace || {});
return response;
},
async getTraceByTraceId(traceId: string) {
this.loading = true;
this.setTraceCondition({
traceId: traceId,
});
await this.getHasQueryTracesV2Support();
if (this.hasQueryTracesV2Support) {
await this.fetchV2Traces();
this.loading = false;
return;
}
this.setCurrentTrace({
traceIds: [{ value: traceId, label: traceId }],
traceId: traceId,
endpointNames: [],
spans: [],
});
await this.getTraceSpans({ traceId });
this.loading = false;
},
},
});

View File

@@ -15,135 +15,308 @@
* limitations under the License.
*/
@use "element-plus/theme-chalk/src/dark/css-vars.scss" as *;
/* ============================================
ANIMATIONS
============================================ */
@keyframes topo-dash {
from {
stroke-dashoffset: 10;
}
to {
stroke-dashoffset: 0;
}
}
@keyframes theme-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* ============================================
BASE THEME TOKENS
============================================ */
:root {
/* Brand Colors */
--sw-green: #70c877;
--sw-orange: #e6a23c;
--sw-red: #e66;
--sw-blue-primary: #409eff;
/* Animation */
--sw-topo-animation: topo-dash 0.3s linear infinite;
/* Timing Functions */
--sw-ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
--sw-ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* Transitions */
--sw-transition-fast: 150ms var(--sw-ease-smooth);
--sw-transition-base: 250ms var(--sw-ease-smooth);
--sw-transition-slow: 350ms var(--sw-ease-smooth);
/* Shadows */
--sw-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--sw-shadow-base: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--sw-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--sw-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
/* ============================================
LIGHT THEME
============================================ */
html {
--el-color-primary: #409eff;
color-scheme: light;
/* === Element Plus Integration === */
--el-color-primary: var(--sw-blue-primary);
--el-color-info-light-9: #666;
--theme-background: #fff;
--font-color: #3d444f;
--disabled-color: #ccc;
--dashboard-tool-bg: rgb(240 242 245);
--text-color-placeholder: #666;
--border-color: #dcdfe6;
--border-color-primary: #eee;
--layout-background: #f7f9fa;
--box-shadow-color: #ccc;
--sw-bg-color-overlay: #fff;
--sw-border-color-light: #e4e7ed;
--popper-hover-bg: #eee;
--sw-icon-btn-bg: #eee;
--sw-icon-btn-color: #666;
--sw-icon-btn-border: #ccc;
--sw-table-col: #fff;
--sw-config-header: aliceblue;
--sw-topology-color: #666;
--vis-tooltip-bg: #fff;
--sw-topology-switch-icon: rgba(0, 0, 0, 0.3);
--sw-topology-box-shadow: #eee 1px 2px 10px;
--sw-topology-setting-bg: #fff;
--sw-topology-border: 1px solid #999;
--sw-trace-success: rgb(46 47 51 / 10%);
--sw-trace-list-border: rgb(0 0 0 / 10%);
--sw-list-selected: #ededed;
--sw-table-header: #f3f4f9;
--sw-list-hover: rgb(0 0 0 / 4%);
--sw-setting-color: #606266;
--sw-alarm-tool: #f0f2f5;
--sw-alarm-tool-border: #c1c5ca41;
--sw-table-color: #000;
--sw-event-vis-selected: #1a1a1a;
--sw-time-axis-text: #4d4d4d;
--sw-drawer-header: #72767b;
--sw-marketplace-border: #dedfe0;
--sw-grid-item-active: #d4d7de;
--sw-trace-line: #999;
--sw-scrollbar-track: #eee;
--sw-scrollbar-thumb: #aaa;
--sw-font-grey-color: #a7aebb;
--sw-trace-list-path: rgba(0, 0, 0, 0.1);
--sw-trace-table-selected: rgba(0, 0, 0, 0.1);
--el-fill-color-blank: transparent;
/* === Foundation === */
--theme-background: hsl(0, 0%, 100%);
--layout-background: hsl(210, 20%, 98%);
/* === Typography === */
--font-color: hsl(220, 13%, 28%);
--font-color-secondary: hsl(220, 9%, 46%);
--font-grey-color: hsl(220, 12%, 68%);
--text-color-placeholder: hsl(0, 0%, 40%);
--disabled-color: hsl(0, 0%, 80%);
/* === Surfaces & Overlays === */
--sw-bg-color-overlay: hsl(0, 0%, 100%);
--sw-config-header: hsl(208, 100%, 97%);
--dashboard-tool-bg: hsl(220, 14%, 96%);
--sw-alarm-tool: hsl(220, 14%, 94%);
--sw-table-header: hsl(225, 25%, 97%);
/* === Borders === */
--border-color: hsl(214, 15%, 91%);
--border-color-primary: hsl(0, 0%, 93%);
--sw-border-color-light: hsl(214, 18%, 91%);
--sw-alarm-tool-border: hsl(220 13% 80% / 0.25);
--sw-marketplace-border: hsl(220, 5%, 87%);
/* === Interactive States === */
--sw-list-hover: hsl(0 0% 0% / 0.04);
--sw-list-selected: hsl(0, 0%, 93%);
--popper-hover-bg: hsl(0, 0%, 93%);
--sw-grid-item-active: hsl(222, 13%, 85%);
/* === Buttons & Icons === */
--sw-icon-btn-bg: hsl(0, 0%, 93%);
--sw-icon-btn-color: hsl(0, 0%, 40%);
--sw-icon-btn-border: hsl(0, 0%, 80%);
/* === Tables === */
--sw-table-col: hsl(0, 0%, 100%);
--sw-table-color: hsl(0, 0%, 0%);
--sw-setting-color: hsl(220, 18%, 38%);
/* === Topology === */
--sw-topology-color: hsl(0, 0%, 40%);
--sw-topology-switch-icon: hsl(0 0% 0% / 0.3);
--sw-topology-box-shadow: var(--sw-shadow-lg);
--sw-topology-setting-bg: hsl(0, 0%, 100%);
--sw-topology-border: 1px solid hsl(0, 0%, 60%);
/* === Trace & Events === */
--sw-trace-success: hsl(220 13% 18% / 0.1);
--sw-trace-list-border: hsl(0 0% 0% / 0.1);
--sw-trace-list-path: hsl(0 0% 0% / 0.1);
--sw-trace-table-selected: hsl(0 0% 0% / 0.1);
--sw-trace-line: hsl(0, 0%, 60%);
--sw-event-vis-selected: hsl(0, 0%, 10%);
--sw-time-axis-text: hsl(0, 0%, 30%);
/* === Tooltips & Popovers === */
--vis-tooltip-bg: hsl(0, 0%, 100%);
/* === Drawer & Modal === */
--sw-drawer-header: hsl(220, 9%, 46%);
/* === Scrollbars === */
--sw-scrollbar-track: hsl(0, 0%, 93%);
--sw-scrollbar-thumb: hsl(0, 0%, 67%);
/* === Shadows === */
--box-shadow-color: hsl(0, 0%, 80%);
}
/* ============================================
DARK THEME
============================================ */
html.dark {
--el-color-primary: #409eff;
--el-color-info-light-9: #333;
--theme-background: #212224;
--font-color: #fafbfc;
--disabled-color: #999;
--dashboard-tool-bg: #000;
--text-color-placeholder: #ccc;
--border-color: #333;
--border-color-primary: #4b4b52;
--layout-background: #000;
--box-shadow-color: #606266;
--sw-bg-color-overlay: #1d1e1f;
--sw-border-color-light: #414243;
--popper-hover-bg: rgb(64, 158, 255, 0.1);
--sw-icon-btn-bg: #222;
--sw-icon-btn-color: #ccc;
--sw-icon-btn-border: #999;
color-scheme: dark;
/* === Element Plus Integration === */
--el-color-primary: var(--sw-blue-primary);
--el-color-info-light-9: hsl(0, 0%, 20%);
--el-fill-color-blank: transparent;
/* === Foundation === */
--theme-background: hsl(220, 6%, 13%);
--layout-background: hsl(0, 0%, 0%);
/* === Typography === */
--font-color: hsl(210, 17%, 98%);
--font-color-secondary: hsl(220, 9%, 78%);
--font-grey-color: hsl(220, 12%, 68%);
--text-color-placeholder: hsl(0, 0%, 80%);
--disabled-color: hsl(0, 0%, 60%);
/* === Surfaces & Overlays === */
--sw-bg-color-overlay: hsl(220, 7%, 12%);
--sw-config-header: hsl(210, 6%, 19%);
--dashboard-tool-bg: hsl(0, 0%, 0%);
--sw-alarm-tool: hsl(210, 6%, 19%);
--sw-table-header: hsl(210, 6%, 19%);
/* === Borders === */
--border-color: hsl(0, 0%, 20%);
--border-color-primary: hsl(225, 5%, 30%);
--sw-border-color-light: hsl(214, 5%, 26%);
--sw-alarm-tool-border: hsl(0, 0%, 27%);
--sw-marketplace-border: hsl(220, 7%, 38%);
/* === Interactive States === */
--sw-list-hover: hsl(220, 6%, 15%);
--sw-list-selected: hsl(220, 13%, 28%);
--popper-hover-bg: hsl(207 100% 62% / 0.1);
--sw-grid-item-active: hsl(220, 5%, 46%);
/* === Buttons & Icons === */
--sw-icon-btn-bg: hsl(0, 0%, 13%);
--sw-icon-btn-color: hsl(0, 0%, 80%);
--sw-icon-btn-border: hsl(0, 0%, 60%);
/* === Tables === */
--sw-table-col: none;
--sw-config-header: #303133;
--sw-topology-color: #ccc;
--vis-tooltip-bg: #414243;
--sw-topology-switch-icon: #999;
--sw-topology-box-shadow: 0 0 2px 0 #444;
--sw-topology-setting-bg: #333;
--sw-topology-border: 1px solid #666;
--sw-trace-success: #aaa;
--sw-trace-list-border: #333133;
--sw-list-hover: #262629;
--sw-table-header: #303133;
--sw-list-selected: #3d444f;
--sw-setting-color: #eee;
--sw-alarm-tool: #303133;
--sw-alarm-tool-border: #444;
--sw-table-color: #fff;
--sw-event-vis-selected: #fff;
--sw-time-axis-text: #aaa;
--sw-drawer-header: #e9e9eb;
--sw-marketplace-border: #606266;
--sw-grid-item-active: #73767a;
--sw-trace-line: #e8e8e8;
--sw-scrollbar-track: #252a2f;
--sw-scrollbar-thumb: #888;
--sw-font-grey-color: #a7aebb;
--sw-trace-list-path: rgba(244, 244, 244, 0.4);
--sw-trace-table-selected: rgba(255, 255, 255, 0.1);
--sw-table-color: hsl(0, 0%, 100%);
--sw-setting-color: hsl(0, 0%, 93%);
/* === Topology === */
--sw-topology-color: hsl(0, 0%, 80%);
--sw-topology-switch-icon: hsl(0, 0%, 60%);
--sw-topology-box-shadow: 0 0 2px 0 hsl(0, 0%, 27%);
--sw-topology-setting-bg: hsl(0, 0%, 20%);
--sw-topology-border: 1px solid hsl(0, 0%, 40%);
/* === Trace & Events === */
--sw-trace-success: hsl(0, 0%, 67%);
--sw-trace-list-border: hsl(220, 3%, 20%);
--sw-trace-list-path: hsl(0 0% 96% / 0.4);
--sw-trace-table-selected: hsl(0 0% 100% / 0.1);
--sw-trace-line: hsl(0, 0%, 91%);
--sw-event-vis-selected: hsl(0, 0%, 100%);
--sw-time-axis-text: hsl(0, 0%, 67%);
/* === Tooltips & Popovers === */
--vis-tooltip-bg: hsl(214, 5%, 26%);
/* === Drawer & Modal === */
--sw-drawer-header: hsl(240, 5%, 91%);
/* === Scrollbars === */
--sw-scrollbar-track: hsl(210, 11%, 15%);
--sw-scrollbar-thumb: hsl(0, 0%, 53%);
/* === Shadows === */
--box-shadow-color: hsl(220, 7%, 38%);
}
/* ============================================
ELEMENT PLUS OVERRIDES
============================================ */
/* === Drawer === */
.el-drawer__header {
margin-bottom: 0;
padding-left: 10px;
font-size: 16px;
color: var(--sw-drawer-header);
}
.el-drawer__body {
padding: 0;
}
/* === Table === */
.el-table {
--el-table-tr-bg-color: var(--theme-background);
--el-table-header-bg-color: var(--theme-background);
}
/* === Popper === */
.el-popper.is-light {
background: var(--sw-bg-color-overlay);
border: 1px solid var(--sw-border-color-light);
box-shadow: var(--sw-shadow-md);
}
/* === Switch === */
.el-switch {
--el-switch-off-color: #aaa;
--el-switch-off-color: var(--disabled-color);
}
/* === Menu === */
.el-menu {
--el-menu-item-height: 50px;
}
.el-menu-item-group__title {
display: none;
}
.el-sub-menu {
.el-menu-item {
height: 40px;
line-height: 40px;
padding: 0 0 0 56px !important;
}
}
.el-sub-menu__title {
.el-icon.menu-icons {
margin-top: -5px !important;
}
}
/* === Input === */
.el-input-number .el-input__inner {
text-align: left !important;
}
.el-input--small .el-input__inner {
--el-input-inner-height: calc(var(--el-input-height, 24px));
}
/* === Loading === */
.el-loading-mask {
background-color: var(--theme-background);
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
.el-loading-mask {
backdrop-filter: blur(1px);
-webkit-backdrop-filter: blur(1px);
}
}
@media (prefers-reduced-transparency: reduce), (prefers-reduced-motion: reduce) {
.el-loading-mask {
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
}
/* ============================================
SCSS VARIABLES (Legacy Support)
============================================ */
$tool-icon-btn-bg: var(--sw-icon-btn-bg);
$tool-icon-btn-color: var(--sw-icon-btn-color);
$popper-hover-bg-color: var(--popper-hover-bg);
@@ -160,90 +333,26 @@ $theme-background: var(--theme-background);
$active-background: var(--el-color-primary);
$font-size-smaller: 12px;
$font-size-normal: 14px;
$error-color: #e66;
$error-color: var(--sw-red);
/* ============================================
COMPONENT STYLES
============================================ */
/* === Interactive Elements === */
.opt {
transition: background-color var(--sw-transition-fast);
}
.opt:hover {
background-color: var(--sw-list-hover) !important;
}
.el-loading-mask {
background-color: var(--theme-background);
}
.el-menu {
--el-menu-item-height: 50px;
}
.el-menu-item-group__title {
display: none;
}
.el-sub-menu .el-menu-item {
height: 40px;
line-height: 40px;
padding: 0 0 0 56px !important;
}
.el-sub-menu__title {
.el-icon.menu-icons {
margin-top: -5px !important;
}
}
.el-drawer__header {
margin-bottom: 0;
padding-left: 10px;
font-size: 16px;
}
.el-drawer__body {
padding: 0;
}
.switch {
margin: 0 5px;
}
div.vis-tooltip {
max-width: 600px;
overflow: hidden;
background-color: var(--vis-tooltip-bg) !important;
white-space: normal !important;
font-size: $font-size-smaller !important;
color: var(--font-color) !important;
}
.vis-item {
cursor: pointer;
height: 20px;
}
.vis-item.Error {
background-color: $error-color;
opacity: 0.8;
border-color: $error-color;
color: var(--sw-event-vis-selected) !important;
}
.vis-item.Normal {
background-color: #fac858;
border-color: #fac858;
color: var(--sw-event-vis-selected) !important;
}
.vis-item .vis-item-content {
padding: 0 3px !important;
}
.vis-item.vis-selected.Error,
.vis-item.vis-selected.Normal {
color: var(--sw-event-vis-selected) !important;
}
.vis-time-axis .vis-text {
color: var(--sw-time-axis-text) !important;
}
/* === Menu Visibility === */
.el-menu--vertical.sub-list {
display: none;
}
@@ -252,34 +361,149 @@ div:has(> a.menu-title) {
display: none;
}
.el-input-number .el-input__inner {
text-align: left !important;
/* ============================================
VIS.JS TIMELINE CUSTOMIZATION
============================================ */
/* === Tooltip === */
div.vis-tooltip {
max-width: 600px;
overflow: hidden;
background-color: var(--vis-tooltip-bg) !important;
border: 1px solid var(--border-color) !important;
box-shadow: var(--sw-shadow-md) !important;
white-space: normal !important;
font-size: $font-size-smaller !important;
color: var(--font-color) !important;
border-radius: 6px;
padding: 8px 12px;
}
.el-input--small .el-input__inner {
--el-input-inner-height: calc(var(--el-input-height, 24px));
/* === Timeline Items === */
.vis-item {
cursor: pointer;
height: 20px;
border-radius: 4px;
transition: transform var(--sw-transition-fast), box-shadow var(--sw-transition-fast);
&:hover {
transform: translateY(-1px);
box-shadow: var(--sw-shadow-sm);
}
&.Error {
background-color: $error-color;
opacity: 0.8;
border-color: $error-color;
color: var(--sw-event-vis-selected) !important;
}
&.Normal {
background-color: #fac858;
border-color: #fac858;
color: var(--sw-event-vis-selected) !important;
}
.vis-item-content {
padding: 0 3px !important;
}
&.vis-selected {
&.Error,
&.Normal {
color: var(--sw-event-vis-selected) !important;
box-shadow: var(--sw-shadow-base);
}
}
}
/* === Time Axis === */
.vis-time-axis .vis-text {
color: var(--sw-time-axis-text) !important;
font-size: $font-size-smaller;
}
/* ============================================
VIEW TRANSITIONS
============================================ */
html {
/* Smooth theme switching with fade */
&::view-transition-old(root),
&::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
/* Light to Dark: Fade out light, fade in dark */
&.dark {
&::view-transition-old(root) {
z-index: 1;
animation: theme-fade-out 0.35s var(--sw-ease-smooth);
}
&::view-transition-new(root) {
z-index: 999;
animation: theme-fade-in 0.35s var(--sw-ease-smooth);
}
}
/* Dark to Light: Fade out dark, fade in light */
&::view-transition-old(root) {
z-index: 999;
animation: theme-fade-out 0.35s var(--sw-ease-smooth);
}
&::view-transition-new(root) {
z-index: 1;
animation: theme-fade-in 0.35s var(--sw-ease-smooth);
}
}
@keyframes theme-fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
/* ============================================
ACCESSIBILITY: REDUCED MOTION
============================================ */
@media (prefers-reduced-motion: reduce) {
/* Disable view transition animations */
html {
&::view-transition-old(root),
&::view-transition-new(root) {
animation: none !important;
}
&.dark {
&::view-transition-old(root),
&::view-transition-new(root) {
animation: none !important;
}
}
}
/* Disable component animations */
.vis-item {
transition: none !important;
&:hover {
transform: none !important;
}
}
.opt:hover {
transition: none !important;
}
/* Disable all keyframe animations */
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

View File

@@ -36,25 +36,29 @@ Object.defineProperty(window, "matchMedia", {
});
// Mock ResizeObserver
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
globalThis.ResizeObserver = class ResizeObserver {
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
};
// Mock IntersectionObserver
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
globalThis.IntersectionObserver = class IntersectionObserver {
root = null;
rootMargin = "";
thresholds = [];
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
takeRecords = vi.fn(() => []);
} as any;
// Mock requestAnimationFrame
global.requestAnimationFrame = vi.fn((cb: FrameRequestCallback) => {
globalThis.requestAnimationFrame = vi.fn((cb: FrameRequestCallback) => {
const id = setTimeout(cb, 0);
return id as unknown as number;
});
global.cancelAnimationFrame = vi.fn();
globalThis.cancelAnimationFrame = vi.fn();
// Configure Vue Test Utils
config.global.plugins = [ElementPlus];

View File

@@ -24,6 +24,7 @@ export interface Alarm {
message: string;
key: string;
startTime: string;
recoveryTime: string;
scope: string;
tags: Array<{ key: string; value: string }>;
events: Event[];

View File

@@ -22,11 +22,13 @@ export interface Duration {
start: Date;
end: Date;
step: string;
coldStage?: boolean;
}
export interface DurationTime {
start: string;
end: string;
step: string;
coldStage?: boolean;
}
export type Paging = {
pageNum: number;

72
src/types/pprof.ts Normal file
View File

@@ -0,0 +1,72 @@
/**
* 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 type PprofTask = {
id: string;
serviceId: string;
serviceInstanceIds: string[];
createTime: number;
events: string;
duration?: number;
dumpPeriod?: number;
logs?: PprofTaskLog[];
errorInstanceIds?: string[];
successInstanceIds?: string[];
};
export type PprofTaskCreationRequest = {
serviceId: string;
serviceInstanceIds: string[];
events: string;
duration?: number;
dumpPeriod?: number;
};
export type PprofStackElement = {
id: string;
parentId: string;
symbol: string;
dumpCount: number;
self: number;
elements?: PprofStackElement[];
};
export type PprofTaskProgress = {
errorInstanceIds: string[];
successInstanceIds: string[];
logs: PprofTaskLog[];
};
type PprofTaskLog = {
id: string;
instanceId: string;
instanceName: string;
operationType: string;
operationTime: number;
};
export type PprofFlameGraphNode = {
id: string;
originId: string;
name: string;
parentId: string;
symbol: string;
dumpCount: number;
self: number;
value: number;
children?: PprofFlameGraphNode[];
};

View File

@@ -22,3 +22,14 @@ export function treeForeach(tree: any, func: (node: any) => void) {
}
return tree;
}
export function escapeHtml(str: string): string {
const htmlEscapes: Record<string, string> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;",
};
return str.replace(/[&<>"']/g, (char) => htmlEscapes[char]);
}

View File

@@ -156,7 +156,6 @@ limitations under the License. -->
.link {
float: right;
margin-bottom: 20px;
}
.active {

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="timeline-table clear" v-loading="alarmStore.loading">
<div v-for="(i, index) in alarmStore.alarms" :key="index" class="clear timeline-item">
<div v-for="(i, index) in displayAlarms" :key="index" class="clear timeline-item">
<div class="g-sm-3 grey sm hide-xs time-line tr">
{{ dateFormat(parseInt(i.startTime)) }}
</div>
@@ -37,9 +37,12 @@ limitations under the License. -->
<div class="grey sm show-xs">
{{ dateFormat(parseInt(i.startTime)) }}
</div>
<div class="grey sm" v-if="i.recoveryTime">
{{ t("recoveredAt") }} {{ dateFormat(parseInt(i.recoveryTime)) }}
</div>
</div>
</div>
<div v-if="!alarmStore.alarms.length" class="tips">{{ t("noData") }}</div>
<div v-if="!displayAlarms.length" class="tips">{{ t("noData") }}</div>
</div>
<el-dialog
v-model="isShowDetails"
@@ -53,6 +56,9 @@ limitations under the License. -->
<span v-if="item.label === 'startTime'">
{{ dateFormat(currentDetail[item.label]) }}
</span>
<span v-else-if="item.label === 'recoveryTime'">
{{ currentDetail[item.label] ? dateFormat(currentDetail[item.label]) : "" }}
</span>
<span v-else-if="item.label === 'tags'">
<div v-for="(d, index) in alarmTags" :key="index">{{ d }}</div>
</span>
@@ -119,11 +125,11 @@ limitations under the License. -->
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import type { Alarm, Event } from "@/types/alarm";
import { useAlarmStore } from "@/store/modules/alarm";
import { EventsDetailHeaders, AlarmDetailCol, EventsDetailKeys } from "./data";
import { EventsDetailHeaders, AlarmDetailCol, EventsDetailKeys, PageSizeDefaultAlarm } from "./data";
import { dateFormat } from "@/utils/dateFormat";
import Snapshot from "./components/Snapshot.vue";
@@ -135,6 +141,11 @@ limitations under the License. -->
const alarmTags = ref<string[]>([]);
const currentEvents = ref<any[]>([]);
const currentEvent = ref<Event | any>({});
const pageSize = PageSizeDefaultAlarm;
const displayAlarms = computed(() =>
alarmStore.alarms.length >= pageSize ? alarmStore.alarms.slice(0, pageSize - 1) : alarmStore.alarms,
);
function showDetails(item: Alarm) {
isShowDetails.value = true;

View File

@@ -41,19 +41,11 @@ limitations under the License. -->
/>
</div>
<div class="pagination">
<el-pagination
v-model="pageNum"
:page-size="pageSize"
layout="prev, pager, next"
:total="total"
@current-change="changePage"
:pager-count="5"
size="small"
:style="
appStore.theme === Themes.Light
? `--el-pagination-bg-color: #f0f2f5; --el-pagination-button-disabled-bg-color: #f0f2f5;`
: ''
"
<Pagination
v-model:currentPage="pageNum"
:pageSize="pageSize"
:total="alarmStore.alarms.length"
@change="changePage"
/>
</div>
</div>
@@ -70,19 +62,19 @@ limitations under the License. -->
import { useI18n } from "vue-i18n";
import { ElMessage } from "element-plus";
import ConditionTags from "@/views/components/ConditionTags.vue";
import { AlarmOptions } from "./data";
import Pagination from "@/views/components/Pagination.vue";
import { AlarmOptions, PageSizeDefaultAlarm } from "./data";
import { useAppStoreWithOut, InitializationDurationRow } from "@/store/modules/app";
import { useAlarmStore } from "@/store/modules/alarm";
import { useDuration } from "@/hooks/useDuration";
import timeFormat from "@/utils/timeFormat";
import type { DurationTime, Duration } from "@/types/app";
import { Themes } from "@/constants/data";
/*global Indexable */
const appStore = useAppStoreWithOut();
const alarmStore = useAlarmStore();
const { t } = useI18n();
const { setDurationRow, getDurationTime, getMaxRange } = useDuration();
const pageSize = 20;
const pageSize = PageSizeDefaultAlarm;
const entity = ref<string>("");
const keyword = ref<string>("");
const pageNum = ref<number>(1);
@@ -90,9 +82,6 @@ limitations under the License. -->
const durationRow = ref<Duration>(InitializationDurationRow);
const tagsMap = ref<{ key: string; value: string }[]>();
const total = computed(() =>
alarmStore.alarms.length === pageSize ? pageSize * pageNum.value + 1 : pageSize * pageNum.value,
);
const maxRange = computed(() =>
getMaxRange(appStore.coldStageMode ? appStore.recordsTTL?.coldNormal || 0 : appStore.recordsTTL?.normal || 0),
);

View File

@@ -14,6 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const PageSizeDefaultAlarm = 21;
export const AlarmOptions = [
{ label: "All", value: "" },
{ label: "Service", value: "Service" },
@@ -40,6 +42,10 @@ export const AlarmDetailCol = [
label: "startTime",
value: "startTime",
},
{
label: "recoveryTime",
value: "recoveryTime",
},
{
label: "tags",
value: "tags",

View File

@@ -0,0 +1,102 @@
<!-- 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="pagination-container">
<el-pagination
v-model:current-page="currentPageModel"
:page-size="displayPageSize"
size="small"
layout="prev, pager, next"
:total="computedTotal"
:pager-count="pagerCount"
@current-change="handlePageChange"
:style="paginationStyle"
/>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
/*global defineProps, defineEmits*/
const props = defineProps({
currentPage: {
type: Number,
default: 1,
},
pageSize: {
type: Number,
required: true,
},
total: {
type: Number,
required: true,
},
pagerCount: {
type: Number,
default: 5,
},
align: {
type: String as () => "left" | "center" | "right",
default: "right",
},
});
const emits = defineEmits<{
(e: "update:currentPage", page: number): void;
(e: "change", page: number): void;
}>();
// The display page size is pageSize - 1 because we fetch pageSize items
// but the last item is only used to check if there are more pages
const displayPageSize = computed(() => props.pageSize - 1);
// Calculate total for pagination display based on fetched items
// If we fetched pageSize items, there might be more pages
const computedTotal = computed(() => {
if (props.total >= props.pageSize) {
return displayPageSize.value * props.currentPage + 1;
}
return displayPageSize.value * props.currentPage;
});
const currentPageModel = computed({
get: () => props.currentPage,
set: (val: number) => {
void val;
},
});
const paginationStyle = computed(() => {
const alignMap = {
left: "flex-start",
center: "center",
right: "flex-end",
};
return {
display: "flex",
justifyContent: alignMap[props.align],
};
});
function handlePageChange(page: number) {
emits("update:currentPage", page);
emits("change", page);
}
</script>
<style lang="scss" scoped>
.pagination-container {
margin: 5px 0;
}
</style>

View File

@@ -539,8 +539,8 @@ limitations under the License. -->
cancelButtonText: "Cancel",
inputValue: row.name,
})
.then(({ value }) => {
updateName(row, value);
.then(() => {
updateName(row);
})
.catch(() => {
ElMessage({
@@ -549,8 +549,8 @@ limitations under the License. -->
});
});
}
async function updateName(d: DashboardItem, value: string) {
if (new RegExp(/\s/).test(value)) {
async function updateName(d: DashboardItem) {
if (new RegExp(/\s/).test(d.name)) {
ElMessage.error("The name cannot contain spaces, carriage returns, etc");
return;
}
@@ -559,7 +559,7 @@ limitations under the License. -->
const c = {
...JSON.parse(layout).configuration,
...d,
name: value,
name: d.name,
};
delete c.id;
delete c.filters;
@@ -575,7 +575,7 @@ limitations under the License. -->
}
dashboardStore.setCurrentDashboard({
...d,
name: value,
name: d.name,
});
dashboards.value = dashboardStore.dashboards.map((item: DashboardItem) => {
if (dashboardStore.currentDashboard?.id === item.id) {

View File

@@ -14,10 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div style="padding: 20px">
<TraceContent v-if="traceStore.currentTrace" :trace="traceStore.currentTrace" />
<div style="text-align: center; padding: 20px" v-if="!traceStore.loading && !traceStore.currentTrace"
>No trace found</div
>
<div v-if="traceStore.traceSpans.length">
<TraceContent
v-if="traceStore.hasQueryTracesV2Support && traceStore.currentTrace"
:trace="traceStore.currentTrace"
/>
<SpanList v-if="!traceStore.hasQueryTracesV2Support" />
</div>
<div style="text-align: center; padding: 20px" v-else> No trace found </div>
</div>
</template>
@@ -26,17 +30,16 @@ limitations under the License. -->
import { computed, onMounted, provide } from "vue";
import { useTraceStore } from "@/store/modules/trace";
import TraceContent from "@/views/dashboard/related/trace/components/TraceQuery/TraceContent.vue";
import SpanList from "@/views/dashboard/related/trace/components/TraceList/SpanList.vue";
const route = useRoute();
const traceStore = useTraceStore();
const traceId = computed(() => route.params.traceId as string);
provide("options", {});
onMounted(() => {
if (traceId.value) {
traceStore.setTraceCondition({
traceId: traceId.value,
});
traceStore.fetchV2Traces();
if (!traceId.value) {
return;
}
traceStore.getTraceByTraceId(traceId.value);
});
</script>

View File

@@ -0,0 +1,86 @@
<!-- 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="profile-wrapper flex-v">
<el-popover placement="bottom" trigger="click" :width="100" v-if="dashboardStore.editMode">
<template #reference>
<span class="operation cp">
<Icon iconName="ellipsis_v" size="middle" />
</span>
</template>
<div class="tools" @click="removeWidget">
<span>{{ t("delete") }}</span>
</div>
</el-popover>
<Header />
<Content :config="props.data" />
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import Content from "../related/pprof/Content.vue";
import Header from "../related/pprof/Header.vue";
import type { LayoutConfig } from "@/types/dashboard";
/*global defineProps*/
const props = defineProps({
data: {
type: Object as PropType<LayoutConfig>,
default: () => ({ graph: {} }),
},
activeIndex: { type: String, default: "" },
});
const { t } = useI18n();
const dashboardStore = useDashboardStore();
function removeWidget() {
dashboardStore.removeControls(props.data);
}
</script>
<style lang="scss" scoped>
.profile-wrapper {
width: 100%;
height: 100%;
font-size: $font-size-smaller;
position: relative;
}
.operation {
position: absolute;
top: 8px;
right: 3px;
}
.header {
padding: 10px;
font-size: $font-size-smaller;
border-bottom: 1px solid $border-color;
}
.tools {
padding: 5px 0;
color: #999;
cursor: pointer;
position: relative;
text-align: center;
&:hover {
color: $active-color;
background-color: $popper-hover-bg-color;
}
}
</style>

View File

@@ -27,6 +27,7 @@ import Event from "./Event.vue";
import NetworkProfiling from "./NetworkProfiling.vue";
import ContinuousProfiling from "./ContinuousProfiling.vue";
import AsyncProfiling from "./AsyncProfiling.vue";
import Pprof from "./Pprof.vue";
import TimeRange from "./TimeRange.vue";
import ThirdPartyApp from "./ThirdPartyApp.vue";
import TaskTimeline from "./TaskTimeline.vue";
@@ -45,6 +46,7 @@ export default {
NetworkProfiling,
ContinuousProfiling,
AsyncProfiling,
Pprof,
TimeRange,
ThirdPartyApp,
TaskTimeline,

View File

@@ -26,6 +26,7 @@ import Event from "./Event.vue";
import NetworkProfiling from "./NetworkProfiling.vue";
import ContinuousProfiling from "./ContinuousProfiling.vue";
import AsyncProfiling from "./AsyncProfiling.vue";
import Pprof from "./Pprof.vue";
import TimeRange from "./TimeRange.vue";
import ThirdPartyApp from "./ThirdPartyApp.vue";
import TaskTimeline from "./TaskTimeline.vue";
@@ -45,5 +46,6 @@ export default {
ThirdPartyApp,
ContinuousProfiling,
AsyncProfiling,
Pprof,
TaskTimeline,
};

View File

@@ -151,6 +151,7 @@ export enum WidgetType {
NetworkProfiling = "NetworkProfiling",
ContinuousProfiling = "ContinuousProfiling",
AsyncProfiling = "AsyncProfiling",
Pprof = "Pprof",
ThirdPartyApp = "ThirdPartyApp",
TaskTimeline = "TaskTimeline",
}
@@ -173,6 +174,7 @@ export const ServiceTools = [
{ name: "insert_chart", content: "Add eBPF Profiling", id: WidgetType.Ebpf },
{ name: "continuous_profiling", content: "Add Continuous Profiling", id: WidgetType.ContinuousProfiling },
{ name: "async_profiling", content: "Add Async Profiling", id: WidgetType.AsyncProfiling },
{ name: "chart", content: "Add PProf Profiling", id: WidgetType.Pprof },
{ name: "assignment", content: "Add Log", id: WidgetType.Log },
{ name: "demand", content: "Add On Demand Log", id: WidgetType.DemandLog },
{ name: "event", content: "Add Event", id: WidgetType.Event },

View File

@@ -191,6 +191,7 @@ limitations under the License. -->
ElMessage.error("No this dashboard");
return;
}
selectorStore.setCurrentService(scope.row);
const path = `/dashboard/${dashboard.layer}/${dashboard.entity}/${scope.row.id}/${dashboard.name}`;
router.push(path);

View File

@@ -234,6 +234,7 @@ limitations under the License. -->
}
.label {
line-height: 24px;
line-height: var(--el-input-height, 24px);
flex-shrink: 0;
}
</style>

View File

@@ -72,7 +72,7 @@ limitations under the License. -->
</div>
<div class="flex-h row" v-show="!isBrowser">
<div class="mr-10 traceId">
<span class="grey mr-5 label">{{ t("traceID") }}:</span>
<span class="grey mr-5">{{ t("traceID") }}:</span>
<el-input v-model="traceId" class="inputs-max" size="small" />
</div>
<ConditionTags :type="'LOG'" @update="updateTags" />
@@ -133,7 +133,7 @@ limitations under the License. -->
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { ElMessage } from "element-plus";
import { useLogStore } from "@/store/modules/log";
import { useLogStore, PageSizeDefault } from "@/store/modules/log";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut, InitializationDurationRow } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
@@ -274,7 +274,7 @@ limitations under the License. -->
serviceId: selectorStore.currentService ? selectorStore.currentService.id : state.service.id,
pagePathId: endpoint || state.endpoint.id || undefined,
serviceVersionId: instance || state.instance.id || undefined,
paging: { pageNum: 1, pageSize: 15 },
paging: { pageNum: 1, pageSize: PageSizeDefault },
queryDuration: duration,
category: state.category.value,
});
@@ -292,7 +292,7 @@ limitations under the License. -->
keywordsOfContent: keywordsOfContent.value,
excludingKeywordsOfContent: excludingKeywordsOfContent.value,
tags: tagsMap.value.length ? tagsMap.value : undefined,
paging: { pageNum: 1, pageSize: 15 },
paging: { pageNum: 1, pageSize: PageSizeDefault },
relatedTrace: traceId.value ? { traceId: traceId.value, segmentId, spanId } : undefined,
});
}
@@ -480,6 +480,7 @@ limitations under the License. -->
}
.label {
line-height: 24px;
line-height: var(--el-input-height, 24px);
flex-shrink: 0;
}
</style>

View File

@@ -14,21 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div>
<LogTable v-loading="logStore.loadLogs" :tableData="logStore.logs || []" :type="type" :noLink="false" :data="data">
<LogTable v-loading="logStore.loadLogs" :tableData="displayLogs" :type="type" :noLink="false" :data="data">
<div class="log-tips" v-if="!logStore.logs.length">{{ t("noData") }}</div>
</LogTable>
<div class="mt-5 mb-5">
<el-pagination
v-model="logStore.conditions.paging.pageNum"
:page-size="pageSize"
size="small"
layout="prev, pager, next"
:pager-count="5"
:total="total"
@current-change="updatePage"
:style="`float: right`"
/>
</div>
<Pagination
v-model:currentPage="logStore.conditions.paging.pageNum"
:pageSize="pageSize"
:total="logStore.logs.length"
@change="updatePage"
/>
</div>
</template>
<script lang="ts" setup>
@@ -37,7 +31,8 @@ limitations under the License. -->
import type { PropType } from "vue";
import type { LayoutConfig } from "@/types/dashboard";
import LogTable from "./LogTable/Index.vue";
import { useLogStore } from "@/store/modules/log";
import Pagination from "@/views/components/Pagination.vue";
import { useLogStore, PageSizeDefault } from "@/store/modules/log";
import { useDashboardStore } from "@/store/modules/dashboard";
import { ElMessage } from "element-plus";
@@ -54,15 +49,13 @@ limitations under the License. -->
const logStore = useLogStore();
const dashboardStore = useDashboardStore();
const type = ref<string>(dashboardStore.layerId === "BROWSER" ? "browser" : "service");
const pageSize = ref<number>(15);
const total = computed(() =>
logStore.logs.length === pageSize.value
? pageSize.value * logStore.conditions.paging.pageNum + 1
: pageSize.value * logStore.conditions.paging.pageNum,
const pageSize = PageSizeDefault;
const displayLogs = computed(() =>
logStore.logs.length === pageSize ? logStore.logs.slice(0, pageSize - 1) : logStore.logs,
);
function updatePage(p: number) {
logStore.setLogCondition({
paging: { pageNum: p, pageSize: pageSize.value },
paging: { pageNum: p, pageSize: pageSize },
});
queryLogs();
}

View File

@@ -27,7 +27,12 @@ limitations under the License. -->
<Icon iconName="merge" />
</el-tooltip>
</span>
<span v-else v-html="highlightKeywords(getDataValue(item.label))"></span>
<span v-else>
<template v-for="(part, partIndex) in highlightKeywords(getDataValue(item.label))" :key="partIndex">
<span v-if="part.highlight" class="keyword-highlight">{{ part.text }}</span>
<template v-else>{{ part.text }}</template>
</template>
</span>
</div>
</div>
</template>
@@ -59,10 +64,41 @@ limitations under the License. -->
}
return (props.data.tags.find((d: { key: string; value: string }) => d.key === "level") || {}).value || "";
});
const highlightKeywords = (content: string) => {
const keywords = Object.values(logStore.conditions.keywordsOfContent || {});
const regex = new RegExp(keywords.join("|"), "gi");
return `${content}`.replace(regex, (match) => `<span style="color: red">${match}</span>`);
type HighlightPart = {
text: string;
highlight: boolean;
};
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const highlightKeywords = (content: string): HighlightPart[] => {
const text = `${content || ""}`;
const keywords = [
...new Set(Object.values(logStore.conditions.keywordsOfContent || {}).map((keyword) => `${keyword}`.trim())),
].filter(Boolean);
if (!keywords.length) {
return [{ text, highlight: false }];
}
const regex = new RegExp(keywords.map(escapeRegExp).join("|"), "gi");
const parts: HighlightPart[] = [];
let lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push({ text: text.slice(lastIndex, match.index), highlight: false });
}
parts.push({ text: match[0], highlight: true });
lastIndex = regex.lastIndex;
}
if (lastIndex < text.length) {
parts.push({ text: text.slice(lastIndex), highlight: false });
}
return parts.length ? parts : [{ text, highlight: false }];
};
function getDataValue(label: string) {
@@ -165,4 +201,8 @@ limitations under the License. -->
.warning {
color: var(--sw-orange);
}
.keyword-highlight {
color: var(--sw-red);
}
</style>

View File

@@ -461,14 +461,28 @@ limitations under the License. -->
}
}
function showNodeTip(d: ProcessNode, event: MouseEvent) {
const tipHtml = ` <div class="mb-5"><span class="grey">name: </span>${d.name}</div>`;
type TooltipRow = {
label: string;
value: unknown;
};
function renderTooltipRows(rows: TooltipRow[]) {
tooltip.value.html("");
const row = tooltip.value.selectAll("div").data(rows).enter().append("div").attr("class", "mb-5");
row
.append("span")
.attr("class", "grey")
.text((d: TooltipRow) => `${d.label}: `);
row.append("span").text((d: TooltipRow) => `${d.value ?? ""}`);
}
function showNodeTip(d: ProcessNode, event: MouseEvent) {
tooltip.value
.style("top", event.offsetY + "px")
.style("left", event.offsetX + "px")
.style("visibility", "visible")
.html(tipHtml);
.style("visibility", "visible");
renderTooltipRows([{ label: "name", value: d.name }]);
}
function hideNodeTip() {
@@ -487,14 +501,14 @@ limitations under the License. -->
if (types.includes("tls")) {
l = "TLS";
}
const tipHtml = `<div><span class="grey">${t("detectPoint")}: </span>${link.detectPoints.join(" | ")}</div>
<div><span class="grey">Type: </span>${l}</div>`;
tooltip.value
.style("top", event.offsetY + "px")
.style("left", event.offsetX + "px")
.style("visibility", "visible")
.html(tipHtml);
.style("visibility", "visible");
renderTooltipRows([
{ label: t("detectPoint"), value: link.detectPoints.join(" | ") },
{ label: "Type", value: l },
]);
}
function hideLinkTip() {

View File

@@ -0,0 +1,68 @@
<!-- 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="flex-h content">
<TaskList />
<div class="vis-graph ml-5">
<div class="mb-20 mt-10">
<Filter />
</div>
<div class="stack" v-loading="pprofStore.loadingTree">
<PprofStack />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { ElMessage } from "element-plus";
import { usePprofStore } from "@/store/modules/pprof";
import { useSelectorStore } from "@/store/modules/selectors";
import TaskList from "./components/TaskList.vue";
import Filter from "./components/Filter.vue";
import PprofStack from "./components/PprofStack.vue";
const pprofStore: ReturnType<typeof usePprofStore> = usePprofStore();
const selectorStore = useSelectorStore();
onMounted(async () => {
const resp = await pprofStore.getServiceInstances({ serviceId: selectorStore.currentService?.id || "" });
if (resp?.errors) {
ElMessage.error(resp.errors);
}
});
</script>
<style lang="scss" scoped>
.content {
height: calc(100% - 30px);
width: 100%;
}
.vis-graph {
height: 100%;
flex-grow: 2;
min-width: 700px;
overflow: hidden;
position: relative;
width: calc(100% - 330px);
}
.stack {
width: 100%;
overflow: auto;
height: calc(100% - 100px);
padding-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,46 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="flex-h header">
<div class="title">PProf Profiling</div>
<el-button class="mr-20" size="small" type="primary" @click="() => (newTask = true)">
{{ t("newTask") }}
</el-button>
</div>
<el-dialog v-model="newTask" :destroy-on-close="true" fullscreen @closed="newTask = false">
<NewTask @close="newTask = false" />
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import NewTask from "./components/NewTask.vue";
const { t } = useI18n();
const newTask = ref<boolean>(false);
</script>
<style lang="scss" scoped>
.header {
padding: 10px;
font-size: $font-size-smaller;
border-bottom: 1px solid $border-color;
justify-content: space-between;
}
.title {
font-weight: bold;
line-height: 24px;
}
</style>

View File

@@ -0,0 +1,80 @@
<!-- 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="flex-h">
<Selector
class="filter-selector"
:multiple="true"
:value="serviceInstanceIds"
size="small"
:options="instances"
placeholder="Select instances"
@change="changeInstances"
/>
<el-button type="primary" size="small" @click="analyzeProfiling">
{{ t("analyze") }}
</el-button>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { ElMessage } from "element-plus";
import { usePprofStore } from "@/store/modules/pprof";
import type { Instance } from "@/types/selector";
import type { Option } from "@/types/app";
const { t } = useI18n();
const pprofStore = usePprofStore();
const serviceInstanceIds = ref<string[]>([]);
const instances = computed(() =>
pprofStore.instances.filter((d: Instance) =>
(pprofStore.selectedTask?.successInstanceIds || []).includes(d.id || ""),
),
);
function changeInstances(options: Option[]) {
serviceInstanceIds.value = options.map((d: Option) => d.value);
pprofStore.setAnalyzeTrees([]);
}
async function analyzeProfiling() {
const instanceIds = (pprofStore.instances || [])
.filter((d: Instance) => (serviceInstanceIds.value || []).includes(d.value))
.map((d: Instance) => d.id || "") as string[];
const res = await pprofStore.getPprofAnalyze({
instanceIds,
taskId: pprofStore.selectedTask?.id || "",
});
if ((res as { errors?: string }).errors) {
ElMessage.error((res as { errors: string }).errors);
}
}
watch(
() => pprofStore.selectedTask?.successInstanceIds,
(value) => {
serviceInstanceIds.value = value || [];
pprofStore.setAnalyzeTrees([]);
},
{ immediate: true },
);
</script>
<style>
.filter-selector {
width: 400px;
margin-right: 10px;
}
</style>

View File

@@ -0,0 +1,185 @@
<!-- 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="pprof-task">
<div>
<div class="label">{{ t("instance") }}</div>
<Selector
class="profile-input"
:multiple="true"
:value="serviceInstanceIds"
size="small"
:options="pprofStore.instances"
placeholder="Select instances"
@change="changeInstances"
:filterable="true"
/>
</div>
<div>
<div class="label">{{ t("pprofEvent") }}</div>
<Radio class="mb-5" :value="eventType" :options="PprofEvents" @change="changeEventType" />
</div>
<div v-if="requiresDuration">
<div class="label">{{ t("duration") }}</div>
<Radio class="mb-5" :value="duration" :options="DurationOptions" @change="changeDuration" />
<div v-if="duration === DurationOptions[5].value" class="custom-duration">
<div class="label">{{ t("customDuration") }} ({{ t("minutes") }})</div>
<el-input
size="small"
class="profile-input"
v-model="customDurationMinutes"
type="number"
:min="1"
placeholder="Enter duration in minutes"
/>
</div>
<div class="hint">{{ t("pprofDurationHint") }}</div>
</div>
<div v-if="requiresDumpPeriod">
<div class="label">{{ t("pprofDumpPeriod") }}</div>
<el-input
size="small"
class="profile-input"
v-model="dumpPeriod"
type="number"
:min="1"
placeholder="Enter dump period"
/>
<div class="hint">
{{ eventType === "BLOCK" ? t("pprofDumpPeriodBlockHint") : t("pprofDumpPeriodMutexHint") }}
</div>
</div>
<div>
<el-button @click="createTask" type="primary" class="create-task-btn" :loading="loading">
{{ t("createTask") }}
</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import { usePprofStore } from "@/store/modules/pprof";
import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus";
import { DurationOptions, PprofEvents, DurationRequiredEvents, DumpPeriodRequiredEvents } from "./data";
import type { PprofTaskCreationRequest } from "@/types/pprof";
/* global defineEmits */
const emits = defineEmits(["close"]);
const pprofStore = usePprofStore();
const selectorStore = useSelectorStore();
const { t } = useI18n();
const serviceInstanceIds = ref<string[]>([]);
const eventType = ref<string>(PprofEvents[0].value);
const duration = ref<string>(DurationOptions[1].value);
const customDurationMinutes = ref<number>(5);
const dumpPeriod = ref<number | string>("");
const loading = ref<boolean>(false);
const requiresDuration = computed(() => DurationRequiredEvents.includes(eventType.value));
const requiresDumpPeriod = computed(() => DumpPeriodRequiredEvents.includes(eventType.value));
function changeDuration(val: string) {
duration.value = val;
}
function changeEventType(val: string) {
eventType.value = val;
}
function changeInstances(options: { id: string }[]) {
serviceInstanceIds.value = options.map((d: { id: string }) => d.id);
}
async function createTask() {
const params: PprofTaskCreationRequest = {
serviceId: selectorStore.currentService?.id || "",
serviceInstanceIds: serviceInstanceIds.value,
events: eventType.value,
};
if (requiresDuration.value) {
const finalDuration =
duration.value === DurationOptions[5].value ? Number(customDurationMinutes.value) : Number(duration.value);
if (!finalDuration || finalDuration < 1) {
ElMessage.error(t("invalidPprofDuration"));
return;
}
params.duration = finalDuration;
}
if (requiresDumpPeriod.value) {
const finalDumpPeriod = Number(dumpPeriod.value);
if (!finalDumpPeriod || finalDumpPeriod < 1) {
ElMessage.error(t("invalidPprofDumpPeriod"));
return;
}
params.dumpPeriod = finalDumpPeriod;
}
loading.value = true;
const res = await pprofStore.createTask(params);
loading.value = false;
if (!res) {
ElMessage.error(t("taskCreationFailed"));
return;
}
if (res.errors) {
ElMessage.error(res.errors);
return;
}
const result = res.data?.task;
if (!result) {
ElMessage.error(t("taskCreationFailed"));
return;
}
if (result.errorReason) {
ElMessage.error(result.errorReason);
return;
}
emits("close");
ElMessage.success(t("taskCreatedSuccessfully"));
}
</script>
<style lang="scss" scoped>
.pprof-task {
margin: 0 auto;
width: 600px;
}
.label {
margin-top: 10px;
font-size: $font-size-normal;
}
.profile-input {
width: 600px;
}
.create-task-btn {
width: 600px;
margin-top: 50px;
}
.custom-duration {
margin-top: 10px;
}
.hint {
font-size: $font-size-smaller;
color: var(--text-color-placeholder);
}
</style>

View File

@@ -0,0 +1,170 @@
<!-- 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="graph"></div>
</template>
<script lang="ts" setup>
/* global Nullable */
import { ref, watch } from "vue";
import * as d3 from "d3";
import d3tip from "d3-tip";
import { flamegraph } from "d3-flame-graph";
import { usePprofStore } from "@/store/modules/pprof";
import type { PprofStackElement, PprofFlameGraphNode } from "@/types/pprof";
import "d3-flame-graph/dist/d3-flamegraph.css";
import { treeForeach, escapeHtml } from "@/utils/flameGraph";
const pprofStore = usePprofStore();
const stackTree = ref<Nullable<PprofFlameGraphNode>>(null);
const selectStack = ref<Nullable<PprofFlameGraphNode>>(null);
const graph = ref<Nullable<HTMLDivElement>>(null);
const flameChart = ref<any>(null);
const min = ref<number>(1);
const max = ref<number>(1);
function drawGraph() {
if (flameChart.value) {
flameChart.value.destroy();
}
if (!pprofStore.analyzeTrees.length || !graph.value) {
stackTree.value = null;
return;
}
const root: PprofFlameGraphNode = {
parentId: "0",
originId: "1",
name: "Virtual Root",
children: [],
value: 0,
id: "1",
symbol: "Virtual Root",
dumpCount: 0,
self: 0,
};
countRange();
const elements = processTree((pprofStore.analyzeTrees[0].elements || []) as PprofStackElement[]);
if (!elements) {
stackTree.value = null;
return;
}
stackTree.value = elements;
const treeRoot = { ...root, ...elements };
const width = graph.value.getBoundingClientRect().width || 0;
const w = width < 800 ? 802 : width;
flameChart.value = flamegraph()
.width(w - 15)
.cellHeight(18)
.transitionDuration(750)
.minFrameSize(1)
.transitionEase(d3.easeCubic as any)
.sort(true)
.title("")
.selfValue(false)
.inverted(true)
.onClick((d: { data: PprofFlameGraphNode }) => {
selectStack.value = d.data;
})
.setColorMapper((d, originalColor) => (d.highlight ? "#6aff8f" : originalColor));
const tip = (d3tip as any)()
.attr("class", "d3-tip")
.direction("s")
.html((d: { data: PprofFlameGraphNode } & { parent: { data: PprofFlameGraphNode } }) => {
const name = escapeHtml(d.data.name);
const rateOfParent =
(d.parent &&
`<div class="mb-5">Percentage Of Selected: ${
(
(d.data.dumpCount / ((selectStack.value && selectStack.value.dumpCount) || treeRoot.dumpCount)) *
100
).toFixed(3) + "%"
}</div>`) ||
"";
const rateOfRoot = `<div class="mb-5">Percentage Of Root: ${
((d.data.dumpCount / treeRoot.dumpCount) * 100).toFixed(3) + "%"
}</div>`;
return `<div class="mb-5 name">Symbol: ${name}</div>
<div class="mb-5">Total: ${d.data.dumpCount}</div>
<div class="mb-5">Self: ${d.data.self}</div>
${rateOfParent}${rateOfRoot}`;
})
.style("max-width", "400px");
flameChart.value.tooltip(tip);
d3.select(graph.value).datum(treeRoot).call(flameChart.value);
}
function countRange() {
const list = (pprofStore.analyzeTrees[0]?.elements || []).map((ele: PprofStackElement) => ele.dumpCount);
max.value = Math.max(...(list.length ? list : [1]));
min.value = Math.min(...(list.length ? list : [1]));
}
function processTree(arr: PprofStackElement[]): Nullable<PprofFlameGraphNode> {
const obj: Record<string, PprofFlameGraphNode> = {};
const childrenByParentId: Record<string, PprofFlameGraphNode[]> = {};
let res = null as Nullable<PprofFlameGraphNode>;
const copyArr = arr.map((item) => {
const node: PprofFlameGraphNode = {
parentId: String(Number(item.parentId) + 1),
originId: String(Number(item.id) + 1),
id: item.id,
name: item.symbol,
symbol: item.symbol,
dumpCount: item.dumpCount,
self: item.self,
value: 0,
};
obj[node.originId] = node;
// Group nodes by their parentId
if (childrenByParentId[node.parentId]) {
childrenByParentId[node.parentId].push(node);
} else {
childrenByParentId[node.parentId] = [node];
}
return node;
});
const scale = d3.scaleLinear().domain([min.value, max.value]).range([1, 200]);
// Link children to parents in O(n) using the adjacency map
for (const item of copyArr) {
item.value = Number(scale(item.dumpCount).toFixed(4));
if (item.parentId === "0") {
res = item as PprofFlameGraphNode;
}
const children = childrenByParentId[item.originId];
if (children) {
item.children = children;
}
}
if (!res) {
return null;
}
treeForeach([res], (node: PprofFlameGraphNode) => {
if (node.children) {
let val = 0;
for (const child of node.children) {
val = child.value + val;
}
node.value = node.value < val ? val : node.value;
}
});
return res;
}
watch(
() => pprofStore.analyzeTrees,
() => {
drawGraph();
},
);
</script>

View File

@@ -0,0 +1,284 @@
<!-- 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="profile-task-list flex-v" v-loading="pprofStore.loadingTasks">
<div class="profile-t-tool flex-h">{{ t("taskList") }}</div>
<div class="profile-t-wrapper">
<div class="no-data" v-show="!pprofStore.taskList.length">
{{ t("noData") }}
</div>
<table class="profile-t">
<tr
class="profile-tr cp"
v-for="(i, index) in pprofStore.taskList"
@click="changeTask(i)"
:key="index"
:class="{
selected: pprofStore.selectedTask?.id === i.id,
}"
>
<td class="profile-td">
<div class="ell">
<span>{{ i.id }}</span>
<a class="profile-btn r" @click="() => (showDetail = true)">
<Icon iconName="view" size="middle" />
</a>
</div>
<div class="grey ell sm task-info">
<span class="mr-10 sm">
{{ dateFormat(i.createTime) }}
</span>
<span class="task-type">{{ i.events }}</span>
</div>
</td>
</tr>
</table>
</div>
</div>
<el-dialog v-model="showDetail" :destroy-on-close="true" fullscreen @closed="showDetail = false">
<div class="profile-detail flex-v" v-if="pprofStore.selectedTask?.id">
<div>
<h5 class="mb-10">{{ t("task") }}.</h5>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">ID:</span>
<span class="g-sm-8 wba">{{ pprofStore.selectedTask.id }}</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("service") }}:</span>
<span class="g-sm-8 wba">{{ service }}</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("events") }}:</span>
<span class="g-sm-8 wba">{{ pprofStore.selectedTask.events }}</span>
</div>
<div class="mb-10 clear item" v-if="pprofStore.selectedTask.duration !== undefined">
<span class="g-sm-4 grey">{{ t("duration") }}:</span>
<span class="g-sm-8 wba">{{ pprofStore.selectedTask.duration }}{{ t("minutes") }}</span>
</div>
<div class="mb-10 clear item" v-if="pprofStore.selectedTask.dumpPeriod !== undefined">
<span class="g-sm-4 grey">{{ t("pprofDumpPeriod") }}:</span>
<span class="g-sm-8 wba">{{ pprofStore.selectedTask.dumpPeriod }}</span>
</div>
</div>
<div>
<h5 class="mb-5 mt-10" v-show="pprofStore.selectedTask?.logs?.length"> {{ t("logs") }}. </h5>
<div v-for="(i, index) in Object.keys(instanceLogs)" :key="index">
<div class="sm">
<span class="mr-10 grey">{{ t("instance") }}:</span>
<span>{{ i }}</span>
</div>
<div v-for="(d, logIndex) in instanceLogs[i]" :key="`${d.instanceId}-${logIndex}`">
<span class="mr-10 grey">{{ t("operationType") }}:</span>
<span class="mr-20">{{ d.operationType }}</span>
<span class="mr-10 grey">{{ t("time") }}:</span>
<span>{{ dateFormat(d.operationTime) }}</span>
</div>
</div>
</div>
<div>
<h5 class="mb-10 mt-10" v-show="errorInstances.length"> {{ t("errorInstances") }}</h5>
<div v-for="(instance, index) in errorInstances" :key="instance.value || index">
<div class="mb-10 sm">
<span class="mr-10 grey">{{ t("instance") }}:</span>
<span>{{ instance.label }}</span>
</div>
<div v-for="(d, attrIndex) in instance.attributes" :key="d.value + attrIndex">
<span class="mr-10 grey">{{ d.name }}:</span>
<span class="mr-20">{{ d.value }}</span>
</div>
</div>
</div>
<div>
<h5 class="mb-10 mt-10" v-show="successInstances.length"> {{ t("successInstances") }}</h5>
<div v-for="(instance, index) in successInstances" :key="instance.value || index">
<div class="mb-10 sm">
<span class="mr-10 grey">{{ t("instance") }}:</span>
<span>{{ instance.label }}</span>
</div>
<div v-for="(d, attrIndex) in instance.attributes" :key="d.value + attrIndex">
<span class="mr-10 grey">{{ d.name }}:</span>
<span class="mr-20">{{ d.value }}</span>
</div>
</div>
</div>
</div>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, onMounted, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useSelectorStore } from "@/store/modules/selectors";
import { usePprofStore } from "@/store/modules/pprof";
import type { TaskLog } from "@/types/profile";
import type { PprofTask } from "@/types/pprof";
import { ElMessage } from "element-plus";
import { dateFormat } from "@/utils/dateFormat";
import type { Instance, Service } from "@/types/selector";
/*global Nullable*/
const { t } = useI18n();
const pprofStore = usePprofStore();
const selectorStore = useSelectorStore();
const showDetail = ref<boolean>(false);
const service = ref<string>("");
const instanceLogs = ref<Record<string, TaskLog[]>>({});
const errorInstances = ref<Instance[]>([]);
const successInstances = ref<Instance[]>([]);
onMounted(() => {
fetchTasks();
});
watch(
() => pprofStore.instances,
() => {
syncTaskDetails(pprofStore.selectedTask);
},
{ deep: true },
);
async function fetchTasks() {
const res = await pprofStore.getTaskList();
if (res?.errors) {
ElMessage.error(res.errors);
return;
}
const errorReason = res?.data?.pprofTaskList?.errorReason;
if (errorReason) {
ElMessage.error(errorReason);
return;
}
if (pprofStore.selectedTask?.id) {
await changeTask(pprofStore.selectedTask as PprofTask);
}
}
function syncTaskDetails(item?: Nullable<PprofTask>) {
if (!item?.id) {
instanceLogs.value = {};
errorInstances.value = [];
successInstances.value = [];
return;
}
errorInstances.value = pprofStore.instances.filter((d: Instance) =>
d.id ? (item.errorInstanceIds || []).includes(d.id) : false,
);
successInstances.value = pprofStore.instances.filter((d: Instance) =>
d.id ? (item.successInstanceIds || []).includes(d.id) : false,
);
instanceLogs.value = {};
for (const d of item.logs || []) {
if (instanceLogs.value[d.instanceName]) {
instanceLogs.value[d.instanceName].push(d);
} else {
instanceLogs.value[d.instanceName] = [d];
}
}
}
async function changeTask(item: PprofTask) {
if (item.id !== pprofStore.selectedTask?.id) {
pprofStore.setAnalyzeTrees([]);
pprofStore.setSelectedTask(item);
}
service.value = (selectorStore.services.find((s: Service) => s.id === item.serviceId) || {}).label || "";
const res = await pprofStore.getTaskLogs({ taskId: item.id });
if (res?.errors) {
ElMessage.error(res.errors);
return;
}
const selectedTask = {
...item,
...pprofStore.taskProgress,
};
pprofStore.setSelectedTask(selectedTask);
syncTaskDetails(selectedTask);
}
</script>
<style lang="scss" scoped>
.profile-task-list {
width: 300px;
height: calc(100% - 20px);
overflow: auto;
border-right: 1px solid var(--sw-trace-list-border);
}
.item span {
height: 21px;
}
.profile-td {
padding: 5px 10px;
border-bottom: 1px solid var(--sw-trace-list-border);
}
.selected {
background-color: var(--sw-list-selected);
}
.no-data {
text-align: center;
margin-top: 10px;
}
.profile-t-wrapper {
overflow: auto;
flex-grow: 1;
}
.profile-t {
width: 100%;
border-spacing: 0;
table-layout: fixed;
flex-grow: 1;
position: relative;
}
.profile-tr {
&:hover {
background-color: var(--sw-list-selected);
}
}
.profile-t-tool {
padding: 5px 10px;
font-weight: bold;
border-right: 1px solid var(--sw-trace-list-border);
border-bottom: 1px solid var(--sw-trace-list-border);
background-color: var(--sw-table-header);
}
.profile-btn {
color: $font-color;
padding: 1px 3px;
border-radius: 2px;
font-size: $font-size-smaller;
float: right;
}
.task-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.task-type {
color: var(--sw-profile-task-type);
background-color: var(--sw-list-selected);
padding: 1px 6px;
border-radius: 3px;
font-size: $font-size-smaller;
}
</style>

View File

@@ -0,0 +1,39 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const DurationOptions = [
{ value: "1", label: "1 min" },
{ value: "5", label: "5 min" },
{ value: "10", label: "10 min" },
{ value: "15", label: "15 min" },
{ value: "30", label: "30 min" },
{ value: "custom", label: "Custom" },
];
export const PprofEvents = [
{ label: "CPU", value: "CPU" },
{ label: "HEAP", value: "HEAP" },
{ label: "BLOCK", value: "BLOCK" },
{ label: "MUTEX", value: "MUTEX" },
{ label: "GOROUTINE", value: "GOROUTINE" },
{ label: "THREADCREATE", value: "THREADCREATE" },
{ label: "ALLOCS", value: "ALLOCS" },
];
export const DurationRequiredEvents = ["CPU", "BLOCK", "MUTEX"];
export const DumpPeriodRequiredEvents = ["BLOCK", "MUTEX"];

View File

@@ -122,27 +122,33 @@ limitations under the License. -->
).dashboard || {};
const exprssions = dashboard.expressions || [];
const nodeMetricConfig = dashboard.expressionsConfig || [];
const html = exprssions.map((m: string, index: number) => {
const metrics = exprssions.map((m: string, index: number) => {
const metric =
topologyStore.hierarchyInstanceNodeMetrics[data.layer || ""][m].values.find(
(val: { id: string; value: unknown }) => val.id === data.id,
) || null;
const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${metric?.value || NaN} ${
opt.unit || ""
}</div>`;
return {
label: opt.label || m,
value: `${metric?.value || NaN} ${opt.unit || ""}`,
};
});
const tipHtml = [
`<div class="mb-5"><span class="grey">name: </span>${data.name}</div><div class="mb-5"><span class="grey">layer: </span>${data.layer}</div>`,
...html,
].join(" ");
popover.value
.style("top", event.offsetY + 10 + "px")
.style("left", event.offsetX + 10 + "px")
.style("visibility", "visible")
.html(tipHtml);
.html("");
const rows = [{ label: "name", value: data.name }, { label: "layer", value: data.layer }, ...metrics];
const row = popover.value.selectAll("div").data(rows).enter().append("div").attr("class", "mb-5");
row
.append("span")
.attr("class", "grey")
.text((d: { label: string }) => `${d.label}: `);
row.append("span").text((d: { value: unknown }) => `${d.value ?? ""}`);
}
function hideTip() {

View File

@@ -130,26 +130,32 @@ limitations under the License. -->
).dashboard || {};
const exprssions = dashboard.expressions || [];
const nodeMetricConfig = dashboard.expressionsConfig || [];
const html = exprssions.map((m: string, index: number) => {
const metrics = exprssions.map((m: string, index: number) => {
const metric =
topologyStore.hierarchyNodeMetrics[data.layer || ""][m].values.find(
(val: { id: string; value: unknown }) => val.id === data.id,
) || null;
const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${metric?.value || NaN} ${
opt.unit || ""
}</div>`;
});
const tipHtml = [
`<div class="mb-5"><span class="grey">name: </span>${data.name}</div><div class="mb-5"><span class="grey">layer: </span>${data.layer}</div>`,
...html,
].join(" ");
return {
label: opt.label || m,
value: `${metric?.value || NaN} ${opt.unit || ""}`,
};
});
popover.value
.style("top", event.offsetY + 10 + "px")
.style("left", event.offsetX + 10 + "px")
.style("visibility", "visible")
.html(tipHtml);
.html("");
const rows = [{ label: "name", value: data.name }, { label: "layer", value: data.layer }, ...metrics];
const row = popover.value.selectAll("div").data(rows).enter().append("div").attr("class", "mb-5");
row
.append("span")
.attr("class", "grey")
.text((d: { label: string }) => `${d.label}: `);
row.append("span").text((d: { value: unknown }) => `${d.value ?? ""}`);
}
function hideTip() {

View File

@@ -304,70 +304,103 @@ limitations under the License. -->
}
return Number(d[legendMQE.expression]) && d.isReal ? icons.CUBEERROR : icons.CUBE;
}
type TooltipRow = {
label: string;
value: unknown;
};
function isTooltipRow(row: TooltipRow | null): row is TooltipRow {
return Boolean(row);
}
function renderTooltipRows(rows: TooltipRow[]) {
tooltip.value.html("");
const row = tooltip.value.selectAll("div").data(rows).enter().append("div").attr("class", "mb-5");
row
.append("span")
.attr("class", "grey")
.text((d: TooltipRow) => `${d.label}: `);
row.append("span").text((d: TooltipRow) => `${d.value ?? ""}`);
}
function showNodeTip(event: MouseEvent, data: Node) {
const nodeMetrics: string[] = settings.value.nodeExpressions || [];
const nodeMetricConfig = settings.value.nodeMetricConfig || [];
const html = nodeMetrics.map((m, index) => {
const metrics = nodeMetrics.map((m, index) => {
const metric =
topologyStore.nodeMetricValue[m]?.values?.find((val: { id: string; value: unknown }) => val.id === data.id) ||
null;
const opt: MetricConfigOpt = nodeMetricConfig[index] || {};
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${metric?.value || NaN} ${
opt.unit || "unknown"
}</div>`;
return {
label: opt.label || m,
value: `${metric?.value || NaN} ${opt.unit || "unknown"}`,
};
});
let tipHtml = `<div class="mb-5"><span class="grey">name: </span>${
data.name
}</div><div class="mb-5"><span class="grey">type: </span>${data.type || "UNKNOWN"}</div>`;
if (data.isReal) {
tipHtml = [tipHtml, ...html].join(" ");
}
const rows = [
{ label: "name", value: data.name },
{ label: "type", value: data.type || "UNKNOWN" },
];
const tipRows = data.isReal ? [...rows, ...metrics] : rows;
tooltip.value
.style("top", event.offsetY + 10 + "px")
.style("left", event.offsetX + 10 + "px")
.style("visibility", "visible")
.html(tipHtml);
.style("visibility", "visible");
renderTooltipRows(tipRows);
}
function showLinkTip(event: MouseEvent, data: Call) {
const linkClientMetrics: string[] = settings.value.linkClientExpressions || [];
const linkServerMetricConfig: MetricConfigOpt[] = settings.value.linkServerMetricConfig || [];
const linkClientMetricConfig: MetricConfigOpt[] = settings.value.linkClientMetricConfig || [];
const linkServerMetrics: string[] = settings.value.linkServerExpressions || [];
const htmlServer = linkServerMetrics.map((m, index) => {
const metric = topologyStore.linkServerMetrics[m]?.values?.find(
(val: { id: string; value: unknown }) => val.id === data.id,
);
if (metric) {
const opt: MetricConfigOpt = linkServerMetricConfig[index] || {};
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${metric.value || NaN} ${
opt.unit || ""
}</div>`;
}
});
const htmlClient = linkClientMetrics.map((m: string, index: number) => {
const opt: MetricConfigOpt = linkClientMetricConfig[index] || {};
const metric = topologyStore.linkClientMetrics[m]?.values?.find(
(val: { id: string; value: unknown }) => val.id === data.id,
);
if (metric) {
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${metric.value || NaN} ${
opt.unit || ""
}</div>`;
}
});
const html = [
...htmlServer,
...htmlClient,
`<div><span class="grey">${t("detectPoint")}:</span>${data.detectPoints.join(" | ")}</div>`,
].join(" ");
const serverRows = linkServerMetrics
.map((m, index): TooltipRow | null => {
const metric = topologyStore.linkServerMetrics[m]?.values?.find(
(val: { id: string; value: unknown }) => val.id === data.id,
);
if (metric) {
const opt: MetricConfigOpt = linkServerMetricConfig[index] || {};
return {
label: opt.label || m,
value: `${metric.value || NaN} ${opt.unit || ""}`,
};
}
return null;
})
.filter(isTooltipRow);
const clientRows = linkClientMetrics
.map((m: string, index: number): TooltipRow | null => {
const opt: MetricConfigOpt = linkClientMetricConfig[index] || {};
const metric = topologyStore.linkClientMetrics[m]?.values?.find(
(val: { id: string; value: unknown }) => val.id === data.id,
);
if (metric) {
return {
label: opt.label || m,
value: `${metric.value || NaN} ${opt.unit || ""}`,
};
}
return null;
})
.filter(isTooltipRow);
const rows = [
...serverRows,
...clientRows,
{
label: t("detectPoint"),
value: data.detectPoints.join(" | "),
},
];
tooltip.value
.style("top", event.offsetY + "px")
.style("left", event.offsetX + "px")
.style("visibility", "visible")
.html(html);
.style("visibility", "visible");
renderTooltipRows(rows);
}
function hideTip() {
tooltip.value.style("visibility", "hidden");
}

View File

@@ -293,7 +293,7 @@ limitations under the License. -->
selectedMinTimestamp: props.selectedMinTimestamp,
});
}
tree.value.draw(() => {
tree.value?.draw(() => {
setTimeout(() => {
loading.value = false;
}, 200);

View File

@@ -105,6 +105,7 @@ limitations under the License. -->
import { EntityType, QueryOrders, Status } from "@/views/dashboard/data";
import type { LayoutConfig, FilterDuration } from "@/types/dashboard";
import { useDuration } from "@/hooks/useDuration";
import { TimeType } from "@/constants/data";
/*global defineProps, defineEmits, Recordable */
const emits = defineEmits(["get", "search"]);
@@ -126,7 +127,7 @@ limitations under the License. -->
const { duration: filtersDuration } = filters.value;
const duration = ref<DurationTime | FilterDuration>(
filtersDuration
? { start: filtersDuration.startTime || "", end: filtersDuration.endTime || "", step: filtersDuration.step || "" }
? { start: filtersDuration.startTime || "", end: filtersDuration.endTime || "", step: TimeType.SECOND_TIME }
: getDurationTime(),
);
const minTraceDuration = ref<number>();
@@ -152,7 +153,6 @@ limitations under the License. -->
}
async function init() {
duration.value = filters.value.duration || appStore.durationTime;
if (dashboardStore.entity === EntityType[1].value) {
await getServices();
}
@@ -335,7 +335,7 @@ limitations under the License. -->
? {
start: filtersDuration.startTime || "",
end: filtersDuration.endTime || "",
step: filtersDuration.step || "",
step: TimeType.SECOND_TIME,
}
: getDurationTime();
init();
@@ -356,6 +356,7 @@ limitations under the License. -->
}
.label {
line-height: 24px;
line-height: var(--el-input-height, 24px);
flex-shrink: 0;
}
</style>

View File

@@ -93,6 +93,8 @@ limitations under the License. -->
import { ElMessage } from "element-plus";
import { EntityType, QueryOrders, Status } from "@/views/dashboard/data";
import type { LayoutConfig } from "@/types/dashboard";
import { TimeType } from "@/constants/data";
import { useDuration } from "@/hooks/useDuration";
const FiltersKeys: { [key: string]: string } = {
status: "traceState",
@@ -130,10 +132,11 @@ limitations under the License. -->
const tagsMap = ref<Option[]>([]);
const traceId = ref<string>(filters.refId || "");
const { duration: filtersDuration } = props.data.filters || {};
const { getDurationTime } = useDuration();
const duration = ref<DurationTime>(
filtersDuration
? { start: filtersDuration.startTime || "", end: filtersDuration.endTime || "", step: filtersDuration.step || "" }
: appStore.durationTime,
? { start: filtersDuration.startTime || "", end: filtersDuration.endTime || "", step: TimeType.SECOND_TIME }
: getDurationTime(),
);
const state = reactive<Recordable>({
instance: "",

View File

@@ -67,6 +67,8 @@ limitations under the License. -->
:traceId="traceStore.currentTrace?.traceIds?.[0]?.value"
:showBtnDetail="false"
:headerType="WidgetType.Trace"
:minTimestamp="NaN"
:maxTimestamp="NaN"
/>
</div>
</div>

View File

@@ -24,6 +24,6 @@ export enum TraceGraphType {
export const GraphTypeOptions = [
{ value: "List", icon: "list-bulleted", label: "list" },
{ value: "Tree", icon: "issue-child", label: "tree" },
{ value: "Table", icon: "table", label: "table" },
{ value: "Table", icon: "list-tree", label: "table" },
{ value: "Statistics", icon: "statistics-bulleted", label: "statistics" },
] as const;