feat: merge

This commit is contained in:
Fine 2023-06-19 09:36:26 +08:00
commit c0e51f28a1
80 changed files with 3953 additions and 598 deletions

View File

@ -49,6 +49,7 @@ jobs:
npm ci
npm run lint
npm run build --if-present
npm run check-components-types
npm run test:unit
env:
CI: true

941
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,8 @@
"lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged",
"prepare": "husky install"
"prepare": "husky install",
"check-components-types": "if (! git diff --quiet -U0 ./src/types); then echo 'type files are not updated correctly'; git diff -U0 ./src/types; exit 1; fi"
},
"dependencies": {
"axios": "^0.24.0",
@ -22,7 +23,7 @@
"d3-flame-graph": "^4.1.3",
"d3-tip": "^0.9.1",
"echarts": "^5.2.2",
"element-plus": "^2.0.2",
"element-plus": "^2.1.0",
"lodash": "^4.17.21",
"monaco-editor": "^0.34.1",
"pinia": "^2.0.28",
@ -57,7 +58,7 @@
"eslint-plugin-vue": "^9.3.0",
"husky": "^8.0.2",
"jsdom": "^20.0.3",
"lint-staged": "^12.1.3",
"lint-staged": "^13.2.1",
"mockjs": "^1.1.0",
"node-sass": "^8.0.0",
"npm-run-all": "^4.1.5",
@ -66,15 +67,15 @@
"prettier": "^2.7.1",
"sass": "^1.56.1",
"start-server-and-test": "^1.15.2",
"stylelint": "^14.1.0",
"stylelint": "^15.6.0",
"stylelint-config-html": "^1.0.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^24.0.0",
"stylelint-order": "^5.0.0",
"stylelint-config-standard": "^33.0.0",
"stylelint-order": "^6.0.3",
"typescript": "~4.7.4",
"unplugin-auto-import": "^0.7.0",
"unplugin-vue-components": "^0.19.2",
"vite": "^4.0.0",
"vite": "^4.0.5",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svg-icons": "^2.0.1",
"vitest": "^0.25.6",

View File

@ -12,6 +12,6 @@ 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="M15 15.984h6v3q0 0.797-0.609 1.406t-1.406 0.609h-13.969q-0.797 0-1.406-0.609t-0.609-1.406v-3h6q0 1.219 0.891 2.109t2.109 0.891 2.109-0.891 0.891-2.109zM18.984 9v-3.984h-13.969v3.984h3.984q0 1.219 0.891 2.109t2.109 0.891 2.109-0.891 0.891-2.109h3.984zM18.984 3q0.797 0 1.406 0.609t0.609 1.406v6.984q0 0.797-0.609 1.406t-1.406 0.609h-13.969q-0.797 0-1.406-0.609t-0.609-1.406v-6.984q0-0.797 0.609-1.406t1.406-0.609h13.969z"></path>
<svg t="1684376918107" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7954">
<path d="M243.921917 780.038686l357.445972 0L601.367889 422.592714 243.921917 422.592714 243.921917 780.038686zM288.679283 467.350081l268.036639 0 0 268.035616L288.679283 735.385696 288.679283 467.350081zM779.993149 65.25112 243.921917 65.25112c-98.640578 0-178.6314 79.990822-178.6314 178.716334L65.290517 780.038686c0 98.640578 79.990822 178.710194 178.6314 178.710194l536.071232 0c98.725512 0 178.716334-80.069617 178.716334-178.710194L958.709483 243.967454C958.709483 145.242965 878.717637 65.25112 779.993149 65.25112zM869.404528 735.385696c0 73.992201-60.07319 133.972271-134.084834 133.972271L288.679283 869.357967c-74.096579 0-134.063345-59.98007-134.063345-133.972271L154.615938 288.61328c0-73.984015 59.966767-134.057205 134.063345-134.057205l446.639386 0c74.011644 0 134.084834 60.07319 134.084834 134.057205L869.403504 735.385696zM511.957533 243.967454l268.035616 0 0 89.319282 0 268.035616-89.326445 0-44.645826 0 0-44.673455 89.298815 0L735.319693 288.61328l-268.014126 0 0 89.326445-44.652989 0 0-133.971247L511.957533 243.968477z" p-id="7955"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.9 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 t="1684390612367" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12207">
<path d="M202.66008 885.33784A10.66632 10.66632 0 0 0 213.3264 874.67152v-85.33056a42.66528 42.66528 0 0 0-42.66528-42.66528H85.33056a42.66528 42.66528 0 0 0-42.66528 42.66528v85.33056a10.66632 10.66632 0 0 0 10.66632 10.66632zM458.65176 885.33784a10.66632 10.66632 0 0 0 10.66632-10.66632v-298.65696a42.66528 42.66528 0 0 0-42.66528-42.665281H341.32224a42.66528 42.66528 0 0 0-42.66528 42.665281v298.65696a10.66632 10.66632 0 0 0 10.66632 10.66632zM714.643441 885.33784a10.66632 10.66632 0 0 0 10.66632-10.66632v-213.3264a42.66528 42.66528 0 0 0-42.66528-42.66528h-85.33056a42.66528 42.66528 0 0 0-42.66528 42.66528v213.3264a10.66632 10.66632 0 0 0 10.66632 10.66632zM970.635121 885.33784a10.66632 10.66632 0 0 0 10.66632-10.66632v-511.983361a42.66528 42.66528 0 0 0-42.66528-42.66528h-85.33056a42.66528 42.66528 0 0 0-42.66528 42.66528v511.983361a10.66632 10.66632 0 0 0 10.66632 10.66632z" p-id="12208"></path><path d="M149.32848 576.01456a85.33056 85.33056 0 0 0 85.33056-85.330561 84.51992 84.51992 0 0 0-4.266528-25.599168l135.120942-112.636339a83.410622 83.410622 0 0 0 104.273945-19.626029l106.6632 35.582844A85.33056 85.33056 0 0 0 746.642401 362.688159a83.79461 83.79461 0 0 0-9.85568-38.910735l140.240776-163.621349A85.074568 85.074568 0 1 0 831.972961 85.363839a83.709279 83.709279 0 0 0 3.967871 24.361875L688.190967 282.136111a82.429321 82.429321 0 0 0-91.346364 25.300511l-106.663201-35.540179A85.117234 85.117234 0 1 0 324.256128 302.956767L189.263182 415.763768A84.263928 84.263928 0 0 0 149.32848 405.353439a85.33056 85.33056 0 0 0 0 170.661121zM981.301441 938.66944H42.66528a43.347925 43.347925 0 0 0-42.66528 42.66528 42.66528 42.66528 0 0 0 42.66528 42.66528h938.636161a42.66528 42.66528 0 0 0 42.66528-42.66528 43.305259 43.305259 0 0 0-42.66528-42.66528z" p-id="12209"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

17
src/assets/icons/edit.svg Normal file
View File

@ -0,0 +1,17 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<svg t="1684722897341" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2408">
<path d="M853.333333 501.333333c-17.066667 0-32 14.933333-32 32v320c0 6.4-4.266667 10.666667-10.666666 10.666667H170.666667c-6.4 0-10.666667-4.266667-10.666667-10.666667V213.333333c0-6.4 4.266667-10.666667 10.666667-10.666666h320c17.066667 0 32-14.933333 32-32s-14.933333-32-32-32H170.666667c-40.533333 0-74.666667 34.133333-74.666667 74.666666v640c0 40.533333 34.133333 74.666667 74.666667 74.666667h640c40.533333 0 74.666667-34.133333 74.666666-74.666667V533.333333c0-17.066667-14.933333-32-32-32z" fill="#666666" p-id="2409"></path><path d="M405.333333 484.266667l-32 125.866666c-2.133333 10.666667 0 23.466667 8.533334 29.866667 6.4 6.4 14.933333 8.533333 23.466666 8.533333h8.533334l125.866666-32c6.4-2.133333 10.666667-4.266667 14.933334-8.533333l300.8-300.8c38.4-38.4 38.4-102.4 0-140.8-38.4-38.4-102.4-38.4-140.8 0L413.866667 469.333333c-4.266667 4.266667-6.4 8.533333-8.533334 14.933334z m59.733334 23.466666L761.6 213.333333c12.8-12.8 36.266667-12.8 49.066667 0 12.8 12.8 12.8 36.266667 0 49.066667L516.266667 558.933333l-66.133334 17.066667 14.933334-68.266667z" p-id="2410"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

16
src/assets/icons/mq.svg Normal file
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 class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M750.08 657.92c-16.384 0-30.208 2.56-44.032 8.192l-44.032-52.224c33.28-38.4 55.296-90.624 55.296-145.92s-19.456-101.888-52.224-140.288l27.648-24.576c16.384 8.192 33.28 13.824 52.224 13.824 63.488 0 115.712-52.224 115.712-115.712s-52.224-115.712-115.712-115.712c-63.488 0-115.712 52.224-115.712 115.712 0 13.824 2.56 30.208 8.192 41.472l-35.84 30.208c-34.304-19.968-73.216-30.208-112.64-30.208-44.032 0-85.504 13.824-121.344 32.768l-41.472-52.224c0-5.632 2.56-13.824 2.56-19.456 0-40.96-32.768-74.24-73.728-74.24h-0.512c-40.96 0-74.24 32.768-74.24 73.728v0.512c0 40.96 32.768 74.24 73.728 74.24H262.656L306.688 332.8c-27.648 38.4-46.592 85.504-46.592 134.656 0 35.84 8.192 71.68 24.576 101.888l-35.84 30.208c-8.192-5.632-19.456-5.632-30.208-5.632-49.664 0-90.624 41.472-90.624 90.624 0 49.664 41.472 90.624 90.624 90.624s90.624-41.472 90.624-90.624c0-8.192 0-16.384-2.56-24.576l30.208-27.648c41.472 35.84 93.696 57.856 154.112 57.856 33.28 0 63.488-5.632 90.624-19.456l46.592 57.856c-11.264 19.456-19.456 44.032-19.456 68.608 0 77.312 63.488 140.288 143.36 140.288s143.36-63.488 143.36-140.288c0.512-75.776-65.536-139.264-145.408-139.264z m-261.632-11.264c-99.328 0-181.76-79.872-181.76-178.688C306.688 368.64 389.12 289.28 488.448 289.28s181.76 79.872 181.76 178.688c0 99.328-82.432 178.688-181.76 178.688zM430.592 465.408c0.512 18.432-13.824 33.28-32.256 33.792-18.432 0.512-33.28-13.824-33.792-32.256v-1.536c0-18.432 14.848-32.768 33.28-32.768 18.432-0.512 32.768 14.336 32.768 32.768z m88.064 0c0 18.432-14.848 33.28-32.768 33.28-18.432 0-33.28-14.848-33.28-32.768 0-18.432 14.848-32.768 33.28-32.768s32.768 13.824 32.768 32.256z m91.136 0c0 18.432-14.848 33.28-32.768 33.28s-33.28-14.848-33.28-32.768c0-18.432 14.848-32.768 33.28-32.768 17.92-1.024 32.768 13.824 32.768 32.256z"></path></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 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 t="1685973573331" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2708">
<path d="M138 573.8V450.2c48.2-12.1 84-55.8 84-107.7 0-51.9-35.8-95.6-84-107.7V68.1c0-14.9-12.1-27-27-27s-27 12.1-27 27v166.7C35.8 246.9 0 290.6 0 342.5c0 51.9 35.8 95.6 84 107.7v123.7c-48.2 12-84 55.7-84 107.6s35.8 95.6 84 107.7v166.7c0 14.9 12.1 27 27 27s27-12.1 27-27V789.2c48.2-12.1 84-55.8 84-107.7s-35.8-95.6-84-107.7zM60 342.5c0-28.1 22.9-51 51-51s51 22.9 51 51-22.9 51-51 51-51-22.9-51-51z m51 390c-28.1 0-51-22.9-51-51s22.9-51 51-51 51 22.9 51 51-22.9 51-51 51zM942 283H352c-16.6 0-30-13.4-30-30s13.4-30 30-30h590c16.6 0 30 13.4 30 30s-13.4 30-30 30zM771.4 457H347.6c-14.2 0-25.6-11.5-25.6-25.6v-8.7c0-14.2 11.5-25.6 25.6-25.6h423.7c14.2 0 25.6 11.5 25.6 25.6v8.7c0.1 14.1-11.4 25.6-25.5 25.6z" p-id="2709"></path><path d="M942 625H352c-16.6 0-30-13.4-30-30s13.4-30 30-30h590c16.6 0 30 13.4 30 30s-13.4 30-30 30zM771.4 799H347.6c-14.2 0-25.6-11.5-25.6-25.6v-8.7c0-14.2 11.5-25.6 25.6-25.6h423.7c14.2 0 25.6 11.5 25.6 25.6v8.7c0.1 14.1-11.4 25.6-25.5 25.6z" p-id="2710"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@ -27,7 +27,13 @@ limitations under the License. -->
:remote-method="remoteMethod"
:filterable="filterable"
>
<el-option v-for="item in options" :key="item.value || ''" :label="item.label || ''" :value="item.value || ''">
<el-option
v-for="item in options"
:key="item.value || ''"
:label="item.label || ''"
:value="item.value || ''"
:disabled="item.disabled || false"
>
</el-option>
</el-select>
</template>

View File

@ -33,20 +33,37 @@ export const createEBPFTask = {
}`,
};
export const queryEBPFTasks = {
variable: "$serviceId: ID, $serviceInstanceId: ID, $targets: [EBPFProfilingTargetType!]",
variable:
"$serviceId: ID, $serviceInstanceId: ID, $targets: [EBPFProfilingTargetType!], $triggerType: EBPFProfilingTriggerType",
query: `
queryEBPFTasks: queryEBPFProfilingTasks(serviceId: $serviceId, serviceInstanceId: $serviceInstanceId, targets: $targets) {
queryEBPFTasks: queryEBPFProfilingTasks(serviceId: $serviceId, serviceInstanceId: $serviceInstanceId, targets: $targets, triggerType: $triggerType) {
taskId
serviceName
serviceId
serviceInstanceId
serviceInstanceName
processLabels
processName
processId
taskStartTime
triggerType
fixedTriggerDuration
targetType
createTime
continuousProfilingCauses {
type
singleValue {
threshold
current
}
uri {
uriRegex
uriPath
threshold
current
}
message
}
}`,
};
export const queryEBPFSchedules = {
@ -111,3 +128,26 @@ export const keepNetworkProfiling = {
errorReason
}`,
};
export const monitoringInstances = {
variable: "$serviceId: ID!, $target: ContinuousProfilingTargetType!",
query: `
instances: queryContinuousProfilingMonitoringInstances(serviceId: $serviceId, target: $target) {
id
name
attributes {
name
value
}
triggeredCount
lastTriggerTimestamp
processes {
id
name
detectType
labels
lastTriggerTimestamp
triggeredCount
}
}`,
};

View File

@ -123,3 +123,29 @@ export const GetProfileTaskLogs = {
}
`,
};
export const GetStrategyList = {
variable: "$serviceId: ID!",
query: `
strategyList: queryContinuousProfilingServiceTargets(serviceId: $serviceId) {
type
checkItems {
type
threshold
period
count
uriList
uriRegex
}
}
`,
};
export const EditStrategy = {
variable: "$request: ContinuousProfilingPolicyCreation!",
query: `
strategy: setContinuousProfilingPolicy(request: $request) {
errorReason
status
}
`,
};

View File

@ -23,6 +23,7 @@ import {
analysisEBPFResult,
createNetworkProfiling,
keepNetworkProfiling,
monitoringInstances,
} from "../fragments/ebpf";
export const getCreateTaskData = `query queryCreateTaskData(${queryCreateTaskData.variable}) {${queryCreateTaskData.query}}`;
@ -38,3 +39,5 @@ export const getEBPFResult = `query analysisEBPFResult(${analysisEBPFResult.vari
export const newNetworkProfiling = `mutation createNetworkProfiling(${createNetworkProfiling.variable}) {${createNetworkProfiling.query}}`;
export const aliveNetworkProfiling = `mutation keepNetworkProfiling(${keepNetworkProfiling.variable}) {${keepNetworkProfiling.query}}`;
export const getMonitoringInstances = `query continuousProfilingMonitoringInstances(${monitoringInstances.variable}) {${monitoringInstances.query}}`;

View File

@ -21,6 +21,8 @@ import {
GetProfileTaskSegmentList,
GetProfileAnalyze,
GetProfileTaskLogs,
GetStrategyList,
EditStrategy,
} from "../fragments/profile";
export const saveProfileTask = `mutation createProfileTask(${CreateProfileTask.variable}) {${CreateProfileTask.query}}`;
@ -34,3 +36,7 @@ export const getProfileTaskSegmentList = `query getProfileTaskSegmentList(${GetP
export const getProfileAnalyze = `query getProfileAnalyze(${GetProfileAnalyze.variable}) {${GetProfileAnalyze.query}}`;
export const getProfileTaskLogs = `query profileTaskLogs(${GetProfileTaskLogs.variable}) {${GetProfileTaskLogs.query}}`;
export const getStrategyList = `query getStrategyList(${GetStrategyList.variable}) {${GetStrategyList.query}}`;
export const editStrategy = `mutation editStrategy(${EditStrategy.variable}) {${EditStrategy.query}}`;

View File

@ -22,6 +22,7 @@ export enum MetricQueryTypes {
READHEATMAP = "readHeatMap",
ReadSampledRecords = "readSampledRecords",
ReadRecords = "readRecords",
ReadNullableMetricsValue = "readNullableMetricsValue",
}
export enum Calculations {
@ -70,10 +71,14 @@ export const RespFields: Indexable = {
readMetricsValues: `{
label
values {
values {value}
values {value isEmptyValue}
}
}`,
readMetricsValue: "",
readMetricsValue: ``,
readNullableMetricsValue: `{
value
isEmptyValue
}`,
sortMetrics: `{
name
id
@ -83,7 +88,7 @@ export const RespFields: Indexable = {
readLabeledMetricsValues: `{
label
values {
values {value}
values {value isEmptyValue}
}
}`,
readHeatMap: `{
@ -107,4 +112,21 @@ export const RespFields: Indexable = {
value
refId
}`,
execExpression: `{
type
results {
metric {
labels {
key
value
}
}
values {
name: id
value
refId: traceID
}
}
error
}`,
};

View File

@ -0,0 +1,313 @@
/**
* 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 { RespFields } from "./data";
import { ExpressionResultType } from "@/views/dashboard/data";
import { ElMessage } from "element-plus";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import { useAppStoreWithOut } from "@/store/modules/app";
import type { MetricConfigOpt } from "@/types/dashboard";
import type { Instance, Endpoint, Service } from "@/types/selector";
export async function useExpressionsQueryProcessor(config: Indexable) {
function expressionsGraphqlPods() {
if (!(config.metrics && config.metrics[0])) {
return;
}
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore();
if (!selectorStore.currentService && dashboardStore.entity !== "All") {
return;
}
const conditions: Recordable = {
duration: appStore.durationTime,
};
const variables: string[] = [`$duration: Duration!`];
const isRelation = ["ServiceRelation", "ServiceInstanceRelation", "EndpointRelation", "ProcessRelation"].includes(
dashboardStore.entity,
);
if (isRelation && !selectorStore.currentDestService) {
return;
}
const fragment = config.metrics.map((name: string, index: number) => {
variables.push(`$expression${index}: String!`, `$entity${index}: Entity!`);
conditions[`expression${index}`] = name;
const entity = {
serviceName: dashboardStore.entity === "All" ? undefined : selectorStore.currentService.value,
normal: dashboardStore.entity === "All" ? undefined : selectorStore.currentService.normal,
serviceInstanceName: ["ServiceInstance", "ServiceInstanceRelation", "ProcessRelation", "Process"].includes(
dashboardStore.entity,
)
? selectorStore.currentPod && selectorStore.currentPod.value
: undefined,
endpointName: dashboardStore.entity.includes("Endpoint")
? selectorStore.currentPod && selectorStore.currentPod.value
: undefined,
processName: dashboardStore.entity.includes("Process")
? selectorStore.currentProcess && selectorStore.currentProcess.value
: undefined,
destNormal: isRelation ? selectorStore.currentDestService.normal : undefined,
destServiceName: isRelation ? selectorStore.currentDestService.value : undefined,
destServiceInstanceName: ["ServiceInstanceRelation", "ProcessRelation"].includes(dashboardStore.entity)
? selectorStore.currentDestPod && selectorStore.currentDestPod.value
: undefined,
destEndpointName:
dashboardStore.entity === "EndpointRelation"
? selectorStore.currentDestPod && selectorStore.currentDestPod.value
: undefined,
destProcessName: dashboardStore.entity.includes("ProcessRelation")
? selectorStore.currentDestProcess && selectorStore.currentDestProcess.value
: undefined,
};
conditions[`entity${index}`] = entity;
return `expression${index}: execExpression(expression: $expression${index}, entity: $entity${index}, duration: $duration)${RespFields.execExpression}`;
});
const queryStr = `query queryData(${variables}) {${fragment}}`;
return {
queryStr,
conditions,
};
}
function expressionsSource(resp: { errors: string; data: Indexable }) {
if (resp.errors) {
ElMessage.error(resp.errors);
return { source: {}, tips: [], typesOfMQE: [] };
}
if (!resp.data) {
ElMessage.error("The query is wrong");
return { source: {}, tips: [], typesOfMQE: [] };
}
const tips: string[] = [];
const source: { [key: string]: unknown } = {};
const keys = Object.keys(resp.data);
const typesOfMQE: string[] = [];
for (let i = 0; i < config.metrics.length; i++) {
const c: MetricConfigOpt = (config.metricConfig && config.metricConfig[i]) || {};
const obj = resp.data[keys[i]] || {};
const results = obj.results || [];
const name = config.metrics[i];
const type = obj.type;
tips.push(obj.error);
typesOfMQE.push(type);
if (!obj.error) {
if (type === ExpressionResultType.TIME_SERIES_VALUES) {
if (results.length === 1) {
source[c.label || name] = results[0].values.map((d: { value: unknown }) => d.value) || [];
} else {
const labels = (c.label || "").split(",").map((item: string) => item.replace(/^\s*|\s*$/g, ""));
for (const item of results) {
const values = item.values.map((d: { value: unknown }) => d.value) || [];
const index = item.metric.labels[0].value;
const indexNum = labels.findIndex((_, i: number) => i === Number(index));
if (labels[indexNum] && indexNum > -1) {
source[labels[indexNum]] = values;
} else {
source[index] = values;
}
}
}
}
if (type === ExpressionResultType.SINGLE_VALUE) {
source[c.label || name] = (results[0].values[0] || {}).value;
}
if (([ExpressionResultType.RECORD_LIST, ExpressionResultType.SORTED_LIST] as string[]).includes(type)) {
source[name] = results[0].values;
}
}
}
return { source, tips, typesOfMQE };
}
const params = await expressionsGraphqlPods();
if (!params) {
return { source: {}, tips: [], typesOfMQE: [] };
}
const dashboardStore = useDashboardStore();
const json = await dashboardStore.fetchMetricValue(params);
if (json.errors) {
ElMessage.error(json.errors);
return { source: {}, tips: [], typesOfMQE: [] };
}
const data = expressionsSource(json);
return data;
}
export async function useExpressionsQueryPodsMetrics(
pods: Array<(Instance | Endpoint | Service) & Indexable>,
config: {
expressions: string[];
subExpressions: string[];
metricConfig: MetricConfigOpt[];
},
scope: string,
) {
function expressionsGraphqlPods() {
const metrics: string[] = [];
const subMetrics: string[] = [];
config.expressions = config.expressions || [];
config.subExpressions = config.subExpressions || [];
for (let i = 0; i < config.expressions.length; i++) {
if (config.expressions[i]) {
metrics.push(config.expressions[i]);
subMetrics.push(config.subExpressions[i]);
}
}
if (!metrics.length) {
return;
}
const appStore = useAppStoreWithOut();
const selectorStore = useSelectorStore();
const conditions: { [key: string]: unknown } = {
duration: appStore.durationTime,
};
const variables: string[] = [`$duration: Duration!`];
const currentService = selectorStore.currentService || {};
const fragmentList = pods.map((d: (Instance | Endpoint | Service) & Indexable, index: number) => {
const entity = {
serviceName: scope === "Service" ? d.label : currentService.label,
serviceInstanceName: scope === "ServiceInstance" ? d.label : undefined,
endpointName: scope === "Endpoint" ? d.label : undefined,
normal: scope === "Service" ? d.normal : currentService.normal,
};
variables.push(`$entity${index}: Entity!`);
conditions[`entity${index}`] = entity;
const f = metrics.map((name: string, idx: number) => {
variables.push(`$expression${index}${idx}: String!`);
conditions[`expression${index}${idx}`] = name;
let str = "";
if (config.subExpressions[idx]) {
variables.push(`$subExpression${index}${idx}: String!`);
conditions[`subExpression${index}${idx}`] = config.subExpressions[idx];
str = `subexpression${index}${idx}: execExpression(expression: $subExpression${index}${idx}, entity: $entity${index}, duration: $duration)${RespFields.execExpression}`;
}
return (
str +
`expression${index}${idx}: execExpression(expression: $expression${index}${idx}, entity: $entity${index}, duration: $duration)${RespFields.execExpression}`
);
});
return f;
});
const fragment = fragmentList.flat(1).join(" ");
const queryStr = `query queryData(${variables}) {${fragment}}`;
return { queryStr, conditions };
}
function expressionsPodsSource(resp: { errors: string; data: Indexable }): Indexable {
if (resp.errors) {
ElMessage.error(resp.errors);
return {};
}
const names: string[] = [];
const subNames: string[] = [];
const metricConfigArr: MetricConfigOpt[] = [];
const metricTypesArr: string[] = [];
const expressionsTips: string[] = [];
const subExpressionsTips: string[] = [];
const data = pods.map((d: any, idx: number) => {
for (let index = 0; index < config.expressions.length; index++) {
const c: MetricConfigOpt = (config.metricConfig && config.metricConfig[index]) || {};
const k = "expression" + idx + index;
const sub = "subexpression" + idx + index;
const obj = resp.data[k] || {};
const results = obj.results || [];
const typesOfMQE = obj.type || "";
const subObj = resp.data[sub] || {};
const subResults = subObj.results || [];
expressionsTips.push(obj.error);
subExpressionsTips.push(subObj.error);
if (results.length > 1) {
const labels = (c.label || "").split(",").map((item: string) => item.replace(/^\s*|\s*$/g, ""));
const labelsIdx = (c.labelsIndex || "").split(",").map((item: string) => item.replace(/^\s*|\s*$/g, ""));
for (let i = 0; i < results.length; i++) {
let name = results[i].metric.labels[0].value || "";
const subValues = subResults[i] && subResults[i].values.map((d: { value: unknown }) => d.value);
const num = labelsIdx.findIndex((d: string) => d === results[i].metric.labels[0].value);
if (labels[num]) {
name = labels[num];
}
if (!d[name]) {
d[name] = {};
}
if (subValues) {
d[name]["values"] = subValues;
}
d[name]["avg"] = (results[i].values[0] || {}).value;
const j = names.find((d: string) => d === name);
if (!j) {
names.push(name);
metricConfigArr.push({ ...c, index: i });
metricTypesArr.push(typesOfMQE);
}
}
} else {
if (!results[0]) {
return d;
}
const name = config.expressions[index] || "";
const subName = config.subExpressions[index] || "";
if (!d[name]) {
d[name] = {};
}
d[name]["avg"] = [(results[0].values[0] || {}).value];
if (subResults[0]) {
if (!d[subName]) {
d[subName] = {};
}
d[subName]["values"] = subResults[0].values.map((d: { value: number }) => d.value);
}
const j = names.find((d: string) => d === name);
if (!j) {
names.push(name);
subNames.push(subName);
metricConfigArr.push(c);
metricTypesArr.push(typesOfMQE);
}
}
}
return d;
});
return { data, names, subNames, metricConfigArr, metricTypesArr, expressionsTips, subExpressionsTips };
}
const dashboardStore = useDashboardStore();
const params = await expressionsGraphqlPods();
const json = await dashboardStore.fetchMetricValue(params);
if (json.errors) {
ElMessage.error(json.errors);
return {};
}
const expressionParams = expressionsPodsSource(json);
return expressionParams;
}

View File

@ -15,7 +15,15 @@
* limitations under the License.
*/
import { MetricQueryTypes, Calculations } from "./data";
export function useListConfig(config: Indexable, index: string) {
import { MetricModes } from "@/views/dashboard/data";
export function useListConfig(config: Indexable, index: number) {
if (config.metricModes === MetricModes.Expression) {
return {
isLinear: false,
isAvg: true,
};
}
const i = Number(index);
const types = [Calculations.Average, Calculations.ApdexAvg, Calculations.PercentageAvg];
const calculation = config.metricConfig && config.metricConfig[i] && config.metricConfig[i].calculation;
@ -25,6 +33,7 @@ export function useListConfig(config: Indexable, index: string) {
const isAvg =
[MetricQueryTypes.ReadMetricsValues, MetricQueryTypes.ReadLabeledMetricsValues].includes(config.metricTypes[i]) &&
types.includes(calculation);
return {
isLinear,
isAvg,

View File

@ -22,7 +22,6 @@ import { useSelectorStore } from "@/store/modules/selectors";
import { useAppStoreWithOut } from "@/store/modules/app";
import type { Instance, Endpoint, Service } from "@/types/selector";
import type { MetricConfigOpt } from "@/types/dashboard";
import { MetricCatalog } from "@/views/dashboard/data";
export function useQueryProcessor(config: Indexable) {
if (!(config.metrics && config.metrics[0])) {
@ -57,13 +56,11 @@ export function useQueryProcessor(config: Indexable) {
name,
parentService: ["All"].includes(dashboardStore.entity) ? null : selectorStore.currentService.value,
normal: selectorStore.currentService ? selectorStore.currentService.normal : true,
scope: config.catalog,
topN: c.topN || 10,
topN: Number(c.topN) || 10,
order: c.sortOrder || "DES",
};
} else {
const entity = {
scope: config.catalog,
serviceName: dashboardStore.entity === "All" ? undefined : selectorStore.currentService.value,
normal: dashboardStore.entity === "All" ? undefined : selectorStore.currentService.normal,
serviceInstanceName: ["ServiceInstance", "ServiceInstanceRelation", "ProcessRelation"].includes(
@ -95,11 +92,10 @@ export function useQueryProcessor(config: Indexable) {
conditions[`condition${index}`] = {
name,
parentEntity: entity,
topN: c.topN || 10,
topN: Number(c.topN) || 10,
order: c.sortOrder || "DES",
};
} else {
entity.scope = dashboardStore.entity;
if (metricType === MetricQueryTypes.ReadLabeledMetricsValues) {
const labels = (c.labelsIndex || "").split(",").map((item: string) => item.replace(/^\s*|\s*$/g, ""));
variables.push(`$labels${index}: [String!]!`);
@ -114,9 +110,10 @@ export function useQueryProcessor(config: Indexable) {
}
if (metricType === MetricQueryTypes.ReadLabeledMetricsValues) {
return `${name}${index}: ${metricType}(condition: $condition${index}, labels: $labels${index}, duration: $duration)${RespFields[metricType]}`;
} else {
return `${name}${index}: ${metricType}(condition: $condition${index}, duration: $duration)${RespFields[metricType]}`;
}
const t = metricType === MetricQueryTypes.ReadMetricsValue ? MetricQueryTypes.ReadNullableMetricsValue : metricType;
return `${name}${index}: ${t}(condition: $condition${index}, duration: $duration)${RespFields[t]}`;
});
const queryStr = `query queryData(${variables}) {${fragment}}`;
@ -156,7 +153,9 @@ export function useSourceProcessor(
const labels = (c.label || "").split(",").map((item: string) => item.replace(/^\s*|\s*$/g, ""));
const labelsIdx = (c.labelsIndex || "").split(",").map((item: string) => item.replace(/^\s*|\s*$/g, ""));
for (const item of resVal) {
const values = item.values.values.map((d: { value: number }) => aggregation(Number(d.value), c));
const values = item.values.values.map((d: { value: number; isEmptyValue: boolean }) =>
d.isEmptyValue ? NaN : aggregation(Number(d.value), c),
);
const indexNum = labelsIdx.findIndex((d: string) => d === item.label);
if (labels[indexNum] && indexNum > -1) {
source[labels[indexNum]] = values;
@ -166,7 +165,8 @@ export function useSourceProcessor(
}
}
if (type === MetricQueryTypes.ReadMetricsValue) {
source[m] = aggregation(Number(Object.values(resp.data)[0]), c);
const v = Object.values(resp.data)[0] || {};
source[m] = v.isEmptyValue ? NaN : aggregation(Number(v.value), c);
}
if (
(
@ -229,7 +229,6 @@ export function useQueryPodsMetrics(
const currentService = selectorStore.currentService || {};
const fragmentList = pods.map((d: (Instance | Endpoint | Service) & Indexable, index: number) => {
const param = {
scope,
serviceName: scope === "Service" ? d.label : currentService.label,
serviceInstanceName: scope === "ServiceInstance" ? d.label : undefined,
endpointName: scope === "Endpoint" ? d.label : undefined,
@ -250,7 +249,9 @@ export function useQueryPodsMetrics(
const labels = (c.labelsIndex || "").split(",").map((item: string) => item.replace(/^\s*|\s*$/g, ""));
conditions[`labels${index}${idx}`] = labels;
}
return `${name}${index}${idx}: ${metricType}(condition: $condition${index}${idx}, ${labelStr}duration: $duration)${RespFields[metricType]}`;
const t =
metricType === MetricQueryTypes.ReadMetricsValue ? MetricQueryTypes.ReadNullableMetricsValue : metricType;
return `${name}${index}${idx}: ${t}(condition: $condition${index}${idx}, ${labelStr}duration: $duration)${RespFields[t]}`;
});
return f;
});
@ -276,12 +277,13 @@ export function usePodsSource(
const names: string[] = [];
const metricConfigArr: MetricConfigOpt[] = [];
const metricTypesArr: string[] = [];
const data = pods.map((d: Instance & Indexable, idx: number) => {
const data = pods.map((d: any, idx: number) => {
config.metrics.map((name: string, index: number) => {
const c: any = (config.metricConfig && config.metricConfig[index]) || {};
const key = name + idx + index;
if (config.metricTypes[index] === MetricQueryTypes.ReadMetricsValue) {
d[name] = aggregation(resp.data[key], c);
const v = resp.data[key];
d[name] = v.isEmptyValue ? NaN : aggregation(v.value, c);
if (idx === 0) {
names.push(name);
metricConfigArr.push(c);
@ -293,7 +295,9 @@ export function usePodsSource(
if ([Calculations.Average, Calculations.ApdexAvg, Calculations.PercentageAvg].includes(c.calculation)) {
d[name]["avg"] = calculateExp(resp.data[key].values.values, c);
}
d[name]["values"] = resp.data[key].values.values.map((val: { value: number }) => aggregation(val.value, c));
d[name]["values"] = resp.data[key].values.values.map((val: { value: number; isEmptyValue: boolean }) =>
val.isEmptyValue ? NaN : aggregation(val.value, c),
);
if (idx === 0) {
names.push(name);
metricConfigArr.push(c);
@ -306,7 +310,9 @@ export function usePodsSource(
const labelsIdx = (c.labelsIndex || "").split(",").map((item: string) => item.replace(/^\s*|\s*$/g, ""));
for (let i = 0; i < resVal.length; i++) {
const item = resVal[i];
const values = item.values.values.map((d: { value: number }) => aggregation(Number(d.value), c));
const values = item.values.values.map((d: { value: number; isEmptyValue: boolean }) =>
d.isEmptyValue ? NaN : aggregation(Number(d.value), c),
);
const indexNum = labelsIdx.findIndex((d: string) => d === item.label);
let key = item.label;
if (labels[indexNum] && indexNum > -1) {
@ -356,8 +362,12 @@ export function useQueryTopologyMetrics(metrics: string[], ids: string[]) {
return { queryStr, conditions };
}
function calculateExp(arr: { value: number }[], config: { calculation?: string }): (number | string)[] {
const sum = arr.map((d: { value: number }) => d.value).reduce((a, b) => a + b);
export function calculateExp(
list: { value: number; isEmptyValue: boolean }[],
config: { calculation?: string },
): (number | string)[] {
const arr = list.filter((d: { value: number; isEmptyValue: boolean }) => !d.isEmptyValue);
const sum = arr.length ? arr.map((d: { value: number }) => Number(d.value)).reduce((a, b) => a + b) : 0;
let data: (number | string)[] = [];
switch (config.calculation) {
case Calculations.Average:
@ -370,7 +380,9 @@ function calculateExp(arr: { value: number }[], config: { calculation?: string }
data = [(sum / arr.length / 10000).toFixed(2)];
break;
default:
data = arr.map((d) => aggregation(d.value, config));
data = list.map((d: { value: number; isEmptyValue: boolean }) =>
d.isEmptyValue ? NaN : aggregation(d.value, config),
);
break;
}
return data;
@ -423,26 +435,3 @@ export function aggregation(val: number, config: { calculation?: string }): numb
return data;
}
export async function useGetMetricEntity(metric: string, metricType: string) {
if (!metric || !metricType) {
return;
}
let catalog = "";
const dashboardStore = useDashboardStore();
if (
([MetricQueryTypes.ReadSampledRecords, MetricQueryTypes.SortMetrics, MetricQueryTypes.ReadRecords] as any).includes(
metricType,
)
) {
const res = await dashboardStore.fetchMetricList(metric);
if (res.errors) {
ElMessage.error(res.errors);
return;
}
const c: string = res.data.metrics[0].catalog;
catalog = (MetricCatalog as Indexable)[c];
}
return catalog;
}

View File

@ -323,6 +323,7 @@ const msg = {
keywordsOfContentLogTips: "Current storage of SkyWalking OAP server does not support this.",
setEvent: "Set Event",
viewAttributes: "View",
attributes: "Attributes",
serviceEvents: "Service Events",
select: "Select",
eventID: "Event ID",
@ -385,5 +386,24 @@ const msg = {
AWSGateway: "AWS API Gateway",
APIGateway: "API Gateway",
redis: "Redis",
elasticsearch: "Elasticsearch",
mq: "MQ",
rabbitMQ: "RabbitMQ",
save: "Save",
editStrategy: "Edit Policies",
policyList: "Policy List",
targetTypes: "Target Type",
monitorType: "Monitor Type",
count: "Count",
threshold: "Threshold",
uriRegex: "URI Regex",
uriList: "URI List",
processes: "Processes",
monitorInstances: "Monitor Instances",
processDashboards: "Process Dashboards",
instanceDashboards: "Instance Dashboards",
detailLabel: "Detail Label",
summary: "Summary",
detail: "Detail",
};
export default msg;

View File

@ -384,5 +384,25 @@ const msg = {
AWSGateway: "AWS API Gateway",
APIGateway: "API Gateway",
redis: "Redis",
elasticsearch: "Elasticsearch",
mq: "MQ",
rabbitMQ: "RabbitMQ",
save: "Salvar",
editStrategy: "Estrategia editorial",
policyList: "Lista de políticas",
targetTypes: "Tipo de objetivo",
monitorType: "Tipo de Monitor",
count: "Contar",
threshold: "Umbral",
uriRegex: "Lista URI",
uriList: "Lista URI",
processes: "Proceso",
attributes: "Atributos",
monitorInstances: "Ejemplo de Monitor",
processDashboards: "Tablero de proceso",
instanceDashboards: "Tablero de ejemplo",
detailLabel: "Detail Label",
summary: "Summary",
detail: "Detail",
};
export default msg;

View File

@ -382,5 +382,25 @@ const msg = {
AWSGateway: "AWS API Gateway",
APIGateway: "API Gateway",
redis: "Redis",
elasticsearch: "Elasticsearch",
mq: "消息队列",
rabbitMQ: "RabbitMQ",
save: "保存",
editStrategy: "编辑策略",
policyList: "策略列表",
targetTypes: "目标类型",
monitorType: "监视器类型",
count: "总数",
threshold: "阈值",
uriRegex: "URI规则",
uriList: "URI列表",
processes: "进程",
attributes: "属性",
monitorInstances: "监视实例",
processDashboards: "进程仪表板",
instanceDashboards: "实例仪表板",
detailLabel: "详细标签",
summary: "概括",
detail: "详细",
};
export default msg;

View File

@ -128,6 +128,27 @@ export const routesDashboard: Array<RouteRecordRaw> = [
},
],
},
{
path: "",
redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name",
component: () => import("@/views/dashboard/Edit.vue"),
name: "ViewProcess",
meta: {
notShow: true,
},
children: [
{
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name",
component: () => import("@/views/dashboard/Edit.vue"),
name: "ViewProcess",
},
{
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name/tab/:activeTabIndex",
component: () => import("@/views/dashboard/Edit.vue"),
name: "ViewProcessActiveTabIndex",
},
],
},
{
path: "",
redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:destServiceId/:destPodId/:name",

View File

@ -90,6 +90,22 @@ export default [
layer: "REDIS",
},
},
{
path: "/elasticsearch",
name: "Elasticsearch",
meta: {
title: "elasticsearch",
layer: "ELASTICSEARCH",
},
},
{
path: "/elasticsearch/tab/:activeTabIndex",
name: "ElasticsearchActiveTabIndex",
meta: {
notShow: true,
layer: "ELASTICSEARCH",
},
},
],
},
];

View File

@ -24,6 +24,7 @@ import browser from "./browser";
import k8s from "./k8s";
import gateway from "./gateway";
import aws from "./aws";
import mq from "./mq";
export default [
...general,
@ -35,5 +36,6 @@ export default [
...browser,
...gateway,
...database,
...mq,
...selfObservability,
];

47
src/router/data/mq.ts Normal file
View File

@ -0,0 +1,47 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export default [
{
path: "",
name: "MQ",
meta: {
title: "mq",
icon: "mq",
hasGroup: true,
},
redirect: "/rabbitMQ",
children: [
{
path: "/rabbitMQ",
name: "RabbitMQ",
meta: {
title: "rabbitMQ",
layer: "RABBITMQ",
},
},
{
path: "/rabbitMQ/tab/:activeTabIndex",
name: "RabbitMQActiveTabIndex",
meta: {
notShow: true,
layer: "RABBITMQ",
},
},
],
},
];

View File

@ -38,4 +38,18 @@ export const TimeRangeConfig = {
text: "text",
};
export const ControlsTypes = ["Trace", "Profile", "Log", "DemandLog", "Ebpf", "NetworkProfiling", "ThirdPartyApp"];
export const ControlsTypes = [
"Trace",
"Profile",
"Log",
"DemandLog",
"Ebpf",
"NetworkProfiling",
"ThirdPartyApp",
"ContinuousProfiling",
"TaskTimeline",
];
export enum EBPFProfilingTriggerType {
FIXED_TIME = "FIXED_TIME",
CONTINUOUS_PROFILING = "CONTINUOUS_PROFILING",
}

View File

@ -0,0 +1,171 @@
/**
* 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 { StrategyItem, CheckItems } from "@/types/continous-profiling";
import type { EBPFTaskList, EBPFProfilingSchedule, AnalyzationTrees } from "@/types/ebpf";
import type { Instance } from "@/types/selector";
import { store } from "@/store";
import graphql from "@/graphql";
import type { MonitorInstance, MonitorProcess } from "@/types/continous-profiling";
import type { AxiosResponse } from "axios";
import { dateFormat } from "@/utils/dateFormat";
interface ContinousProfilingState {
strategyList: Array<Recordable<StrategyItem>>;
selectedStrategy: Recordable<StrategyItem>;
taskList: Array<Recordable<EBPFTaskList>>;
selectedTask: Recordable<EBPFTaskList>;
errorTip: string;
errorReason: string;
instances: Instance[];
instance: Nullable<Instance>;
eBPFSchedules: EBPFProfilingSchedule[];
currentSchedule: EBPFProfilingSchedule | Record<string, never>;
analyzeTrees: AnalyzationTrees[];
ebpfTips: string;
aggregateType: string;
instancesLoading: boolean;
policyLoading: boolean;
}
export const continousProfilingStore = defineStore({
id: "continousProfiling",
state: (): ContinousProfilingState => ({
strategyList: [],
selectedStrategy: {},
taskList: [],
selectedTask: {},
errorReason: "",
errorTip: "",
ebpfTips: "",
instances: [],
eBPFSchedules: [],
currentSchedule: {},
analyzeTrees: [],
aggregateType: "COUNT",
instance: null,
instancesLoading: false,
policyLoading: false,
}),
actions: {
setSelectedStrategy(task: Recordable<StrategyItem>) {
this.selectedStrategy = task || {};
},
setselectedTask(task: Recordable<EBPFTaskList>) {
this.selectedTask = task || {};
},
setCurrentSchedule(s: EBPFProfilingSchedule) {
this.currentSchedule = s;
},
setAnalyzeTrees(tree: AnalyzationTrees[]) {
this.analyzeTrees = tree;
},
setCurrentInstance(instance: Nullable<Instance>) {
this.instance = instance;
},
async setContinuousProfilingPolicy(
serviceId: string,
targets: {
targetType: string;
checkItems: CheckItems[];
}[],
) {
const res: AxiosResponse = await graphql.query("editStrategy").params({
request: {
serviceId,
targets,
},
});
if (res.data.errors) {
return res.data;
}
return res.data;
},
async getStrategyList(params: { serviceId: string }) {
if (!params.serviceId) {
return new Promise((resolve) => resolve({}));
}
this.policyLoading = true;
const res: AxiosResponse = await graphql.query("getStrategyList").params(params);
this.policyLoading = false;
if (res.data.errors) {
return res.data;
}
const list = res.data.data.strategyList || [];
if (!list.length) {
this.taskList = [];
this.instances = [];
this.instance = null;
}
const arr = list.length ? res.data.data.strategyList : [{ type: "", checkItems: [{ type: "" }] }];
this.strategyList = arr.map((d: StrategyItem, index: number) => {
return {
...d,
id: index,
};
});
this.setSelectedStrategy(this.strategyList[0]);
if (!this.selectedStrategy.type) {
return res.data;
}
this.getMonitoringInstances(params.serviceId);
return res.data;
},
async getMonitoringInstances(serviceId: string): Promise<Nullable<AxiosResponse>> {
this.instancesLoading = true;
if (!serviceId) {
return null;
}
const res: AxiosResponse = await graphql.query("getMonitoringInstances").params({
serviceId,
target: this.selectedStrategy.type,
});
this.instancesLoading = false;
if (res.data.errors) {
return res.data;
}
this.instances = (res.data.data.instances || [])
.map((d: MonitorInstance) => {
const processes = (d.processes || [])
.sort((c: MonitorProcess, d: MonitorProcess) => d.lastTriggerTimestamp - c.lastTriggerTimestamp)
.map((p: MonitorProcess) => {
return {
...p,
lastTriggerTime: d.lastTriggerTimestamp ? dateFormat(d.lastTriggerTimestamp) : "",
labels: p.labels.join("; "),
};
});
return {
...d,
processes,
lastTriggerTime: d.lastTriggerTimestamp ? dateFormat(d.lastTriggerTimestamp) : "",
};
})
.sort((a: MonitorInstance, b: MonitorInstance) => b.lastTriggerTimestamp - a.lastTriggerTimestamp);
this.instance = this.instances[0] || null;
return res.data;
},
},
});
export function useContinousProfilingStore(): Recordable {
return continousProfilingStore(store);
}

View File

@ -25,7 +25,7 @@ import { NewControl, TextConfig, TimeRangeConfig, ControlsTypes } from "../data"
import type { AxiosResponse } from "axios";
import { ElMessage } from "element-plus";
import { useI18n } from "vue-i18n";
import { EntityType } from "@/views/dashboard/data";
import { EntityType, MetricModes } from "@/views/dashboard/data";
interface DashboardState {
showConfig: boolean;
layout: LayoutConfig[];
@ -92,6 +92,10 @@ export const dashboardStore = defineStore({
metricTypes: [""],
metrics: [""],
};
if (type === "Widget") {
newItem.metricMode = MetricModes.Expression;
}
if (type === "Tab") {
newItem.h = 36;
newItem.activedTabIndex = 0;
@ -167,6 +171,9 @@ export const dashboardStore = defineStore({
metricTypes: [""],
metrics: [""],
};
if (type === "Widget") {
newItem.metricMode = MetricModes.Expression;
}
if (type === "Topology") {
newItem.h = 32;
newItem.graph = {
@ -309,6 +316,11 @@ export const dashboardStore = defineStore({
return res.data;
},
async getTypeOfMQE(expression: string) {
const res: AxiosResponse = await graphql.query("getTypeOfMQE").params({ expression });
return res.data;
},
async fetchMetricList(regex: string) {
const res: AxiosResponse = await graphql.query("queryMetrics").params({ regex });

View File

@ -20,6 +20,7 @@ import type { EBPFTaskCreationRequest, EBPFProfilingSchedule, EBPFTaskList, Anal
import { store } from "@/store";
import graphql from "@/graphql";
import type { AxiosResponse } from "axios";
import { EBPFProfilingTriggerType } from "../data";
interface EbpfState {
taskList: Array<Recordable<EBPFTaskList>>;
eBPFSchedules: EBPFProfilingSchedule[];
@ -27,7 +28,7 @@ interface EbpfState {
analyzeTrees: AnalyzationTrees[];
labels: Option[];
couldProfiling: boolean;
tip: string;
ebpfTips: string;
selectedTask: Recordable<EBPFTaskList>;
aggregateType: string;
}
@ -41,7 +42,7 @@ export const ebpfStore = defineStore({
analyzeTrees: [],
labels: [{ value: "", label: "" }],
couldProfiling: false,
tip: "",
ebpfTips: "",
selectedTask: {},
aggregateType: "COUNT",
}),
@ -77,6 +78,7 @@ export const ebpfStore = defineStore({
this.getTaskList({
serviceId: param.serviceId,
targets: ["ON_CPU", "OFF_CPU"],
triggerType: EBPFProfilingTriggerType.FIXED_TIME,
});
return res.data;
},
@ -86,7 +88,7 @@ export const ebpfStore = defineStore({
}
const res: AxiosResponse = await graphql.query("getEBPFTasks").params(params);
this.tip = "";
this.ebpfTips = "";
if (res.data.errors) {
return res.data;
}
@ -103,13 +105,14 @@ export const ebpfStore = defineStore({
if (!params.taskId) {
return new Promise((resolve) => resolve({}));
}
const res: AxiosResponse = await graphql.query("getEBPFSchedules").params({ ...params });
if (res.data.errors) {
this.eBPFSchedules = [];
return res.data;
}
this.tip = "";
this.ebpfTips = "";
const { eBPFSchedules } = res.data.data;
this.eBPFSchedules = eBPFSchedules;
@ -138,7 +141,7 @@ export const ebpfStore = defineStore({
return res.data;
}
const { analysisEBPFResult } = res.data.data;
this.tip = analysisEBPFResult.tip;
this.ebpfTips = analysisEBPFResult.tip;
if (!analysisEBPFResult) {
this.analyzeTrees = [];
return res.data;

View File

@ -65,6 +65,12 @@ export const networkProfilingStore = defineStore({
setLink(link: Call) {
this.call = link;
},
seNodes(nodes: Node[]) {
this.nodes = nodes;
},
setLinks(links: Call[]) {
this.calls = links;
},
setMetricsLayout(layout: LayoutConfig[]) {
this.metricsLayout = layout;
},

View File

@ -0,0 +1,134 @@
/**
* 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 { ElMessage } from "element-plus";
import { store } from "@/store";
import graphql from "@/graphql";
import type { AxiosResponse } from "axios";
import { useAppStoreWithOut } from "@/store/modules/app";
import type { EBPFTaskList } from "@/types/ebpf";
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
import { useSelectorStore } from "@/store/modules/selectors";
import { useEbpfStore } from "@/store/modules/ebpf";
import dateFormatStep from "@/utils/dateFormat";
import getLocalTime from "@/utils/localtime";
import { TargetTypes } from "@/views/dashboard/related/continuous-profiling/data";
interface taskTimelineState {
loading: boolean;
taskList: EBPFTaskList[];
selectedTask: Recordable<EBPFTaskList>;
}
export const taskTimelineStore = defineStore({
id: "taskTimeline",
state: (): taskTimelineState => ({
loading: false,
taskList: [],
selectedTask: {},
}),
actions: {
setSelectedTask(task: Recordable<EBPFTaskList>) {
this.selectedTask = task || {};
},
setTaskList(list: EBPFTaskList[]) {
this.taskList = list;
},
async getContinousTaskList(params: {
serviceId: string;
serviceInstanceId: string;
targets: string[];
triggerType: string;
}) {
if (!params.serviceId) {
return new Promise((resolve) => resolve({}));
}
this.loading = true;
const res: AxiosResponse = await graphql.query("getEBPFTasks").params(params);
this.loading = false;
this.errorTip = "";
if (res.data.errors) {
return res.data;
}
const selectorStore = useSelectorStore();
this.taskList = (res.data.data.queryEBPFTasks || []).filter(
(d: EBPFTaskList) => selectorStore.currentProcess && d.processId === selectorStore.currentProcess.id,
);
// this.selectedTask = this.taskList[0] || {};
// await this.getGraphData();
return res.data;
},
async getGraphData() {
let res: any = {};
if (this.selectedTask.targetType === TargetTypes[2].value) {
res = await this.getTopology();
} else {
const ebpfStore = useEbpfStore();
res = await ebpfStore.getEBPFSchedules({
taskId: this.selectedTask.taskId,
});
}
if (res.errors) {
ElMessage.error(res.errors);
}
},
async getTopology() {
const networkProfilingStore = useNetworkProfilingStore();
const appStore = useAppStoreWithOut();
const selectorStore = useSelectorStore();
networkProfilingStore.setSelectedNetworkTask(this.selectedTask);
const { taskStartTime, fixedTriggerDuration } = this.selectedTask;
const startTime =
fixedTriggerDuration > 1800 ? taskStartTime + fixedTriggerDuration * 1000 - 30 * 60 * 1000 : taskStartTime;
let endTime = taskStartTime + fixedTriggerDuration * 1000;
if (taskStartTime + fixedTriggerDuration * 1000 > new Date().getTime()) {
endTime = new Date().getTime();
}
const resp = await networkProfilingStore.getProcessTopology({
serviceInstanceId: (selectorStore.currentPod || {}).id || "",
duration: {
start: dateFormatStep(getLocalTime(appStore.utc, new Date(startTime)), appStore.duration.step, true),
end: dateFormatStep(getLocalTime(appStore.utc, new Date(endTime)), appStore.duration.step, true),
step: appStore.duration.step,
},
});
if (resp.errors) {
ElMessage.error(resp.errors);
}
return resp;
},
async preAnalyzeTask() {
if (this.selectedStrategy.type === "NETWORK") {
const networkProfilingStore = useNetworkProfilingStore();
await networkProfilingStore.setSelectedNetworkTask(this.selectedTask);
return;
}
const res = await this.getEBPFSchedules({
taskId: this.selectedTask.taskId,
});
if (res.errors) {
ElMessage.error(res.errors);
}
},
},
});
export function useTaskTimelineStore(): Recordable {
return taskTimelineStore(store);
}

View File

@ -216,3 +216,6 @@ div:has(> a.menu-title) {
.el-breadcrumb {
line-height: 28px;
}
.el-input-number .el-input__inner {
text-align: left !important;
}

3
src/types/app.d.ts vendored
View File

@ -42,8 +42,9 @@ export type EventParams = {
dataIndex: number;
data: unknown;
dataType: string;
value: number | Array;
value: number | any[];
color: string;
event: Record<string, T>;
dataIndex: number;
event: any;
};

View File

@ -24,7 +24,6 @@ declare module '@vue/runtime-core' {
ElMenuItemGroup: typeof import('element-plus/es')['ElMenuItemGroup']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']

45
src/types/continous-profiling.d.ts vendored Normal file
View File

@ -0,0 +1,45 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface StrategyItem {
type: string;
checkItems: CheckItems[];
}
export type CheckItems = {
type: string;
threshold: string;
period: number;
count: number;
uriList?: string[];
uriRegex?: string;
};
export interface MonitorInstance {
id: string;
name: string;
attributes: { name: string; value: string }[];
triggeredCount: number;
lastTriggerTimestamp: number;
processes: MonitorProcess[];
}
interface MonitorProcess {
id: string;
name: string;
detectType: string;
labels: string[];
lastTriggerTimestamp: number;
triggeredCount: number;
}

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DurationTime } from "./app";
export type DashboardItem = {
id?: string;
entity: string;
@ -28,11 +28,14 @@ export interface LayoutConfig {
w: number;
h: number;
i: string;
type: string;
metricMode?: string;
widget?: WidgetConfig;
graph?: GraphConfig;
metrics?: string[];
type: string;
expressions?: string[];
metricTypes?: string[];
typesOfMQE?: string[];
children?: { name: string; children: LayoutConfig[] }[];
activedTabIndex?: number;
metricConfig?: MetricConfigOpt[];
@ -41,6 +44,8 @@ export interface LayoutConfig {
eventAssociate?: boolean;
filters?: Filters;
relatedTrace?: RelatedTrace;
subExpressions?: string[];
subTypesOfMQE?: string[];
}
export type RelatedTrace = {
duration: DurationTime;
@ -70,10 +75,11 @@ export type MetricConfigOpt = {
unit?: string;
label?: string;
calculation?: string;
labelsIndex: string;
sortOrder: string;
labelsIndex?: string;
sortOrder?: string;
topN?: number;
index?: number;
detailLabel?: string;
};
export interface WidgetConfig {

19
src/types/ebpf.d.ts vendored
View File

@ -28,12 +28,31 @@ export interface EBPFTaskList {
taskId: string;
serviceName: string;
serviceId: string;
serviceInstanceId: string;
serviceInstanceName: string;
processId: string;
processName: string;
processLabels: string[];
taskStartTime: number;
fixedTriggerDuration: number;
targetType: string;
createTime: number;
triggerType: string;
continuousProfilingCauses: ProfilingCause[];
}
interface ProfilingCause {
type: string;
singleValue: {
threshold: number;
current: number;
};
uri: {
uriRegex: string;
uriPath: string;
threshold: number;
current: number;
};
}
export interface EBPFProfilingSchedule {

View File

@ -21,6 +21,7 @@ export type Service = {
layers?: string[];
normal?: boolean;
group?: string;
merge?: string;
};
export type Instance = {
@ -30,12 +31,14 @@ export type Instance = {
instanceUUID?: string;
attributes?: { name: string; value: string }[];
id?: string;
merge?: boolean;
};
export type Endpoint = {
id?: string;
label: string;
value: string;
merge?: string;
};
export type Service = {

View File

@ -35,6 +35,11 @@ limitations under the License. -->
metrics: config.metrics,
metricTypes: config.metricTypes,
metricConfig: config.metricConfig,
metricMode: config.metricMode,
expressions: config.expressions || [],
typesOfMQE: typesOfMQE || [],
subExpressions: config.subExpressions || [],
subTypesOfMQE: config.subTypesOfMQE || [],
}"
:needQuery="true"
/>
@ -51,10 +56,12 @@ limitations under the License. -->
import { useRoute } from "vue-router";
import { useSelectorStore } from "@/store/modules/selectors";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useQueryProcessor, useSourceProcessor, useGetMetricEntity } from "@/hooks/useMetricsProcessor";
import { useQueryProcessor, useSourceProcessor } from "@/hooks/useMetricsProcessor";
import { useExpressionsQueryProcessor } from "@/hooks/useExpressionsProcessor";
import graphs from "./graphs";
import { EntityType } from "./data";
import timeFormat from "@/utils/timeFormat";
import { MetricModes } from "./data";
export default defineComponent({
name: "WidgetPage",
@ -73,6 +80,7 @@ limitations under the License. -->
const dashboardStore = useDashboardStore();
const title = computed(() => (config.value.widget && config.value.widget.title) || "");
const tips = computed(() => (config.value.widget && config.value.widget.tips) || "");
const typesOfMQE = ref<string[]>([]);
init();
async function init() {
@ -122,10 +130,21 @@ limitations under the License. -->
}
}
async function queryMetrics() {
const metricTypes = config.value.metricTypes || [];
const metrics = config.value.metrics || [];
const catalog = await useGetMetricEntity(metrics[0], metricTypes[0]);
const params = await useQueryProcessor({ ...config.value, catalog });
const isExpression = config.value.metricMode === MetricModes.Expression;
if (isExpression) {
loading.value = true;
const params = await useExpressionsQueryProcessor({
metrics: config.value.expressions || [],
metricConfig: config.value.metricConfig || [],
subExpressions: config.value.subExpressions || [],
});
loading.value = false;
source.value = params.source || {};
typesOfMQE.value = params.typesOfMQE;
return;
}
const params = await useQueryProcessor({ ...config.value });
if (!params) {
source.value = {};
return;
@ -141,7 +160,7 @@ limitations under the License. -->
metricTypes: config.value.metricTypes || [],
metricConfig: config.value.metricConfig || [],
};
source.value = useSourceProcessor(json, d);
source.value = await useSourceProcessor(json, d);
}
watch(
() => appStoreWithOut.durationTime,
@ -157,6 +176,7 @@ limitations under the License. -->
config,
title,
tips,
typesOfMQE,
};
},
});
@ -171,7 +191,7 @@ limitations under the License. -->
.widget-chart {
background: #fff;
box-shadow: 0px 1px 4px 0px #00000029;
box-shadow: 0 1px 4px 0 #00000029;
border-radius: 3px;
padding: 5px;
width: 100%;

View File

@ -59,6 +59,7 @@ limitations under the License. -->
import copy from "@/utils/copy";
import { RefreshOptions } from "@/views/dashboard/data";
import { TimeType } from "@/constants/data";
import { MetricModes } from "../data";
const { t } = useI18n();
const appStore = useAppStoreWithOut();
@ -91,7 +92,8 @@ limitations under the License. -->
step: appStore.durationRow.step,
utc: appStore.utc,
});
const { widget, graph, metrics, metricTypes, metricConfig } = dashboardStore.selectedGrid;
const { widget, graph, metrics, metricTypes, metricConfig, metricMode, expressions, typesOfMQE, subExpressions } =
dashboardStore.selectedGrid;
const c = (metricConfig || []).map((d: any) => {
const t: any = {};
if (d.label) {
@ -105,11 +107,20 @@ limitations under the License. -->
const opt: any = {
type: dashboardStore.selectedGrid.type,
graph: graph,
metrics: metrics,
metricTypes: metricTypes,
metricMode,
metricConfig: c,
height: dashboardStore.selectedGrid.h * 20 + 60,
};
if (metricMode === MetricModes.Expression) {
opt.expressions = expressions;
opt.typesOfMQE = typesOfMQE;
if (subExpressions && subExpressions.length) {
opt.subExpressions = subExpressions;
}
} else {
opt.metrics = metrics;
opt.metricTypes = metricTypes;
}
if (widget) {
opt.widget = {
title: encodeURIComponent(widget.title || ""),

View File

@ -0,0 +1,108 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="item">
<div>{{ t("instanceDashboards") }}</div>
<Selector
:value="instanceDashboardName || ''"
:options="instanceDashboards"
size="small"
placeholder="Please select a dashboard name"
@change="changeDashboard({ instanceDashboardName: $event[0].value })"
class="selectors"
:clearable="true"
/>
</div>
<div class="item">
<div>{{ t("processDashboards") }}</div>
<Selector
:value="processDashboardName || ''"
:options="processDashboards"
size="small"
placeholder="Please select a dashboard name"
@change="changeDashboard({ processDashboardName: $event[0].value })"
class="selectors"
:clearable="true"
/>
</div>
<div class="footer">
<el-button size="small">
{{ t("cancel") }}
</el-button>
<el-button size="small" type="primary" @click="applyConfig">
{{ t("apply") }}
</el-button>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import { EntityType } from "../data";
import type { DashboardItem } from "@/types/dashboard";
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const instanceDashboardName = ref<boolean>(dashboardStore.selectedGrid.instanceDashboardName);
const processDashboardName = ref<number>(dashboardStore.selectedGrid.processDashboardName);
const list = JSON.parse(sessionStorage.getItem("dashboards") || "[]");
const instanceDashboards: Array<DashboardItem & { label: string; value: string }> = [];
const processDashboards: Array<DashboardItem & { label: string; value: string }> = [];
for (const item of list) {
if (item.layer === dashboardStore.layerId) {
const i = {
...item,
label: item.name,
value: item.name,
};
if (item.entity === EntityType[8].value) {
processDashboards.push(i);
}
if (item.entity === EntityType[3].value) {
instanceDashboards.push(i);
}
}
}
function applyConfig() {
dashboardStore.setConfigs(dashboardStore.selectedGrid);
dashboardStore.setConfigPanel(false);
}
function changeDashboard(param: { [key: string]: unknown }) {
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
...param,
});
}
</script>
<style lang="scss" scoped>
.footer {
position: fixed;
bottom: 0;
right: 0;
border-top: 1px solid #eee;
padding: 10px;
text-align: right;
width: 100%;
background-color: #fff;
}
.item {
margin: 10px 0;
}
.selectors {
width: 500px;
}
</style>

View File

@ -38,8 +38,14 @@ limitations under the License. -->
metricTypes: dashboardStore.selectedGrid.metricTypes,
metricConfig: dashboardStore.selectedGrid.metricConfig,
relatedTrace: dashboardStore.selectedGrid.relatedTrace,
metricMode: dashboardStore.selectedGrid.metricMode,
expressions: dashboardStore.selectedGrid.expressions || [],
typesOfMQE: dashboardStore.selectedGrid.typesOfMQE || [],
subExpressions: dashboardStore.selectedGrid.subExpressions || [],
subTypesOfMQE: dashboardStore.selectedGrid.subTypesOfMQE || [],
}"
:needQuery="true"
@expressionTips="getErrors"
/>
<div v-show="!graph.type" class="no-data">
{{ t("noData") }}
@ -49,7 +55,7 @@ limitations under the License. -->
<div class="collapse" :style="{ height: configHeight + 'px' }">
<el-collapse v-model="states.activeNames" :style="{ '--el-collapse-header-font-size': '15px' }">
<el-collapse-item :title="t('selectVisualization')" name="1">
<MetricOptions @update="getSource" @loading="setLoading" />
<MetricOptions @update="getSource" @loading="setLoading" :errors="errors" :subErrors="subErrors" />
</el-collapse-item>
<el-collapse-item :title="t('graphStyles')" name="2">
<component :is="`${graph.type}Config`" />
@ -83,6 +89,7 @@ limitations under the License. -->
import type { Option } from "@/types/app";
import graphs from "../graphs";
import CustomOptions from "./widget/index";
import { MetricModes } from "../data";
export default defineComponent({
name: "WidgetEdit",
@ -96,6 +103,8 @@ limitations under the License. -->
const dashboardStore = useDashboardStore();
const appStoreWithOut = useAppStoreWithOut();
const loading = ref<boolean>(false);
const errors = ref<string[]>([]);
const subErrors = ref<string[]>([]);
const states = reactive<{
activeNames: string;
source: unknown;
@ -122,12 +131,34 @@ limitations under the License. -->
states.source = source;
}
function getErrors(params: { tips: string[]; subTips: string[] }) {
errors.value = params.tips;
subErrors.value = params.subTips;
}
function setLoading(load: boolean) {
loading.value = load;
}
function applyConfig() {
dashboardStore.setConfigPanel(false);
const { metricMode } = dashboardStore.selectedGrid;
let p = {};
if (metricMode === MetricModes.Expression) {
p = {
metrics: [],
metricTypes: [],
};
} else {
p = {
expressions: [],
typesOfMQE: [],
};
}
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
...p,
});
dashboardStore.setConfigs(dashboardStore.selectedGrid);
}
@ -146,12 +177,15 @@ limitations under the License. -->
applyConfig,
cancelConfig,
getSource,
getErrors,
setLoading,
widget,
graph,
title,
tips,
hasAssociate,
errors,
subErrors,
};
},
});
@ -188,7 +222,7 @@ limitations under the License. -->
.render-chart {
padding: 5px;
height: 400px;
height: 420px;
width: 100%;
}

View File

@ -21,6 +21,7 @@ import Topology from "./Topology.vue";
import Event from "./Event.vue";
import TimeRange from "./TimeRange.vue";
import ThirdPartyApp from "./ThirdPartyApp.vue";
import ContinuousProfiling from "./ContinuousProfiling.vue";
export default {
Text,
@ -29,4 +30,5 @@ export default {
Event,
TimeRange,
ThirdPartyApp,
ContinuousProfiling,
};

View File

@ -50,12 +50,13 @@ limitations under the License. -->
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import Legend from "./components/Legend.vue";
import { isDef } from "@/utils/is";
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const graph = computed(() => dashboardStore.selectedGrid.graph || {});
const smooth = ref(graph.value.smooth);
const showSymbol = ref(graph.value.showSymbol);
const showSymbol = ref(isDef(graph.value.showSymbol) ? graph.value.showSymbol : true);
const step = ref(graph.value.step);
function updateConfig(param: { [key: string]: unknown }) {

View File

@ -26,23 +26,50 @@ limitations under the License. -->
/>
</div>
<div>{{ t("metrics") }}</div>
<div v-for="(metric, index) in states.metrics" :key="index" class="metric-item">
<Selector
:value="metric"
:options="states.metricList"
size="small"
placeholder="Select a metric"
@change="changeMetrics(index, $event)"
class="selectors"
/>
<Selector
:value="states.metricTypes[index]"
:options="states.metricTypeList[index]"
size="small"
:disabled="graph.type && !states.isList && index !== 0"
@change="changeMetricType(index, $event)"
class="selectors"
/>
<el-switch
v-model="isExpression"
class="mb-5"
active-text="Expressions"
inactive-text="General"
size="small"
@change="changeMetricMode"
/>
<div v-if="isExpression && states.isList">
<span class="title">{{ t("summary") }}</span>
<span>{{ t("detail") }}</span>
</div>
<div v-for="(metric, index) in states.metrics" :key="index" class="mb-10">
<span v-if="isExpression">
<div class="expression-param" contenteditable="true" @blur="changeExpression($event, index)">
{{ metric }}
</div>
<div
v-if="states.isList"
class="expression-param"
contenteditable="true"
@blur="changeSubExpression($event, index)"
>
{{ states.subMetrics[index] }}
</div>
</span>
<span v-else>
<Selector
:value="metric"
:options="states.metricList"
size="small"
placeholder="Select a metric"
@change="changeMetrics(index, $event)"
class="selectors"
/>
<Selector
:value="states.metricTypes[index]"
:options="states.metricTypeList[index]"
size="small"
:disabled="graph.type && !states.isList && index !== 0"
@change="changeMetricType(index, $event)"
class="selectors"
/>
</span>
<el-popover placement="top" :width="400" trigger="click">
<template #reference>
<span @click="setMetricConfig(index)">
@ -51,7 +78,12 @@ limitations under the License. -->
</template>
<Standard @update="queryMetrics" :currentMetricConfig="currentMetricConfig" :index="index" />
</el-popover>
<span v-show="states.isList || states.metricTypes[0] === ProtocolTypes.ReadMetricsValues">
<span
v-show="
states.isList ||
[ProtocolTypes.ReadMetricsValues, ExpressionResultType.TIME_SERIES_VALUES as string].includes(states.metricTypes[0])
"
>
<Icon
class="cp mr-5"
v-if="index === states.metrics.length - 1 && states.metrics.length < defaultLen"
@ -61,6 +93,15 @@ limitations under the License. -->
/>
<Icon class="cp" iconName="remove_circle_outline" size="middle" @click="deleteMetric(index)" />
</span>
<div v-if="(states.tips || [])[index] && isExpression" class="ml-10 red sm">
{{ states.tips[index] }}
</div>
<div v-if="(errors || [])[index] && isExpression" class="ml-10 red sm">
{{ (errors || [])[index] }}
</div>
<div v-if="(subErrors || [])[index] && isExpression" class="ml-10 red sm">
{{ (subErrors || [])[index] }}
</div>
</div>
<div>{{ t("visualization") }}</div>
<div class="chart-types">
@ -68,9 +109,7 @@ limitations under the License. -->
v-for="(type, index) in setVisTypes"
:key="index"
@click="changeChartType(type)"
:class="{
active: type.value === graph.type,
}"
:class="{ active: type.value === graph.type }"
>
{{ type.label }}
</span>
@ -78,6 +117,7 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import { reactive, ref, computed } from "vue";
import type { PropType } from "vue";
import type { Option } from "@/types/app";
import { useDashboardStore } from "@/store/modules/dashboard";
import {
@ -89,29 +129,52 @@ limitations under the License. -->
PodsChartTypes,
MetricsType,
ProtocolTypes,
ExpressionResultType,
MetricModes,
} from "../../../data";
import { ElMessage } from "element-plus";
import Icon from "@/components/Icon.vue";
import { useQueryProcessor, useSourceProcessor, useGetMetricEntity } from "@/hooks/useMetricsProcessor";
import { useQueryProcessor, useSourceProcessor } from "@/hooks/useMetricsProcessor";
import { useExpressionsQueryProcessor } from "@/hooks/useExpressionsProcessor";
import { useI18n } from "vue-i18n";
import type { DashboardItem, MetricConfigOpt } from "@/types/dashboard";
import Standard from "./Standard.vue";
/*global defineEmits */
/*global defineEmits, Indexable */
const { t } = useI18n();
const emit = defineEmits(["update", "loading"]);
/*global defineProps */
defineProps({
errors: {
type: Array as PropType<string[]>,
},
subErrors: {
type: Array as PropType<string[]>,
},
});
const dashboardStore = useDashboardStore();
const metrics = computed(() => dashboardStore.selectedGrid.metrics || []);
const isExpression = ref<boolean>(dashboardStore.selectedGrid.metricMode === MetricModes.Expression ? true : false);
const metrics = computed(
() => (isExpression.value ? dashboardStore.selectedGrid.expressions : dashboardStore.selectedGrid.metrics) || [],
);
const subMetrics = computed(() => (isExpression.value ? dashboardStore.selectedGrid.subExpressions : []) || []);
const subMetricTypes = computed(() => (isExpression.value ? dashboardStore.selectedGrid.subTypesOfMQE : []) || []);
const graph = computed(() => dashboardStore.selectedGrid.graph || {});
const metricTypes = computed(() => dashboardStore.selectedGrid.metricTypes || []);
const metricTypes = computed(
() => (isExpression.value ? dashboardStore.selectedGrid.typesOfMQE : dashboardStore.selectedGrid.metricTypes) || [],
);
const states = reactive<{
metrics: string[];
subMetrics: string[];
subMetricTypes: string[];
metricTypes: string[];
metricTypeList: Option[][];
isList: boolean;
metricList: (Option & { type: string })[];
dashboardName: string;
dashboardList: ((DashboardItem & { label: string; value: string }) | any)[];
tips: string[];
subTips: string[];
}>({
metrics: metrics.value.length ? metrics.value : [""],
metricTypes: metricTypes.value.length ? metricTypes.value : [""],
@ -120,6 +183,10 @@ limitations under the License. -->
metricList: [],
dashboardName: graph.value.dashboardName,
dashboardList: [{ label: "", value: "" }],
tips: [],
subTips: [],
subMetrics: subMetrics.value.length ? subMetrics.value : [""],
subMetricTypes: subMetricTypes.value.length ? subMetricTypes.value : [""],
});
const currentMetricConfig = ref<MetricConfigOpt>({
unit: "",
@ -131,6 +198,8 @@ limitations under the License. -->
states.isList = ListChartTypes.includes(graph.value.type);
const defaultLen = ref<number>(states.isList ? 5 : 20);
const backupMetricConfig = ref<MetricConfigOpt[]>([]);
setDashboards();
setMetricType();
@ -158,7 +227,7 @@ limitations under the License. -->
}
arr = json.data.metrics;
}
states.metricList = (arr || []).filter((d: { catalog: string; type: string }) => {
states.metricList = (arr || []).filter((d: { type: string }) => {
if (states.isList) {
if (d.type === MetricsType.REGULAR_VALUE || d.type === MetricsType.LABELED_VALUE) {
return d;
@ -171,6 +240,14 @@ limitations under the License. -->
return d;
}
});
if (isExpression.value) {
if (states.metrics && states.metrics[0]) {
queryMetrics();
} else {
emit("update", {});
}
return;
}
const metrics: any = states.metricList.filter((d: { value: string; type: string }) =>
states.metrics.includes(d.value),
);
@ -234,12 +311,22 @@ limitations under the License. -->
...dashboardStore.selectedGrid,
metrics: [""],
metricTypes: [""],
expressions: [""],
typesOfMQE: [""],
});
states.metrics = [""];
states.metricTypes = [""];
defaultLen.value = 5;
}
setMetricType(chart);
if (isExpression.value) {
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
graph: chart,
});
} else {
setMetricType(chart);
}
setDashboards(chart.type);
states.dashboardName = "";
defaultLen.value = 10;
@ -300,12 +387,15 @@ limitations under the License. -->
if (states.isList) {
return;
}
const { metricConfig, metricTypes, metrics } = dashboardStore.selectedGrid;
if (!(metrics && metrics[0] && metricTypes && metricTypes[0])) {
if (isExpression.value) {
queryMetricsWithExpressions();
return;
}
const catalog = await useGetMetricEntity(metrics[0], metricTypes[0]);
const params = useQueryProcessor({ ...states, metricConfig, catalog });
const { metricConfig, metricTypes, metrics } = dashboardStore.selectedGrid;
if (!(metrics && metrics[0] && metricTypes && metricTypes[0])) {
return emit("update", {});
}
const params = useQueryProcessor({ ...states, metricConfig });
if (!params) {
emit("update", {});
return;
@ -322,6 +412,23 @@ limitations under the License. -->
emit("update", source);
}
async function queryMetricsWithExpressions() {
const { expressions, metricConfig } = dashboardStore.selectedGrid;
if (!(expressions && expressions[0])) {
return emit("update", {});
}
const params: Indexable = (await useExpressionsQueryProcessor({ ...states, metricConfig })) || {};
states.tips = params.tips || [];
states.metricTypes = params.typesOfMQE || [];
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
typesOfMQE: states.metricTypes,
});
emit("update", params.source || {});
}
function changeDashboard(opt: any) {
if (!opt[0]) {
states.dashboardName = "";
@ -339,31 +446,77 @@ limitations under the License. -->
}
function addMetric() {
states.metrics.push("");
states.tips.push("");
if (isExpression.value && states.isList) {
states.subMetrics.push("");
states.subTips.push("");
}
if (!states.isList) {
states.metricTypes.push(states.metricTypes[0]);
states.metricTypeList.push(states.metricTypeList[0]);
if (!isExpression.value) {
states.metricTypeList.push(states.metricTypeList[0]);
}
return;
}
states.metricTypes.push("");
if (isExpression.value && states.isList) {
states.subMetricTypes.push("");
}
}
function deleteMetric(index: number) {
if (states.metrics.length === 1) {
states.metrics = [""];
states.metricTypes = [""];
states.tips = [""];
let v = {};
if (isExpression.value) {
v = { typesOfMQE: states.metricTypes, expressions: states.metrics };
if (states.isList) {
states.subMetrics = [""];
states.subMetricTypes = [""];
states.subTips = [""];
v = {
...v,
subTypesOfMQE: states.subMetricTypes,
subExpressions: states.subMetrics,
};
}
} else {
v = { metricTypes: states.metricTypes, metrics: states.metrics };
}
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
...{ metricTypes: states.metricTypes, metrics: states.metrics },
...v,
metricConfig: [],
});
return;
}
states.metrics.splice(index, 1);
states.metricTypes.splice(index, 1);
states.tips.splice(index, 1);
const config = dashboardStore.selectedGrid.metricConfig || [];
const metricConfig = config[index] ? config.splice(index, 1) : config;
let p = {};
if (isExpression.value) {
if (states.isList) {
states.subMetrics.splice(index, 1);
states.subMetricTypes.splice(index, 1);
states.subTips.splice(index, 1);
p = {
...p,
typesOfMQE: states.metricTypes,
expressions: states.metrics,
subTypesOfMQE: states.subMetricTypes,
subExpressions: states.subMetrics,
};
}
} else {
p = { metricTypes: states.metricTypes, metrics: states.metrics };
}
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
...{ metricTypes: states.metricTypes, metrics: states.metrics },
...p,
metricConfig,
});
}
@ -393,6 +546,45 @@ limitations under the License. -->
...dashboardStore.selectedGrid.metricConfig[index],
};
}
function changeMetricMode() {
states.metrics = metrics.value.length ? metrics.value : [""];
states.subMetrics = subMetrics.value.length ? subMetrics.value : [""];
states.metricTypes = metricTypes.value.length ? metricTypes.value : [""];
states.subMetricTypes = subMetricTypes.value.length ? subMetricTypes.value : [""];
const config = dashboardStore.selectedGrid.metricConfig;
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
metricMode: isExpression.value ? MetricModes.Expression : MetricModes.General,
metricConfig: backupMetricConfig.value,
});
backupMetricConfig.value = config;
queryMetrics();
}
async function changeExpression(event: any, index: number) {
const params = (event.target.textContent || "").replace(/\s+/g, "");
states.metrics[index] = params;
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
expressions: states.metrics,
});
if (params) {
await queryMetrics();
}
}
async function changeSubExpression(event: any, index: number) {
const params = (event.target.textContent || "").replace(/\s+/g, "");
states.subMetrics[index] = params;
dashboardStore.selectWidget({
...dashboardStore.selectedGrid,
subExpressions: states.subMetrics,
subTypesOfMQE: states.subMetricTypes,
});
if (params) {
await queryMetrics();
}
}
</script>
<style lang="scss" scoped>
.ds-name {
@ -400,7 +592,7 @@ limitations under the License. -->
}
.selectors {
width: 500px;
width: 400px;
margin-right: 10px;
}
@ -427,4 +619,26 @@ limitations under the License. -->
background-color: #409eff;
color: #fff;
}
.expression-param {
display: inline-block;
width: 400px;
border: 1px solid #dcdfe6;
cursor: text;
padding: 0 5px;
border-radius: 3px;
color: #606266;
outline: none;
margin-right: 5px;
min-height: 26px;
&:focus {
border-color: #409eff;
}
}
.title {
display: inline-block;
width: 410px;
}
</style>

View File

@ -28,13 +28,13 @@ limitations under the License. -->
"
/>
</div>
<div class="item mb-10" v-if="hasLabel">
<div class="item mb-10" v-if="hasLabel || isExpression">
<span class="label">{{ t("labels") }}</span>
<el-input
class="input"
v-model="currentMetric.label"
size="small"
placeholder="Please input a name"
placeholder="Please input a label"
@change="
updateConfig(index, {
label: encodeURIComponent(currentMetric.label || ''),
@ -42,7 +42,21 @@ limitations under the License. -->
"
/>
</div>
<div class="item mb-10" v-if="metricType === 'readLabeledMetricsValues'">
<div class="item mb-10" v-if="isList && isExpression">
<span class="label">{{ t("detailLabel") }}</span>
<el-input
class="input"
v-model="currentMetric.detailLabel"
size="small"
placeholder="Please input a label"
@change="
updateConfig(index, {
detailLabel: encodeURIComponent(currentMetric.detailLabel || ''),
})
"
/>
</div>
<div class="item mb-10" v-if="[ProtocolTypes.ReadLabeledMetricsValues].includes(metricType) && !isExpression">
<span class="label">{{ t("labelsIndex") }}</span>
<el-input
class="input"
@ -51,12 +65,12 @@ limitations under the License. -->
placeholder="auto"
@change="
updateConfig(index, {
labelsIndex: encodeURIComponent(currentMetric.labelsIndex),
labelsIndex: encodeURIComponent(currentMetric.labelsIndex || ''),
})
"
/>
</div>
<div class="item mb-10">
<div class="item mb-10" v-show="!isExpression">
<span class="label">{{ t("aggregation") }}</span>
<SelectSingle
:value="currentMetric.calculation"
@ -85,7 +99,7 @@ limitations under the License. -->
type="number"
:min="1"
:max="100"
@change="changeConfigs(index, { topN: currentMetric.topN || 10 })"
@change="changeConfigs(index, { topN: Number(currentMetric.topN) || 10 })"
/>
</div>
</div>
@ -94,7 +108,7 @@ limitations under the License. -->
import { ref, watch, computed } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { SortOrder, CalculationOpts } from "../../../data";
import { SortOrder, CalculationOpts, MetricModes } from "../../../data";
import { useDashboardStore } from "@/store/modules/dashboard";
import type { MetricConfigOpt } from "@/types/dashboard";
import { ListChartTypes, ProtocolTypes } from "../../../data";
@ -110,12 +124,15 @@ limitations under the License. -->
const { t } = useI18n();
const emit = defineEmits(["update"]);
const dashboardStore = useDashboardStore();
const isExpression = ref<boolean>(dashboardStore.selectedGrid.metricMode === MetricModes.Expression);
const currentMetric = ref<MetricConfigOpt>({
...props.currentMetricConfig,
topN: props.currentMetricConfig.topN || 10,
});
const metricTypes = dashboardStore.selectedGrid.metricTypes || [];
const metricType = computed(() => (dashboardStore.selectedGrid.metricTypes || [])[props.index]);
const metricTypes = computed(
() => (isExpression.value ? dashboardStore.selectedGrid.typesOfMQE : dashboardStore.selectedGrid.metricTypes) || [],
);
const metricType = computed(() => metricTypes.value[props.index]);
const hasLabel = computed(() => {
const graph = dashboardStore.selectedGrid.graph || {};
return (
@ -123,11 +140,16 @@ limitations under the License. -->
[ProtocolTypes.ReadLabeledMetricsValues, ProtocolTypes.ReadMetricsValues].includes(metricType.value)
);
});
const isList = computed(() => {
const graph = dashboardStore.selectedGrid.graph || {};
return ListChartTypes.includes(graph.type);
});
const isTopn = computed(() =>
[ProtocolTypes.SortMetrics, ProtocolTypes.ReadSampledRecords, ProtocolTypes.ReadRecords].includes(
metricTypes[props.index],
metricTypes.value[props.index],
),
);
function updateConfig(index: number, param: { [key: string]: string }) {
const key = Object.keys(param)[0];
if (!key) {
@ -148,9 +170,10 @@ limitations under the License. -->
watch(
() => props.currentMetricConfig,
() => {
isExpression.value = dashboardStore.selectedGrid.metricMode === MetricModes.Expression;
currentMetric.value = {
...props.currentMetricConfig,
topN: props.currentMetricConfig.topN || 10,
topN: Number(props.currentMetricConfig.topN) || 10,
};
},
);

View File

@ -0,0 +1,100 @@
<!-- 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">
<div class="title">Continuous Profiling</div>
<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="editConfig">
<span>{{ t("edit") }}</span>
</div>
<div class="tools" @click="removeWidget">
<span>{{ t("delete") }}</span>
</div>
</el-popover>
<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/continuous-profiling/Content.vue";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<any>,
default: () => ({ graph: {} }),
},
activeIndex: { type: String, default: "" },
needQuery: { type: Boolean, default: true },
});
const { t } = useI18n();
const dashboardStore = useDashboardStore();
function removeWidget() {
dashboardStore.removeControls(props.data);
}
function editConfig() {
dashboardStore.setConfigPanel(true);
dashboardStore.selectWidget(props.data);
}
</script>
<style lang="scss" scoped>
.profile-wrapper {
width: 100%;
height: 100%;
font-size: 12px;
position: relative;
}
.operation {
position: absolute;
top: 8px;
right: 3px;
}
.header {
padding: 10px;
font-size: 12px;
border-bottom: 1px solid #dcdfe6;
}
.tools {
padding: 5px 0;
color: #999;
cursor: pointer;
position: relative;
text-align: center;
&:hover {
color: #409eff;
background-color: #eee;
}
}
.title {
font-weight: bold;
line-height: 40px;
padding: 0 10px;
border-bottom: 1px solid #dcdfe6;
}
</style>

View File

@ -285,7 +285,7 @@ limitations under the License. -->
}
.tab-name {
max-width: 110px;
max-width: 130px;
height: 20px;
line-height: 20px;
outline: none;
@ -348,7 +348,7 @@ limitations under the License. -->
.vue-grid-item:not(.vue-grid-placeholder) {
background: #fff;
box-shadow: 0px 1px 4px 0px #00000029;
box-shadow: 0 1px 4px 0 #00000029;
border-radius: 3px;
}

View File

@ -0,0 +1,92 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="content-wrapper flex-v">
<div class="title">Task Timeline</div>
<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>
<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/task-timeline/Content.vue";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<any>,
default: () => ({ graph: {} }),
},
activeIndex: { type: String, default: "" },
needQuery: { type: Boolean, default: true },
});
const { t } = useI18n();
const dashboardStore = useDashboardStore();
function removeWidget() {
dashboardStore.removeControls(props.data);
}
</script>
<style lang="scss" scoped>
.content-wrapper {
width: 100%;
height: 100%;
font-size: 12px;
position: relative;
}
.operation {
position: absolute;
top: 8px;
right: 3px;
}
.header {
padding: 10px;
font-size: 12px;
border-bottom: 1px solid #dcdfe6;
}
.tools {
padding: 5px 0;
color: #999;
cursor: pointer;
position: relative;
text-align: center;
&:hover {
color: #409eff;
background-color: #eee;
}
}
.title {
font-weight: bold;
line-height: 40px;
padding: 0 10px;
border-bottom: 1px solid #dcdfe6;
}
</style>

View File

@ -59,6 +59,11 @@ limitations under the License. -->
filters: data.filters || {},
relatedTrace: data.relatedTrace || {},
associate: data.associate || [],
metricMode: data.metricMode,
expressions: data.expressions || [],
typesOfMQE: typesOfMQE || [],
subExpressions: data.subExpressions || [],
subTypesOfMQE: data.subTypesOfMQE || [],
}"
:needQuery="needQuery"
@click="clickHandle"
@ -76,10 +81,12 @@ limitations under the License. -->
import { useSelectorStore } from "@/store/modules/selectors";
import graphs from "../graphs";
import { useI18n } from "vue-i18n";
import { useQueryProcessor, useSourceProcessor, useGetMetricEntity } from "@/hooks/useMetricsProcessor";
import { useQueryProcessor, useSourceProcessor } from "@/hooks/useMetricsProcessor";
import { useExpressionsQueryProcessor } from "@/hooks/useExpressionsProcessor";
import { EntityType, ListChartTypes } from "../data";
import type { EventParams } from "@/types/dashboard";
import getDashboard from "@/hooks/useDashboardsSession";
import { MetricModes } from "../data";
const props = {
data: {
@ -107,16 +114,28 @@ limitations under the License. -->
const graph = computed(() => props.data.graph || {});
const widget = computed(() => props.data.widget || {});
const isList = computed(() => ListChartTypes.includes((props.data.graph && props.data.graph.type) || ""));
const typesOfMQE = ref<string[]>([]);
if ((props.needQuery || !dashboardStore.currentDashboard.id) && !isList.value) {
queryMetrics();
}
async function queryMetrics() {
const metricTypes: string[] = props.data.metricTypes || [];
const metrics = props.data.metrics || [];
const catalog = await useGetMetricEntity(metrics[0], metricTypes[0]);
const params = await useQueryProcessor({ ...props.data, catalog });
const isExpression = props.data.metricMode === MetricModes.Expression;
if (isExpression) {
loading.value = true;
const e = {
metrics: props.data.expressions || [],
metricConfig: props.data.metricConfig || [],
};
const params = (await useExpressionsQueryProcessor(e)) || {};
loading.value = false;
state.source = params.source || {};
typesOfMQE.value = params.typesOfMQE;
return;
}
const params = await useQueryProcessor({ ...props.data });
if (!params) {
state.source = {};
@ -133,7 +152,7 @@ limitations under the License. -->
metricTypes: props.data.metricTypes || [],
metricConfig: props.data.metricConfig || [],
};
state.source = useSourceProcessor(json, d);
state.source = await useSourceProcessor(json, d);
}
function removeWidget() {
@ -169,7 +188,7 @@ limitations under the License. -->
dashboardStore.selectWidget(props.data);
}
watch(
() => [props.data.metricTypes, props.data.metrics],
() => [props.data.metricTypes, props.data.metrics, props.data.expressions],
() => {
if (!dashboardStore.selectedGrid) {
return;
@ -190,7 +209,7 @@ limitations under the License. -->
if (isList.value) {
return;
}
if (dashboardStore.entity === EntityType[0].value || dashboardStore.entity === EntityType[4].value) {
if ([EntityType[0].value, EntityType[4].value].includes(dashboardStore.entity)) {
queryMetrics();
}
},
@ -198,7 +217,7 @@ limitations under the License. -->
watch(
() => [selectorStore.currentPod, selectorStore.currentDestPod],
() => {
if (dashboardStore.entity === EntityType[0].value || dashboardStore.entity === EntityType[7].value) {
if ([EntityType[0].value, EntityType[7].value, EntityType[8].value].includes(dashboardStore.entity)) {
return;
}
if (isList.value) {
@ -210,10 +229,10 @@ limitations under the License. -->
watch(
() => [selectorStore.currentProcess, selectorStore.currentDestProcess],
() => {
if (!(selectorStore.currentDestProcess && selectorStore.currentProcess)) {
if (isList.value) {
return;
}
if (dashboardStore.entity === EntityType[7].value) {
if ([EntityType[7].value, EntityType[8].value].includes(dashboardStore.entity)) {
queryMetrics();
}
},
@ -242,6 +261,7 @@ limitations under the License. -->
t,
graph,
widget,
typesOfMQE,
clickHandle,
};
},

View File

@ -25,8 +25,10 @@ import Ebpf from "./Ebpf.vue";
import DemandLog from "./DemandLog.vue";
import Event from "./Event.vue";
import NetworkProfiling from "./NetworkProfiling.vue";
import ContinuousProfiling from "./ContinuousProfiling.vue";
import TimeRange from "./TimeRange.vue";
import ThirdPartyApp from "./ThirdPartyApp.vue";
import TaskTimeline from "./TaskTimeline.vue";
export default {
Tab,
@ -40,6 +42,8 @@ export default {
DemandLog,
Event,
NetworkProfiling,
ContinuousProfiling,
TimeRange,
ThirdPartyApp,
TaskTimeline,
};

View File

@ -24,8 +24,10 @@ import Ebpf from "./Ebpf.vue";
import DemandLog from "./DemandLog.vue";
import Event from "./Event.vue";
import NetworkProfiling from "./NetworkProfiling.vue";
import ContinuousProfiling from "./ContinuousProfiling.vue";
import TimeRange from "./TimeRange.vue";
import ThirdPartyApp from "./ThirdPartyApp.vue";
import TaskTimeline from "./TaskTimeline.vue";
export default {
Widget,
@ -40,4 +42,6 @@ export default {
NetworkProfiling,
TimeRange,
ThirdPartyApp,
ContinuousProfiling,
TaskTimeline,
};

View File

@ -56,6 +56,15 @@ export enum ProtocolTypes {
ReadMetricsValues = "readMetricsValues",
ReadMetricsValue = "readMetricsValue",
}
export enum ExpressionResultType {
UNKNOWN = "UNKNOWN",
SINGLE_VALUE = "SINGLE_VALUE",
TIME_SERIES_VALUES = "TIME_SERIES_VALUES",
SORTED_LIST = "SORTED_LIST",
RECORD_LIST = "RECORD_LIST",
}
export const DefaultGraphConfig: { [key: string]: any } = {
Bar: {
type: "Bar",
@ -65,7 +74,7 @@ export const DefaultGraphConfig: { [key: string]: any } = {
type: "Line",
step: false,
smooth: false,
showSymbol: false,
showSymbol: true,
showXAxis: true,
showYAxis: true,
},
@ -167,6 +176,7 @@ export const EntityType = [
},
{ value: "EndpointRelation", label: "Endpoint Relation", key: 4 },
{ value: "ProcessRelation", label: "Process Relation", key: 5 },
{ value: "Process", label: "Process", key: 6 },
];
export const ListEntity: any = {
InstanceList: EntityType[3].value,
@ -194,6 +204,7 @@ export const ServiceTools = [
{ name: "merge", content: "Add Trace", id: "addTrace" },
{ name: "timeline", content: "Add Trace Profiling", id: "addProfile" },
{ name: "insert_chart", content: "Add eBPF Profiling", id: "addEbpf" },
{ name: "continuous_profiling", content: "Add Continuous Profiling", id: "addContinuousProfiling" },
{ name: "assignment", content: "Add Log", id: "addLog" },
{ name: "demand", content: "Add On Demand Log", id: "addDemandLog" },
{ name: "event", content: "Add Event", id: "addEvent" },
@ -225,10 +236,16 @@ export const EndpointTools = [
{ name: "add_iframe", content: "Add Iframe", id: "addIframe" },
];
export const ProcessTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
{ name: "task_timeline", content: "Add Task Timeline", id: "addTaskTimeline" },
{ name: "library_books", content: "Add Text", id: "addText" },
{ name: "add_iframe", content: "Add Iframe", id: "addIframe" },
];
export const ProcessRelationTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
{ name: "library_books", content: "Add Text", id: "addText" },
{ name: "time_range", content: "Add Time Range Text", id: "addTimeRange" },
{ name: "add_iframe", content: "Add Iframe", id: "addIframe" },
];
export const ServiceRelationTools = [
@ -322,3 +339,8 @@ export const RefreshOptions = [
{ label: "Last 8 hours", value: "8", step: "HOUR" },
{ label: "Last 7 days", value: "7", step: "DAY" },
];
export enum MetricModes {
Expression = "Expression",
General = "General",
}

View File

@ -23,6 +23,7 @@ limitations under the License. -->
import type { PropType } from "vue";
import type { BarConfig, EventParams, RelatedTrace, Filters } from "@/types/dashboard";
import useLegendProcess from "@/hooks/useLegendProcessor";
import Legend from "./components/Legend.vue";
/*global defineProps, defineEmits */
const emits = defineEmits(["click"]);

View File

@ -15,7 +15,7 @@ limitations under the License. -->
<template>
<div
v-if="!isNaN(singleVal)"
v-if="singleVal !== null && !isNaN(singleVal)"
class="chart-card"
:style="{
fontSize: `${config.fontSize}px`,
@ -27,7 +27,7 @@ limitations under the License. -->
{{ decodeURIComponent(unit) }}
</span>
</div>
<div class="center no-data" v-else>{{ t("noData") }}</div>
<div v-else class="card-no-data" :class="config.textAlign === 'center' ? 'center' : ''">{{ t("noData") }}</div>
</template>
<script lang="ts" setup>
import { computed } from "vue";
@ -68,13 +68,20 @@ limitations under the License. -->
align-items: center;
}
.no-data {
.card-no-data {
height: 100%;
color: #666;
display: flex;
align-items: center;
font-size: 14px;
}
.unit {
display: inline-block;
margin-left: 2px;
}
.center {
justify-content: center;
}
</style>

View File

@ -17,7 +17,7 @@ limitations under the License. -->
<div class="search">
<el-input v-model="searchText" placeholder="Search for more endpoints" @change="searchList" class="inputs">
<template #append>
<el-button @click="searchList" class="btn">
<el-button @click="searchList">
<Icon size="middle" iconName="search" />
</el-button>
</template>
@ -36,11 +36,12 @@ limitations under the License. -->
<ColumnGraph
:intervalTime="intervalTime"
:colMetrics="colMetrics"
:colSubMetrics="colSubMetrics"
:config="{
...config,
metrics: colMetrics,
metricConfig,
metricTypes,
metricMode,
}"
v-if="colMetrics.length"
/>
@ -58,7 +59,8 @@ limitations under the License. -->
import type { Endpoint } from "@/types/selector";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useQueryPodsMetrics, usePodsSource } from "@/hooks/useMetricsProcessor";
import { EntityType } from "../data";
import { useExpressionsQueryPodsMetrics } from "@/hooks/useExpressionsProcessor";
import { EntityType, MetricModes } from "../data";
import router from "@/router";
import getDashboard from "@/hooks/useDashboardsSession";
import type { MetricConfigOpt } from "@/types/dashboard";
@ -75,6 +77,11 @@ limitations under the License. -->
i: string;
metrics: string[];
metricTypes: string[];
metricMode: string;
expressions: string[];
typesOfMQE: string[];
subExpressions: string[];
subTypesOfMQE: string[];
} & { metricConfig: MetricConfigOpt[] }
>,
default: () => ({
@ -89,6 +96,7 @@ limitations under the License. -->
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
const emit = defineEmits(["expressionTips"]);
const { t } = useI18n();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
@ -96,8 +104,10 @@ limitations under the License. -->
const endpoints = ref<Endpoint[]>([]);
const searchText = ref<string>("");
const colMetrics = ref<string[]>([]);
const colSubMetrics = ref<string[]>([]);
const metricConfig = ref<MetricConfigOpt[]>(props.config.metricConfig || []);
const metricTypes = ref<string[]>(props.config.metricTypes || []);
const metricMode = ref<string>(props.config.metricMode);
if (props.needQuery) {
queryEndpoints();
@ -113,11 +123,23 @@ limitations under the License. -->
ElMessage.error(resp.errors);
return;
}
endpoints.value = selectorStore.pods;
endpoints.value = resp.data.pods || [];
queryEndpointMetrics(endpoints.value);
}
async function queryEndpointMetrics(currentPods: Endpoint[]) {
if (!currentPods.length) {
async function queryEndpointMetrics(arr: Endpoint[]) {
if (!arr.length) {
return;
}
const currentPods = arr.map((d: Endpoint) => {
return {
id: d.id,
value: d.value,
label: d.label,
merge: d.merge,
};
});
if (props.config.metricMode === MetricModes.Expression) {
queryEndpointExpressions(currentPods);
return;
}
const metrics = props.config.metrics || [];
@ -141,6 +163,35 @@ limitations under the License. -->
return;
}
endpoints.value = currentPods;
colMetrics.value = [];
metricTypes.value = [];
metricConfig.value = [];
}
async function queryEndpointExpressions(currentPods: Endpoint[]) {
const expressions = props.config.expressions || [];
const subExpressions = props.config.subExpressions || [];
if (expressions.length && expressions[0]) {
const params = await useExpressionsQueryPodsMetrics(
currentPods,
{ metricConfig: metricConfig.value || [], expressions, subExpressions },
EntityType[2].value,
);
endpoints.value = params.data;
colMetrics.value = params.names;
colSubMetrics.value = params.subNames;
metricTypes.value = params.metricTypesArr;
metricConfig.value = params.metricConfigArr;
emit("expressionTips", { tips: params.expressionsTips, subTips: params.subExpressionsTips });
return;
}
endpoints.value = currentPods;
colMetrics.value = [];
colSubMetrics.value = [];
metricTypes.value = [];
metricConfig.value = [];
emit("expressionTips", [], []);
}
function clickEndpoint(scope: any) {
const { dashboard } = getDashboard({
@ -160,12 +211,20 @@ limitations under the License. -->
await queryEndpoints();
}
watch(
() => [...(props.config.metricTypes || []), ...(props.config.metrics || []), ...(props.config.metricConfig || [])],
() => [
...(props.config.metricTypes || []),
...(props.config.metrics || []),
...(props.config.metricConfig || []),
...(props.config.expressions || []),
...(props.config.subExpressions || []),
props.config.metricMode,
],
(data, old) => {
if (JSON.stringify(data) === JSON.stringify(old)) {
return;
}
metricConfig.value = props.config.metricConfig;
metricMode.value = props.config.metricMode;
queryEndpointMetrics(endpoints.value);
},
);
@ -177,9 +236,9 @@ limitations under the License. -->
);
</script>
<style lang="scss" scoped>
@import "./style.scss";
@import url("./style.scss");
.tips {
color: rgba(255, 0, 0, 0.5);
color: rgb(255 0 0 / 50%);
}
</style>

View File

@ -17,7 +17,7 @@ limitations under the License. -->
<div class="search">
<el-input v-model="searchText" placeholder="Please input instance name" @change="searchList" class="inputs">
<template #append>
<el-button class="btn" @click="searchList">
<el-button @click="searchList">
<Icon size="sm" iconName="search" />
</el-button>
</template>
@ -35,11 +35,12 @@ limitations under the License. -->
<ColumnGraph
:intervalTime="intervalTime"
:colMetrics="colMetrics"
:colSubMetrics="colSubMetrics"
:config="{
...config,
metrics: colMetrics,
metricConfig,
metricTypes,
metricMode,
}"
v-if="colMetrics.length"
/>
@ -70,7 +71,7 @@ limitations under the License. -->
small
layout="prev, pager, next"
:page-size="pageSize"
:total="selectorStore.pods.length"
:total="pods.length"
@current-change="changePage"
@prev-click="changePage"
@next-click="changePage"
@ -87,7 +88,8 @@ limitations under the License. -->
import type { InstanceListConfig } from "@/types/dashboard";
import type { Instance } from "@/types/selector";
import { useQueryPodsMetrics, usePodsSource } from "@/hooks/useMetricsProcessor";
import { EntityType } from "../data";
import { useExpressionsQueryPodsMetrics } from "@/hooks/useExpressionsProcessor";
import { EntityType, MetricModes } from "../data";
import router from "@/router";
import getDashboard from "@/hooks/useDashboardsSession";
import type { MetricConfigOpt } from "@/types/dashboard";
@ -102,6 +104,11 @@ limitations under the License. -->
metrics: string[];
metricTypes: string[];
isEdit: boolean;
metricMode: string;
expressions: string[];
typesOfMQE: string[];
subExpressions: string[];
subTypesOfMQE: string[];
} & { metricConfig: MetricConfigOpt[] }
>,
default: () => ({
@ -115,6 +122,7 @@ limitations under the License. -->
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
needQuery: { type: Boolean, default: false },
});
const emit = defineEmits(["expressionTips"]);
const { t } = useI18n();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
@ -123,8 +131,11 @@ limitations under the License. -->
const pageSize = 10;
const searchText = ref<string>("");
const colMetrics = ref<string[]>([]);
const colSubMetrics = ref<string[]>([]);
const metricConfig = ref<MetricConfigOpt[]>(props.config.metricConfig || []);
const metricTypes = ref<string[]>(props.config.metricTypes || []);
const pods = ref<Instance[]>([]); // all instances
const metricMode = ref<string>(props.config.metricMode);
if (props.needQuery) {
queryInstance();
}
@ -137,14 +148,31 @@ limitations under the License. -->
if (resp && resp.errors) {
ElMessage.error(resp.errors);
instances.value = [];
pods.value = [];
return;
}
instances.value = selectorStore.pods.filter((d: unknown, index: number) => index < pageSize);
pods.value = resp.data.pods || [];
instances.value = pods.value.filter((d: unknown, index: number) => index < pageSize);
queryInstanceMetrics(instances.value);
}
async function queryInstanceMetrics(currentInstances: Instance[]) {
if (!currentInstances.length) {
async function queryInstanceMetrics(arr: Instance[]) {
if (!arr.length) {
return;
}
const currentInstances = arr.map((d: Instance) => {
return {
id: d.id,
value: d.value,
label: d.label,
merge: d.merge,
language: d.language,
instanceUUID: d.instanceUUID,
attributes: d.attributes,
};
});
if (props.config.metricMode === MetricModes.Expression) {
queryInstanceExpressions(currentInstances);
return;
}
const metrics = props.config.metrics || [];
@ -166,9 +194,40 @@ limitations under the License. -->
colMetrics.value = names;
metricTypes.value = metricTypesArr;
metricConfig.value = metricConfigArr;
return;
}
instances.value = currentInstances;
colMetrics.value = [];
metricTypes.value = [];
metricConfig.value = [];
}
async function queryInstanceExpressions(currentInstances: Instance[]) {
const expressions = props.config.expressions || [];
const subExpressions = props.config.subExpressions || [];
if (expressions.length && expressions[0]) {
const params = await useExpressionsQueryPodsMetrics(
currentInstances,
{ metricConfig: metricConfig.value || [], expressions, subExpressions },
EntityType[3].value,
);
instances.value = params.data;
colMetrics.value = params.names;
colSubMetrics.value = params.subNames;
metricTypes.value = params.metricTypesArr;
metricConfig.value = params.metricConfigArr;
emit("expressionTips", { tips: params.expressionsTips, subTips: params.subExpressionsTips });
return;
}
instances.value = currentInstances;
colSubMetrics.value = [];
colMetrics.value = [];
metricTypes.value = [];
metricConfig.value = [];
emit("expressionTips", [], []);
}
function clickInstance(scope: any) {
@ -189,7 +248,7 @@ limitations under the License. -->
}
function changePage(pageIndex: number) {
instances.value = selectorStore.pods.filter((d: unknown, index: number) => {
instances.value = pods.value.filter((d: unknown, index: number) => {
if (index >= (pageIndex - 1) * pageSize && index < pageIndex * pageSize) {
return d;
}
@ -198,18 +257,26 @@ limitations under the License. -->
}
function searchList() {
const searchInstances = selectorStore.pods.filter((d: { label: string }) => d.label.includes(searchText.value));
const searchInstances = pods.value.filter((d: { label: string }) => d.label.includes(searchText.value));
instances.value = searchInstances.filter((d: unknown, index: number) => index < pageSize);
queryInstanceMetrics(instances.value);
}
watch(
() => [...(props.config.metricTypes || []), ...(props.config.metrics || []), ...(props.config.metricConfig || [])],
() => [
...(props.config.metricTypes || []),
...(props.config.metrics || []),
...(props.config.metricConfig || []),
...(props.config.expressions || []),
...(props.config.subExpressions || []),
props.config.metricMode,
],
(data, old) => {
if (JSON.stringify(data) === JSON.stringify(old)) {
return;
}
metricConfig.value = props.config.metricConfig;
metricMode.value = props.config.metricMode;
queryInstanceMetrics(instances.value);
},
);
@ -221,7 +288,7 @@ limitations under the License. -->
);
</script>
<style lang="scss" scoped>
@import "./style.scss";
@import url("./style.scss");
.attributes {
max-height: 400px;

View File

@ -30,6 +30,7 @@ limitations under the License. -->
import type { LineConfig, EventParams, RelatedTrace, Filters } from "@/types/dashboard";
import Legend from "./components/Legend.vue";
import useLegendProcess from "@/hooks/useLegendProcessor";
import { isDef } from "@/utils/is";
/*global defineProps, defineEmits */
const emits = defineEmits(["click"]);
@ -73,12 +74,12 @@ limitations under the License. -->
name: i,
type: "line",
symbol: "circle",
symbolSize: 8,
showSymbol: props.config.showSymbol,
symbolSize: 4,
showSymbol: isDef(props.config.showSymbol) ? props.config.showSymbol : true,
step: props.config.step,
smooth: props.config.smooth,
lineStyle: {
width: 1.5,
width: 2,
type: "solid",
},
};

View File

@ -17,7 +17,7 @@ limitations under the License. -->
<div class="search">
<el-input v-model="searchText" placeholder="Please input service name" @change="searchList" class="inputs mt-5">
<template #append>
<el-button class="btn" @click="searchList">
<el-button @click="searchList">
<Icon size="sm" iconName="search" />
</el-button>
</template>
@ -47,11 +47,12 @@ limitations under the License. -->
<ColumnGraph
:intervalTime="intervalTime"
:colMetrics="colMetrics"
:colSubMetrics="colSubMetrics"
:config="{
...config,
metrics: colMetrics,
metricConfig,
metricTypes,
metricMode,
}"
v-if="colMetrics.length"
/>
@ -80,7 +81,8 @@ limitations under the License. -->
import { useAppStoreWithOut } from "@/store/modules/app";
import type { Service } from "@/types/selector";
import { useQueryPodsMetrics, usePodsSource } from "@/hooks/useMetricsProcessor";
import { EntityType } from "../data";
import { useExpressionsQueryPodsMetrics } from "@/hooks/useExpressionsProcessor";
import { EntityType, MetricModes } from "../data";
import router from "@/router";
import getDashboard from "@/hooks/useDashboardsSession";
import type { MetricConfigOpt } from "@/types/dashboard";
@ -100,6 +102,11 @@ limitations under the License. -->
isEdit: boolean;
names: string[];
metricConfig: MetricConfigOpt[];
metricMode: string;
expressions: string[];
typesOfMQE: string[];
subExpressions: string[];
subTypesOfMQE: string[];
}
>,
default: () => ({ dashboardName: "", fontSize: 12 }),
@ -107,6 +114,7 @@ limitations under the License. -->
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
isEdit: { type: Boolean, default: false },
});
const emit = defineEmits(["expressionTips"]);
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const appStore = useAppStoreWithOut();
@ -114,11 +122,13 @@ limitations under the License. -->
const pageSize = 10;
const services = ref<Service[]>([]);
const colMetrics = ref<string[]>([]);
const colSubMetrics = ref<string[]>([]);
const searchText = ref<string>("");
const groups = ref<any>({});
const sortServices = ref<(Service & { merge: boolean })[]>([]);
const metricConfig = ref<MetricConfigOpt[]>(props.config.metricConfig || []);
const metricTypes = ref<string[]>(props.config.metricTypes || []);
const metricMode = ref<string>(props.config.metricMode);
queryServices();
@ -187,8 +197,23 @@ limitations under the License. -->
router.push(path);
}
async function queryServiceMetrics(currentServices: Service[]) {
if (!currentServices.length) {
async function queryServiceMetrics(arr: Service[]) {
if (!arr.length) {
return;
}
const currentServices = arr.map((d: Service) => {
return {
id: d.id,
value: d.value,
label: d.label,
layers: d.layers,
group: d.group,
normal: d.normal,
merge: d.merge,
};
});
if (props.config.metricMode === MetricModes.Expression) {
queryServiceExpressions(currentServices);
return;
}
const metrics = props.config.metrics || [];
@ -211,6 +236,7 @@ limitations under the License. -->
...props.config,
metricConfig: metricConfig.value || [],
});
services.value = data;
colMetrics.value = names;
metricTypes.value = metricTypesArr;
@ -219,6 +245,35 @@ limitations under the License. -->
return;
}
services.value = currentServices;
colMetrics.value = [];
colMetrics.value = [];
metricTypes.value = [];
metricConfig.value = [];
}
async function queryServiceExpressions(currentServices: Service[]) {
const expressions = props.config.expressions || [];
const subExpressions = props.config.subExpressions || [];
if (expressions.length && expressions[0]) {
const params = await useExpressionsQueryPodsMetrics(
currentServices,
{ metricConfig: metricConfig.value || [], expressions, subExpressions },
EntityType[0].value,
);
services.value = params.data;
colMetrics.value = params.names;
colSubMetrics.value = params.subNames;
metricTypes.value = params.metricTypesArr;
metricConfig.value = params.metricConfigArr;
emit("expressionTips", { tips: params.expressionsTips, subTips: params.subExpressionsTips });
return;
}
services.value = currentServices;
colMetrics.value = [];
colSubMetrics.value = [];
metricTypes.value = [];
metricConfig.value = [];
emit("expressionTips", [], []);
}
function objectSpanMethod(param: any): any {
if (!props.config.showGroup) {
@ -251,15 +306,24 @@ limitations under the License. -->
}
watch(
() => [...(props.config.metricTypes || []), ...(props.config.metrics || []), ...(props.config.metricConfig || [])],
() => [
...(props.config.metricTypes || []),
...(props.config.metrics || []),
...(props.config.metricConfig || []),
...(props.config.expressions || []),
...(props.config.subExpressions || []),
props.config.metricMode,
],
(data, old) => {
if (JSON.stringify(data) === JSON.stringify(old)) {
return;
}
metricConfig.value = props.config.metricConfig;
metricMode.value = props.config.metricMode;
queryServiceMetrics(services.value);
},
);
watch(
() => appStore.durationTime,
() => {
@ -270,5 +334,5 @@ limitations under the License. -->
);
</script>
<style lang="scss" scoped>
@import "./style.scss";
@import url("./style.scss");
</style>

View File

@ -64,21 +64,23 @@ limitations under the License. -->
import copy from "@/utils/copy";
import { TextColors } from "@/views/dashboard/data";
import Trace from "@/views/dashboard/related/trace/Index.vue";
import { QueryOrders, Status, RefIdTypes, ProtocolTypes } from "../data";
import { QueryOrders, Status, RefIdTypes, ProtocolTypes, ExpressionResultType } from "../data";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{
[key: string]: { name: string; value: number; id: string }[];
[key: string]: { name: string; value: number; refId: string }[];
}>,
default: () => ({}),
},
config: {
type: Object as PropType<{
metricMode: string;
color: string;
metrics: string[];
metricTypes: string[];
typesOfMQE: string[];
relatedTrace: any;
}>,
default: () => ({ color: "purple" }),
@ -107,14 +109,17 @@ limitations under the License. -->
function handleClick(i: string) {
copy(i);
}
function viewTrace(item: { name: string; id: string; value: unknown }) {
function viewTrace(item: { name: string; refId: string; value: unknown }) {
const filters = {
...item,
queryOrder: QueryOrders[1].value,
status: Status[2].value,
id: item.id || item.name,
id: item.refId,
metricValue: [{ label: props.config.metrics[0], data: item.value, value: item.name }],
isReadRecords: props.config.metricTypes.includes(ProtocolTypes.ReadRecords) || undefined,
isReadRecords:
props.config.typesOfMQE.includes(ExpressionResultType.RECORD_LIST) ||
props.config.metricTypes.includes(ProtocolTypes.ReadRecords) ||
undefined,
};
traceOptions.value = {
...traceOptions.value,
@ -163,7 +168,7 @@ limitations under the License. -->
}
.chart-slow-link {
padding: 4px 10px 7px 10px;
padding: 4px 10px 7px;
border-radius: 4px;
border: 1px solid #ddd;
color: #333;

View File

@ -23,7 +23,7 @@ limitations under the License. -->
<template #default="scope">
<div class="chart">
<Line
v-if="useListConfig(config, index).isLinear"
v-if="useListConfig(config, index).isLinear && config.metricMode !== MetricModes.Expression"
:data="{
[metric]: scope.row[metric] && scope.row[metric].values,
}"
@ -35,7 +35,10 @@ limitations under the License. -->
showlabels: false,
}"
/>
<span class="item flex-h" v-else-if="useListConfig(config, index).isAvg">
<span
class="item flex-h"
v-else-if="useListConfig(config, index).isAvg || config.metricMode === MetricModes.Expression"
>
<el-popover placement="left" :width="400" trigger="click">
<template #reference>
<span class="trend">
@ -45,7 +48,8 @@ limitations under the License. -->
<div class="view-line">
<Line
:data="{
[metric]: scope.row[metric] && scope.row[metric].values,
[decodeURIComponent(getLabel(colSubMetrics[index], index, true)) || metric]:
scope.row[colSubMetrics[index] || metric] && scope.row[colSubMetrics[index] || metric].values,
}"
:intervalTime="intervalTime"
:config="{
@ -79,23 +83,25 @@ limitations under the License. -->
import Line from "../Line.vue";
import Card from "../Card.vue";
import { MetricQueryTypes } from "@/hooks/data";
import { ExpressionResultType, MetricModes } from "@/views/dashboard/data";
/*global defineProps */
const props = defineProps({
colMetrics: { type: Object },
colMetrics: { type: Array as PropType<string[]>, default: () => [] },
colSubMetrics: { type: Array as PropType<string[]>, default: () => [] },
config: {
type: Object as PropType<{
i: string;
metrics: string[];
metricTypes: string[];
metricConfig: MetricConfigOpt[];
metricMode: string;
}>,
default: () => ({}),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
function getUnit(index: string) {
function getUnit(index: number) {
const i = Number(index);
const u = props.config.metricConfig && props.config.metricConfig[i] && props.config.metricConfig[i].unit;
if (u) {
@ -103,11 +109,25 @@ limitations under the License. -->
}
return encodeURIComponent("");
}
function getLabel(metric: string, index: string) {
function getLabel(metric: string, index: number, isDetail?: boolean) {
const i = Number(index);
const label = props.config.metricConfig && props.config.metricConfig[i] && props.config.metricConfig[i].label;
let label = "";
if (isDetail) {
label =
(props.config.metricConfig && props.config.metricConfig[i] && props.config.metricConfig[i].detailLabel) || "";
} else {
label = (props.config.metricConfig && props.config.metricConfig[i] && props.config.metricConfig[i].label) || "";
}
if (label) {
if (props.config.metricTypes[i] === MetricQueryTypes.ReadLabeledMetricsValues) {
if (
(
[
MetricQueryTypes.ReadLabeledMetricsValues,
ExpressionResultType.TIME_SERIES_VALUES,
ExpressionResultType.SINGLE_VALUE,
] as string[]
).includes(props.config.metricTypes[i])
) {
const name = (label || "").split(",").map((item: string) => item.replace(/^\s*|\s*$/g, ""))[
props.config.metricConfig[i].index || 0
];
@ -115,7 +135,7 @@ limitations under the License. -->
}
return encodeURIComponent(label);
}
return encodeURIComponent(metric);
return encodeURIComponent(metric || "");
}
</script>
<style lang="scss" scoped>

View File

@ -38,6 +38,7 @@
color: #409eff;
display: inline-block;
width: 100%;
text-decoration: underline;
}
.search {
@ -48,10 +49,6 @@
width: 400px;
}
.btn {
margin-top: -12px;
}
.chart {
height: 60px;
}

View File

@ -27,7 +27,7 @@ limitations under the License. -->
class="selectors"
/>
</div>
<div class="selectors-item" v-if="key === 3 || key === 4 || key === 5">
<div class="selectors-item" v-if="key === 3 || key === 4 || key === 5 || key === 6">
<span class="label">
{{ ["EndpointRelation", "Endpoint"].includes(dashboardStore.entity) ? "$Endpoint" : "$ServiceInstance" }}
</span>
@ -42,7 +42,7 @@ limitations under the License. -->
:isRemote="['EndpointRelation', 'Endpoint'].includes(dashboardStore.entity)"
/>
</div>
<div class="selectors-item" v-if="key === 5">
<div class="selectors-item" v-if="key === 5 || key === 6">
<span class="label"> $Process </span>
<Selector
v-model="states.currentProcess"
@ -118,8 +118,8 @@ limitations under the License. -->
<div class="switch">
<el-switch
v-model="dashboardStore.editMode"
active-text="Edit"
inactive-text="View"
active-text="E"
inactive-text="V"
size="small"
inline-prompt
active-color="#409eff"
@ -145,6 +145,7 @@ limitations under the License. -->
InstanceRelationTools,
ServiceRelationTools,
ProcessTools,
ProcessRelationTools,
} from "../data";
import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus";
@ -216,10 +217,11 @@ limitations under the License. -->
EntityType[5].value,
EntityType[6].value,
EntityType[7].value,
EntityType[8].value,
].includes(String(params.entity))
) {
setSourceSelector();
if ([EntityType[2].value, EntityType[3].value].includes(String(params.entity))) {
if ([EntityType[2].value, EntityType[3].value, EntityType[8].value].includes(String(params.entity))) {
return;
}
setDestSelector();
@ -261,6 +263,7 @@ limitations under the License. -->
await selectorStore.getService(String(params.serviceId));
states.currentService = selectorStore.currentService.value;
const e = String(params.entity).split("Relation")[0];
await fetchPods(e, selectorStore.currentService.id, true);
}
@ -316,6 +319,7 @@ limitations under the License. -->
EntityType[5].value,
EntityType[6].value,
EntityType[7].value,
EntityType[8].value,
].includes(dashboardStore.entity)
) {
await fetchPods(e, selectorStore.currentService.id, true);
@ -336,11 +340,8 @@ limitations under the License. -->
selectorStore.setCurrentPod(null);
states.currentPod = "";
states.currentProcess = "";
if (dashboardStore.entity === EntityType[7].value) {
fetchPods("Process", selectorStore.currentService.id, true);
} else {
fetchPods(dashboardStore.entity, selectorStore.currentService.id, true);
}
const e = dashboardStore.entity === EntityType[7].value ? EntityType[8].value : dashboardStore.entity;
fetchPods(e, selectorStore.currentService.id, true);
} else {
selectorStore.setCurrentService(null);
}
@ -361,9 +362,7 @@ limitations under the License. -->
async function changePods(pod: Option[]) {
selectorStore.setCurrentPod(pod[0] || null);
if (dashboardStore.entity === EntityType[7].value) {
selectorStore.setCurrentProcess(null);
states.currentProcess = "";
if ([EntityType[7].value, EntityType[8].value].includes(dashboardStore.entity)) {
fetchProcess(true);
}
}
@ -445,12 +444,18 @@ limitations under the License. -->
case "addNetworkProfiling":
dashboardStore.addTabControls("NetworkProfiling");
break;
case "addContinuousProfiling":
dashboardStore.addTabControls("ContinuousProfiling");
break;
case "addTimeRange":
dashboardStore.addTabControls("TimeRange");
break;
case "addIframe":
dashboardStore.addTabControls("ThirdPartyApp");
break;
case "addTaskTimeline":
dashboardStore.addTabControls("TaskTimeline");
break;
default:
ElMessage.info("Don't support this control");
break;
@ -492,12 +497,18 @@ limitations under the License. -->
case "addNetworkProfiling":
dashboardStore.addControl("NetworkProfiling");
break;
case "addContinuousProfiling":
dashboardStore.addControl("ContinuousProfiling");
break;
case "addTimeRange":
dashboardStore.addControl("TimeRange");
break;
case "addIframe":
dashboardStore.addControl("ThirdPartyApp");
break;
case "addTaskTimeline":
dashboardStore.addControl("TaskTimeline");
break;
default:
dashboardStore.addControl("Widget");
}
@ -507,9 +518,16 @@ limitations under the License. -->
let resp;
switch (type) {
case EntityType[2].value:
resp = await selectorStore.getEndpoints({ serviceId, ...param });
if (setPod) {
updateCurrentPod();
if (params.podId) {
if (setPod) {
await setCurrentEndpoint();
}
resp = await selectorStore.getEndpoints({ serviceId, ...param });
} else {
resp = await selectorStore.getEndpoints({ serviceId, ...param });
if (setPod) {
updateCurrentPod();
}
}
break;
case EntityType[3].value:
@ -519,13 +537,24 @@ limitations under the License. -->
}
break;
case EntityType[6].value:
resp = await selectorStore.getEndpoints({
serviceId,
isRelation: true,
...param,
});
if (setPod) {
updateCurrentDestPod();
if (params.destPodId) {
if (setPod) {
await updateCurrentDestEndpoint();
}
resp = await selectorStore.getEndpoints({
serviceId,
isRelation: true,
...param,
});
} else {
resp = await selectorStore.getEndpoints({
serviceId,
isRelation: true,
...param,
});
if (setPod) {
updateCurrentDestPod();
}
}
break;
case EntityType[5].value:
@ -541,7 +570,7 @@ limitations under the License. -->
await fetchPods(EntityType[5].value, serviceId, setPod, param);
resp = await fetchDestProcess(setPod);
break;
case "Process":
case EntityType[8].value:
await fetchPods(EntityType[3].value, serviceId, setPod, param);
resp = await fetchProcess(setPod);
break;
@ -563,6 +592,9 @@ limitations under the License. -->
if (currentProcess) {
selectorStore.setCurrentProcess(currentProcess);
states.currentProcess = currentProcess.label;
} else {
selectorStore.setCurrentProcess(null);
states.currentProcess = "";
}
}
return resp;
@ -585,6 +617,19 @@ limitations under the License. -->
}
return resp;
}
async function updateCurrentDestEndpoint() {
const resp = await selectorStore.getEndpoint(params.destPodId, true);
if (resp.errors) {
return ElMessage.error(resp.errors);
}
const pod = resp.data.endpoint || {};
if (!pod.id) {
ElMessage.info("The Destination endpoint ID doesn't exist.");
}
selectorStore.setCurrentDestPod(pod);
states.currentDestPod = pod.label;
}
function updateCurrentDestPod() {
if (!(selectorStore.destPods.length && selectorStore.destPods[0])) {
@ -597,11 +642,27 @@ limitations under the License. -->
if (!currentDestPod) {
states.currentDestPod = "";
selectorStore.setCurrentDestPod(null);
ElMessage.info(
`The Destination ${params.entity === EntityType[6].value ? "endpoint" : "instance"} ID doesn't exist.`,
);
return;
}
selectorStore.setCurrentDestPod(currentDestPod);
states.currentDestPod = currentDestPod.label;
}
async function setCurrentEndpoint() {
const resp = await selectorStore.getEndpoint(params.podId);
if (resp.errors) {
return ElMessage.error(resp.errors);
}
const pod = resp.data.endpoint || {};
if (!pod.id) {
ElMessage.info("The endpoint ID doesn't exist.");
}
selectorStore.setCurrentPod(pod);
states.currentPod = pod.label || "";
}
function updateCurrentPod() {
if (!(selectorStore.pods.length && selectorStore.pods[0])) {
selectorStore.setCurrentPod(null);
@ -613,6 +674,7 @@ limitations under the License. -->
if (!currentPod) {
selectorStore.setCurrentPod(null);
states.currentPod = "";
ElMessage.info(`The ${params.entity === EntityType[2].value ? "endpoint" : "instance"} ID doesn't exist.`);
return;
}
selectorStore.setCurrentPod(currentPod);
@ -642,6 +704,9 @@ limitations under the License. -->
toolIcons.value = EndpointRelationTools;
break;
case EntityType[7].value:
toolIcons.value = ProcessRelationTools;
break;
case EntityType[8].value:
toolIcons.value = ProcessTools;
break;
default:
@ -673,8 +738,8 @@ limitations under the License. -->
<style lang="scss" scoped>
.dashboard-tool {
text-align: right;
padding: 3px 5px 5px 5px;
background: rgb(240, 242, 245);
padding: 3px 5px 5px;
background: rgb(240 242 245);
border-bottom: 1px solid #dfe4e8;
justify-content: space-between;
}

View File

@ -0,0 +1,51 @@
<!-- 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">
<policy-list />
<div class="flex-v list" v-loading="continousProfilingStore.instancesLoading">
<instance-list :config="config" />
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import PolicyList from "./components/PolicyList.vue";
import InstanceList from "./components/InstanceList.vue";
const continousProfilingStore = useContinousProfilingStore();
/*global defineProps */
defineProps({
config: {
type: Object as PropType<any>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped>
.content {
height: calc(100% - 50px);
width: 100%;
}
.list {
height: 100%;
flex-grow: 2;
min-width: 600px;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,134 @@
<!-- 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="policy-list">
<el-collapse v-model="activeNames">
<el-collapse-item v-for="(_, index) in policyList" :key="index" :name="String(index)">
<template #title>
<div>
<span class="title">{{ `Policy - ${index + 1}` }}</span>
<Icon
class="mr-5 cp"
iconName="remove_circle_outline"
size="middle"
v-show="policyList.length !== 1"
@click="removePolicy($event, index)"
/>
<Icon
class="cp"
v-show="index === policyList.length - 1"
iconName="add_circle_outlinecontrol_point"
size="middle"
@click="createPolicy"
/>
</div>
</template>
<Policy :policyList="policyList" @edit="changePolicy" :order="index" />
</el-collapse-item>
</el-collapse>
<div>
<el-button @click="save" type="primary" class="save-btn">
{{ t("save") }}
</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import Policy from "./Policy.vue";
import type { StrategyItem, CheckItems } from "@/types/continous-profiling";
/* global defineEmits, defineProps */
const props = defineProps({
policyList: {
type: Array as PropType<StrategyItem[]>,
default: () => [],
},
});
const emits = defineEmits(["save"]);
const { t } = useI18n();
const activeNames = ref(["0"]);
const policyList = ref<StrategyItem[]>([...props.policyList]);
function changePolicy(params: StrategyItem, order: number) {
policyList.value = policyList.value.map((d: StrategyItem, index: number) => {
if (order === index) {
return params;
}
return d;
});
}
function removePolicy(e: PointerEvent, key: number) {
e.stopPropagation();
if (policyList.value.length === 1) {
return;
}
policyList.value = policyList.value.filter((_, index: number) => index !== key);
}
function createPolicy(e: PointerEvent) {
e.stopPropagation();
policyList.value.push({
type: "",
checkItems: [
{
type: "",
threshold: "",
period: NaN,
count: NaN,
},
],
});
activeNames.value = [String(policyList.value.length - 1)];
}
function save() {
const params = [];
for (const d of policyList.value) {
const checkItems = d.checkItems.filter(
(item: CheckItems) => item.type && item.threshold && item.period && item.count,
);
if (d.type && checkItems.length) {
const v = {
targetType: d.type,
checkItems,
};
params.push(v);
}
}
emits("save", params);
}
</script>
<style lang="scss" scoped>
.policy-list {
margin: 0 auto;
width: 300px;
}
.save-btn {
width: 300px;
margin-top: 10px;
}
.title {
display: inline-block;
margin-right: 5px;
}
</style>

View File

@ -0,0 +1,169 @@
<!-- 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="header">
{{ t("monitorInstances") }}
</div>
<el-table :data="currentInstances" style="width: 99%" height="440">
<el-table-column type="expand">
<template #default="props">
<div class="child">
<div class="title">{{ t("attributes") }}</div>
<div v-for="(attr, index) in props.row.attributes" :key="index">
{{ `${attr.name}: ${attr.value}` }}
</div>
<div class="title mt-10">{{ t("processes") }}</div>
<el-table :data="props.row.processes" size="small" max-height="300">
<el-table-column prop="name" label="Name">
<template #default="scope">
<span
:class="config.processDashboardName ? 'link' : ''"
@click="viewProcessDashboard(scope.row, props.row)"
>
{{ scope.row.name }}
</span>
</template>
</el-table-column>
<el-table-column
v-for="item in HeaderChildLabels"
:key="item.value"
:label="item.label"
:prop="item.value"
:width="item.width"
/>
</el-table>
</div>
</template>
</el-table-column>
<el-table-column prop="name" label="Name">
<template #default="scope">
<span :class="config.instanceDashboardName ? 'link' : ''" @click="viewInstanceDashboard(scope.row)">
{{ scope.row.name }}
</span>
</template>
</el-table-column>
<el-table-column
v-for="item in HeaderLabels"
:key="item.value"
:label="item.label"
:prop="item.value"
:width="item.width"
/>
</el-table>
<el-pagination
class="mt-10"
small
background
layout="prev, pager, next"
:page-size="pageSize"
:total="continousProfilingStore.instances.length"
@current-change="changePage"
@prev-click="changePage"
@next-click="changePage"
/>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import type { MonitorInstance, MonitorProcess } from "@/types/continous-profiling";
import router from "@/router";
import { HeaderLabels, HeaderChildLabels } from "../data";
import { EntityType } from "../../../data";
/*global defineProps */
const props = defineProps({
config: {
type: Object as PropType<any>,
default: () => ({}),
},
});
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore();
const continousProfilingStore = useContinousProfilingStore();
const pageSize = 10;
const currentInstances = ref<MonitorInstance[]>([]);
function viewProcessDashboard(process: MonitorProcess, instance: MonitorInstance) {
if (!props.config.processDashboardName) {
return;
}
router.push(
`/dashboard/${dashboardStore.layerId}/${EntityType[8].value}/${selectorStore.currentService.id}/${instance.id}/${process.id}/${props.config.processDashboardName}`,
);
}
function viewInstanceDashboard(instance: MonitorInstance) {
if (!props.config.instanceDashboardName) {
return;
}
router.push(
`/dashboard/${dashboardStore.layerId}/${EntityType[3].value}/${selectorStore.currentService.id}/${instance.id}/${props.config.instanceDashboardName}`,
);
}
async function changePage(pageIndex: number) {
currentInstances.value = continousProfilingStore.instances.filter((d: unknown, index: number) => {
if (index >= (pageIndex - 1) * pageSize && index < pageIndex * pageSize) {
return d;
}
});
}
watch(
() => continousProfilingStore.instances,
() => {
currentInstances.value = continousProfilingStore.instances.filter(
(_: unknown, index: number) => index < pageSize,
);
},
);
</script>
<style lang="scss" scoped>
.title {
font-size: 12px;
font-weight: bold;
}
.child {
padding-left: 20px;
}
.header {
font-size: 13px;
font-weight: bold;
border-bottom: 1px solid rgb(0 0 0 / 7%);
padding: 10px 20px;
background-color: #f3f4f9;
}
.settings {
padding: 1px 0;
border: 1px solid #666;
border-radius: 3px;
color: #666;
cursor: pointer;
}
.link {
cursor: pointer;
color: #409eff;
text-decoration: underline;
}
</style>

View File

@ -0,0 +1,226 @@
<!-- 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>
<div class="label">{{ t("targetTypes") }}</div>
<Selector
class="profile-input"
size="small"
:value="states.type"
:options="TargetTypes"
placeholder="Select a type"
@change="changeType"
/>
</div>
<div v-for="(item, index) in states.checkItems" :key="index">
<div class="item-title">
<span class="title">{{ `Item - ${index + 1}` }}</span>
<Icon
class="ml-5 cp"
iconName="remove_circle_outline"
size="middle"
v-show="states.checkItems.length !== 1"
@click="removeItem($event, index)"
/>
<Icon
class="ml-5 cp"
v-show="index === states.checkItems.length - 1"
iconName="add_circle_outlinecontrol_point"
size="middle"
@click="createItem"
/>
</div>
<div>
<div class="label">{{ t("monitorType") }}</div>
<Selector
class="profile-input"
size="small"
:value="item.type"
:options="MonitorType"
placeholder="Select a type"
@change="changeMonitorType($event, index)"
/>
</div>
<div>
<div class="label">
<span class="mr-5">{{ t("threshold") }}</span>
<span>({{ getNotice(item.type) }} )</span>
</div>
<el-input
type="number"
size="small"
class="profile-input"
v-model="item.threshold"
@change="changeThreshold(index)"
/>
</div>
<div>
<div class="label">{{ t("count") }}</div>
<el-input-number size="small" class="profile-input" :min="0" v-model="item.count" @change="changeParam" />
</div>
<div>
<div class="label">{{ t("period") }}</div>
<el-input-number size="small" class="profile-input" :min="0" v-model="item.period" @change="changeParam" />
</div>
<div v-show="TYPES.includes(item.type)">
<div class="label">{{ t("uriRegex") }}</div>
<el-input size="small" class="profile-input" v-model="item.uriRegex" @change="changeParam(index)" />
</div>
<div v-show="TYPES.includes(item.type)">
<div class="label">{{ t("uriList") }}</div>
<div id="uri-param" contenteditable="true" @input="changeURI($event, index)" class="profile-input">
{{ (item.uriList || []).join("; ") }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { ElMessage } from "element-plus";
import type { StrategyItem, CheckItems } from "@/types/continous-profiling";
import { MonitorType, TargetTypes } from "../data";
/* global defineEmits, defineProps */
const props = defineProps({
policyList: {
type: Object as PropType<StrategyItem[]>,
default: () => ({}),
},
order: {
type: Number,
default: 0,
},
});
const emits = defineEmits(["edit"]);
const { t } = useI18n();
const states = reactive<StrategyItem>(props.policyList[props.order]);
const TYPES = ["HTTP_ERROR_RATE", "HTTP_AVG_RESPONSE_TIME"];
function changeType(opt: { value: string }[]) {
const types = props.policyList.map((item: StrategyItem) => item.type);
if (types.includes(opt[0].value)) {
return ElMessage.warning("Target type cannot be configured repeatedly.");
}
states.type = opt[0].value;
emits("edit", states, props.order);
}
function changeMonitorType(opt: { value: string }[], index: number) {
const types = states.checkItems.map((item: CheckItems) => item.type);
if (types.includes(opt[0].value)) {
return ElMessage.warning("Monitor type cannot be configured repeatedly.");
}
states.checkItems[index].type = opt[0].value;
emits("edit", states, props.order);
}
function changeURI(event: any, index: number) {
if (states.checkItems[index].uriRegex) {
return ElMessage.warning("UriList or UriRegex only be configured with one option.");
}
const params = (event.target.textContent || "").replace(/\s+/g, "");
const arr = params.splice(";");
states.checkItems[index].uriList = arr.length ? arr : null;
emits("edit", states, props.order);
}
function changeThreshold(index: number) {
let regex = /^(100(\.0{1,2})?|[1-9]?\d(\.\d{1,2})?)$/;
if (MonitorType[1].value === states.checkItems[index].type) {
regex = /^\d+$/;
}
if (MonitorType[2].value === states.checkItems[index].type) {
regex = /^(\d+)(\.\d+)?$/;
}
if (MonitorType[4].value === states.checkItems[index].type) {
regex = /^[+]{0,1}(\d+)$|^[+]{0,1}(\d+\.\d+)$/;
}
if (!regex.test(states.checkItems[index].threshold)) {
return ElMessage.error(getNotice(states.checkItems[index].type));
}
emits("edit", states, props.order);
}
function changeParam(index?: any) {
if (index !== undefined && (states.checkItems[index] || {}).uriList) {
return ElMessage.warning("UriList or UriRegex only be configured with one option");
}
const checkItems = states.checkItems.map((d: CheckItems) => {
d.count = Number(d.count);
d.period = Number(d.period);
return d;
});
emits("edit", { ...states, checkItems }, props.order);
}
function createItem(e: PointerEvent) {
e.stopPropagation();
states.checkItems.push({
type: "",
threshold: "",
period: NaN,
count: NaN,
});
emits("edit", states, props.order);
}
function removeItem(e: PointerEvent, key: number) {
e.stopPropagation();
if (states.checkItems.length === 1) {
return;
}
states.checkItems = states.checkItems.filter((_, index: number) => index !== key);
emits("edit", states, props.order);
}
function getNotice(type: string) {
const map: { [key: string]: string } = {
PROCESS_CPU: "It is a percentage data",
PROCESS_THREAD_COUNT: "It is a positive integer",
SYSTEM_LOAD: "It is a floating point number",
HTTP_ERROR_RATE: "It is percentage data",
HTTP_AVG_RESPONSE_TIME: "It is a response time in milliseconds",
};
return map[type];
}
</script>
<style lang="scss" scoped>
.profile-input {
width: 300px;
margin-bottom: 10px;
}
#uri-param {
border: 1px solid #dcdfe6;
cursor: text;
padding: 0 5px;
border-radius: 4px;
color: #606266;
outline: none;
height: 100px;
&:focus {
border-color: #409eff;
}
}
.item-title {
margin-bottom: 5px;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,206 @@
<!-- 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="continousProfilingStore.policyLoading">
<div class="profile-task-wrapper flex-v">
<div class="profile-t-tool">
<span>{{ t("policyList") }}</span>
<span class="new-task cp" @click="setStrategies">
<Icon iconName="edit" size="middle" />
</span>
</div>
<div class="profile-t-wrapper">
<div class="no-data" v-show="!continousProfilingStore.strategyList.length">
{{ t("noData") }}
</div>
<table class="profile-t">
<tr
class="profile-tr cp"
v-for="(i, index) in continousProfilingStore.strategyList"
@click="changePolicy(i)"
:key="index"
>
<td
class="profile-td"
:class="{
selected: continousProfilingStore.selectedStrategy.id === i.id,
}"
v-if="i.type"
>
<div class="ell">
<span class="sm">
{{ i.type }}
</span>
</div>
<div class="grey ell sm" v-for="(item, index) in i.checkItems" :key="index">
<span class="sm">
{{
`${item.type} >= ${item.threshold}${
[MonitorType[0].value, MonitorType[3].value].includes(item.type) ? "%" : ""
}; `
}}
</span>
</div>
</td>
</tr>
</table>
</div>
</div>
</div>
<el-dialog
v-model="updateStrategies"
:title="t('editStrategy')"
:destroy-on-close="true"
fullscreen
@closed="updateStrategies = false"
>
<EditPolicy :policyList="continousProfilingStore.strategyList" @save="editStrategies" />
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import { useSelectorStore } from "@/store/modules/selectors";
import type { StrategyItem, CheckItems } from "@/types/continous-profiling";
import { ElMessage } from "element-plus";
import EditPolicy from "./EditPolicy.vue";
import { MonitorType } from "../data";
const { t } = useI18n();
const selectorStore = useSelectorStore();
const continousProfilingStore = useContinousProfilingStore();
const updateStrategies = ref<boolean>(false);
const inProcess = ref<boolean>(false);
fetchStrategyList();
async function changePolicy(item: StrategyItem) {
continousProfilingStore.setSelectedStrategy(item);
const serviceId = (selectorStore.currentService && selectorStore.currentService.id) || "";
await continousProfilingStore.getMonitoringInstances(serviceId);
}
function setStrategies() {
updateStrategies.value = true;
}
async function editStrategies(
targets: {
targetType: string;
checkItems: CheckItems[];
}[],
) {
const serviceId = (selectorStore.currentService && selectorStore.currentService.id) || "";
if (!serviceId) {
return ElMessage.error("No Service ID");
}
const res = await continousProfilingStore.setContinuousProfilingPolicy(serviceId, targets);
if (res.errors) {
ElMessage.error(res.errors);
return;
}
if (!res.data.strategy.status) {
ElMessage.error(res.data.strategy.errorReason);
return;
}
updateStrategies.value = false;
await fetchStrategyList();
}
async function fetchStrategyList() {
const serviceId = (selectorStore.currentService && selectorStore.currentService.id) || "";
const res = await continousProfilingStore.getStrategyList({
serviceId,
});
if (res.errors) {
return ElMessage.error(res.errors);
}
if (!continousProfilingStore.strategyList.length) {
return;
}
}
watch(
() => selectorStore.currentService,
() => {
inProcess.value = false;
fetchStrategyList();
},
);
</script>
<style lang="scss" scoped>
.profile-task-list {
width: 300px;
height: 98%;
overflow: auto;
border-right: 1px solid rgb(0 0 0 / 10%);
}
.item span {
height: 21px;
}
.profile-td {
padding: 10px 5px 10px 10px;
border-bottom: 1px solid rgb(0 0 0 / 7%);
&.selected {
background-color: #ededed;
}
}
.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;
border: none;
}
.profile-tr {
&:hover {
background-color: rgb(0 0 0 / 4%);
}
}
.profile-t-tool {
padding: 10px 5px 10px 10px;
border-bottom: 1px solid rgb(0 0 0 / 7%);
background: #f3f4f9;
width: 100%;
font-weight: bold;
}
.new-task {
float: right;
}
.reload {
margin-left: 30px;
}
</style>

View File

@ -0,0 +1,44 @@
/**
* 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 MonitorType: any = [
{ label: "PROCESS_CPU", value: "PROCESS_CPU" },
{ label: "PROCESS_THREAD_COUNT", value: "PROCESS_THREAD_COUNT" },
{ label: "SYSTEM_LOAD", value: "SYSTEM_LOAD" },
{ label: "HTTP_ERROR_RATE", value: "HTTP_ERROR_RATE" },
{ label: "HTTP_AVG_RESPONSE_TIME", value: "HTTP_AVG_RESPONSE_TIME" },
];
export const TargetTypes = [
{ label: "ON_CPU", value: "ON_CPU" },
{ label: "OFF_CPU", value: "OFF_CPU" },
{ label: "NETWORK", value: "NETWORK" },
];
export const ComponentType = "CONTINOUS_PROFILING";
export const HeaderLabels = [
{ value: "triggeredCount", label: "Triggered Count", width: 150 },
{ value: "lastTriggerTime", label: "Last Trigger Time", width: 170 },
];
export const HeaderChildLabels = [
{ value: "detectType", label: "Detect Type", width: 100 },
{ value: "triggeredCount", label: "Triggered Count", width: 120 },
{ value: "lastTriggerTime", label: "Last Trigger Time", width: 160 },
{ value: "labels", label: "Labels" },
];

View File

@ -33,6 +33,7 @@ limitations under the License. -->
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { EntityType } from "../../data";
import { EBPFProfilingTriggerType } from "@/store/data";
/*global defineProps */
const props = defineProps({
@ -54,6 +55,7 @@ limitations under the License. -->
const res = await ebpfStore.getTaskList({
serviceId,
targets: ["ON_CPU", "OFF_CPU"],
triggerType: EBPFProfilingTriggerType.FIXED_TIME,
});
if (res.errors) {

View File

@ -42,7 +42,7 @@ limitations under the License. -->
/>
<el-popover placement="bottom" :width="680" trigger="click" :persistent="false">
<template #reference>
<el-button type="primary" size="small">
<el-button size="small">
{{ t("processSelect") }}
</el-button>
</template>
@ -98,12 +98,21 @@ limitations under the License. -->
import type { Option } from "@/types/app";
import { TableHeader, AggregateTypes } from "./data";
import { useEbpfStore } from "@/store/modules/ebpf";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import type { EBPFProfilingSchedule, Process } from "@/types/ebpf";
import { ElMessage, ElTable } from "element-plus";
import { dateFormat } from "@/utils/dateFormat";
import { ComponentType } from "@/views/dashboard/related/continuous-profiling/data";
const { t } = useI18n();
const ebpfStore = useEbpfStore();
/*global defineProps*/
const props = defineProps({
type: {
type: String,
default: "",
},
});
const ebpfStore = props.type === ComponentType ? useContinousProfilingStore() : useEbpfStore();
const pageSize = 5;
const multipleTableRef = ref<InstanceType<typeof ElTable>>();
const selectedProcesses = ref<string[]>([]);

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div id="graph-stack" ref="graph">
<span class="tip" v-show="ebpfStore.tip">{{ ebpfStore.tip }}</span>
<span class="tip" v-show="ebpfStore.ebpfTips">{{ ebpfStore.ebpfTips }}</span>
</div>
</template>
<script lang="ts" setup>
@ -23,12 +23,20 @@ limitations under the License. -->
import d3tip from "d3-tip";
import { flamegraph } from "d3-flame-graph";
import { useEbpfStore } from "@/store/modules/ebpf";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import { ComponentType } from "@/views/dashboard/related/continuous-profiling/data";
import type { StackElement } from "@/types/ebpf";
import { AggregateTypes } from "./data";
import "d3-flame-graph/dist/d3-flamegraph.css";
/*global Nullable*/
const ebpfStore = useEbpfStore();
/*global Nullable, defineProps*/
const props = defineProps({
type: {
type: String,
default: "",
},
});
const ebpfStore = props.type === ComponentType ? useContinousProfilingStore() : useEbpfStore();
const stackTree = ref<Nullable<StackElement>>(null);
const selectStack = ref<Nullable<StackElement>>(null);
const graph = ref<Nullable<HTMLDivElement>>(null);
@ -90,7 +98,7 @@ limitations under the License. -->
.setColorMapper((d, originalColor) => (d.highlight ? "#6aff8f" : originalColor));
const tip = (d3tip as any)()
.attr("class", "d3-tip")
.direction("w")
.direction("s")
.html((d: { data: StackElement } & { parent: { data: StackElement } }) => {
const name = d.data.name.replace("<", "&lt;").replace(">", "&gt;");
const valStr =
@ -111,7 +119,7 @@ limitations under the License. -->
}</div>`;
return `<div class="mb-5 name">Symbol: ${name}</div>${valStr}${rateOfParent}${rateOfRoot}`;
})
.style("max-width", "500px");
.style("max-width", "400px");
flameChart.value.tooltip(tip);
d3.select("#graph-stack").datum(stackTree.value).call(flameChart.value);
}

View File

@ -34,7 +34,7 @@ limitations under the License. -->
defineProps({
config: {
type: Object as PropType<any>,
default: () => ({ graph: {} }),
default: () => ({}),
},
});
const networkProfilingStore = useNetworkProfilingStore();

View File

@ -19,7 +19,7 @@ limitations under the License. -->
<g class="hex-polygon">
<path :d="getHexPolygonVertices()" stroke="#D5DDF6" stroke-width="2" fill="none" />
<text :x="0" :y="radius - 15" fill="#000" text-anchor="middle">
{{ selectorStore.currentPod.label }}
{{ selectorStore.currentPod && selectorStore.currentPod.label }}
</text>
</g>
<g class="nodes">
@ -530,11 +530,11 @@ limitations under the License. -->
border-radius: 3px;
position: absolute;
top: 10px;
right: 10px;
right: 20px;
}
.range {
right: 50px;
right: 60px;
}
.topo-call {

View File

@ -77,6 +77,7 @@ limitations under the License. -->
import getLocalTime from "@/utils/localtime";
import { useAppStoreWithOut } from "@/store/modules/app";
import NewTask from "./NewTask.vue";
import { EBPFProfilingTriggerType } from "@/store/data";
/*global Nullable */
const { t } = useI18n();
@ -185,6 +186,7 @@ limitations under the License. -->
serviceId,
serviceInstanceId,
targets: ["NETWORK"],
triggerType: EBPFProfilingTriggerType.FIXED_TIME,
});
if (res.errors) {
@ -214,7 +216,7 @@ limitations under the License. -->
width: 330px;
height: calc(100% - 10px);
overflow: auto;
border-right: 1px solid rgba(0, 0, 0, 0.1);
border-right: 1px solid rgb(0 0 0 / 10%);
}
.item span {
@ -223,7 +225,7 @@ limitations under the License. -->
.profile-td {
padding: 10px 5px 10px 10px;
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
border-bottom: 1px solid rgb(0 0 0 / 7%);
&.selected {
background-color: #ededed;
@ -251,13 +253,13 @@ limitations under the License. -->
.profile-tr {
&:hover {
background-color: rgba(0, 0, 0, 0.04);
background-color: rgb(0 0 0 / 4%);
}
}
.profile-t-tool {
padding: 10px 5px 10px 10px;
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
border-bottom: 1px solid rgb(0 0 0 / 7%);
background: #f3f4f9;
width: 100%;
}

View File

@ -0,0 +1,40 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="content">
<Timeline />
<ProfilingPanel :config="config" />
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import Timeline from "./components/Timeline.vue";
import ProfilingPanel from "./components/ProfilingPanel.vue";
/* global defineProps */
defineProps({
config: {
type: Object as PropType<any>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped>
.content {
height: calc(100% - 50px);
width: 100%;
padding: 0 10px;
}
</style>

View File

@ -0,0 +1,83 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="content" v-if="taskTimelineStore.selectedTask.targetType === TargetTypes[2].value">
<process-topology v-if="networkProfilingStore.nodes.length" :config="config" />
</div>
<div
class="content"
v-if="[TargetTypes[1].value, TargetTypes[0].value].includes(taskTimelineStore.selectedTask.targetType)"
>
<div class="schedules">
<EBPFSchedules />
</div>
<div class="item">
<EBPFStack />
</div>
</div>
<div class="text" v-if="!taskTimelineStore.selectedTask.targetType">
{{ t("noData") }}
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { useTaskTimelineStore } from "@/store/modules/task-timeline";
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
import { TargetTypes } from "../../continuous-profiling/data";
import ProcessTopology from "@/views/dashboard/related/network-profiling/components/ProcessTopology.vue";
import EBPFSchedules from "@/views/dashboard/related/ebpf/components/EBPFSchedules.vue";
import EBPFStack from "@/views/dashboard/related/ebpf/components/EBPFStack.vue";
/*global defineProps */
defineProps({
config: {
type: Object as PropType<any>,
default: () => ({}),
},
});
const { t } = useI18n();
const taskTimelineStore = useTaskTimelineStore();
const networkProfilingStore = useNetworkProfilingStore();
</script>
<style lang="scss" scoped>
.content {
width: 100%;
height: calc(100% - 30px);
flex-grow: 2;
min-width: 700px;
overflow: hidden;
position: relative;
}
.text {
width: 100%;
text-align: center;
margin-top: 100px;
}
.item {
width: 100%;
overflow: auto;
height: calc(100% - 270px);
padding-bottom: 10px;
}
.schedules {
height: 90px;
border-bottom: 1px solid #ccc;
padding-right: 10px;
}
</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="timeline" class="task-timeline"></div>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted, onUnmounted } from "vue";
import dayjs from "dayjs";
import { useThrottleFn } from "@vueuse/core";
import { ElMessage } from "element-plus";
import type { EBPFTaskList } from "@/types/ebpf";
import { useTaskTimelineStore } from "@/store/modules/task-timeline";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import { useContinousProfilingStore } from "@/store/modules/continous-profiling";
import { DataSet, Timeline } from "vis-timeline/standalone";
import "vis-timeline/styles/vis-timeline-graph2d.css";
import { EBPFProfilingTriggerType } from "@/store/data";
const taskTimelineStore = useTaskTimelineStore();
const selectorStore = useSelectorStore();
const continousProfilingStore = useContinousProfilingStore();
const dashboardStore = useDashboardStore();
/* global defineProps, Nullable */
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
});
const timeline = ref<Nullable<HTMLDivElement>>(null);
const visGraph = ref<Nullable<any>>(null);
const oldVal = ref<{ width: number; height: number }>({ width: 0, height: 0 });
const visDate = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") => dayjs(date).format(pattern);
init();
onMounted(() => {
oldVal.value = (timeline.value && timeline.value.getBoundingClientRect()) || {
width: 0,
height: 0,
};
useThrottleFn(resize, 500)();
});
async function init() {
const serviceId = (selectorStore.currentService && selectorStore.currentService.id) || "";
const serviceInstanceId = (selectorStore.currentPod && selectorStore.currentPod.id) || "";
const type = continousProfilingStore.selectedStrategy.type;
const res = await taskTimelineStore.getContinousTaskList({
serviceId,
serviceInstanceId,
targets: type ? [type] : null,
triggerType: EBPFProfilingTriggerType.CONTINUOUS_PROFILING,
});
if (res.errors) {
ElMessage.error(res.errors);
return;
}
visTimeline();
}
function visTimeline() {
if (!timeline.value) {
return;
}
if (visGraph.value) {
visGraph.value.destroy();
}
const h = timeline.value.getBoundingClientRect().height;
const taskList = taskTimelineStore.taskList.map((d: EBPFTaskList, index: number) => {
return {
id: index,
// content: d.targetType,
start: new Date(Number(d.taskStartTime)),
end: new Date(Number(d.taskStartTime + d.fixedTriggerDuration * 1000)),
data: d,
className: d.targetType,
};
});
const items: any = new DataSet(taskList);
const options: any = {
height: h,
width: "100%",
locale: "en",
groupHeightMode: "fitItems",
autoResize: false,
tooltip: {
overflowMethod: "cap",
template(item: EBPFTaskList | any) {
const data = item.data || {};
const end = data.taskStartTime ? visDate(data.taskStartTime + data.fixedTriggerDuration * 1000) : "";
let str = "";
for (const item of data.continuousProfilingCauses || []) {
str += `${item.message};`;
}
let tmp = `
<div>Task ID: ${data.taskId || ""}</div>
<div>Target Type: ${data.targetType || ""}</div>
<div>Trigger Type: ${data.triggerType || ""}</div>
<div>Start Time: ${data.taskStartTime ? visDate(data.taskStartTime) : ""}</div>
<div>End Time: ${end}</div>
<div>Causes: ${str}</div>
<div>Process Labels: ${data.processLabels.join("; ") || ""}</div>`;
return tmp;
},
},
};
visGraph.value = new Timeline(timeline.value, items, options);
visGraph.value.on("select", async (properties: { items: number[] }) => {
dashboardStore.selectWidget(props.data);
const index = properties.items[0];
const task = taskTimelineStore.taskList[index];
await taskTimelineStore.setSelectedTask(task);
await taskTimelineStore.getGraphData();
});
}
function resize() {
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
const cr = entry.contentRect;
if (Math.abs(cr.width - oldVal.value.width) < 3 && Math.abs(cr.height - oldVal.value.height) < 3) {
return;
}
visTimeline();
oldVal.value = { width: cr.width, height: cr.height };
});
if (timeline.value) {
observer.observe(timeline.value);
}
}
onUnmounted(() => {
if (visGraph.value) {
visGraph.value.destroy();
}
taskTimelineStore.setTaskList([]);
});
watch(
() => selectorStore.currentProcess,
() => {
init();
},
);
</script>
<style lang="scss" scoped>
.task-timeline {
width: calc(100% - 5px);
margin: 0 5px 5px 0;
height: 200px;
}
.message {
max-width: 400px;
text-overflow: ellipsis;
overflow: hidden;
}
</style>