feat: Implement task timeline and policy list widget for continous profiling (#280)
@ -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 |
17
src/assets/icons/continuous_profiling.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<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
@ -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 |
@ -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 |
17
src/assets/icons/task_timeline.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<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 |
@ -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>
|
||||
|
@ -33,9 +33,10 @@ 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
|
||||
@ -111,3 +112,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
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
@ -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}}`;
|
||||
|
@ -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}}`;
|
||||
|
@ -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",
|
||||
@ -388,6 +389,19 @@ const msg = {
|
||||
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",
|
||||
|
@ -387,6 +387,20 @@ const msg = {
|
||||
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",
|
||||
|
@ -385,6 +385,20 @@ const msg = {
|
||||
elasticsearch: "Elasticsearch",
|
||||
mq: "消息队列",
|
||||
rabbitMQ: "RabbitMQ",
|
||||
save: "保存",
|
||||
editStrategy: "编辑策略",
|
||||
policyList: "策略列表",
|
||||
targetTypes: "目标类型",
|
||||
monitorType: "监视器类型",
|
||||
count: "总数",
|
||||
threshold: "阈值",
|
||||
uriRegex: "URI规则",
|
||||
uriList: "URI列表",
|
||||
processes: "进程",
|
||||
attributes: "属性",
|
||||
monitorInstances: "监视实例",
|
||||
processDashboards: "进程仪表板",
|
||||
instanceDashboards: "实例仪表板",
|
||||
detailLabel: "详细标签",
|
||||
summary: "概括",
|
||||
detail: "详细",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
}
|
||||
|
146
src/store/modules/continous-profiling.ts
Normal file
@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 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 { AxiosResponse } from "axios";
|
||||
|
||||
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;
|
||||
}
|
||||
this.strategyList = (res.data.data.strategyList || []).map((d: StrategyItem, index: number) => {
|
||||
return {
|
||||
...d,
|
||||
id: index,
|
||||
};
|
||||
});
|
||||
this.setSelectedStrategy(this.strategyList[0] || {});
|
||||
if (!this.strategyList.length) {
|
||||
this.taskList = [];
|
||||
}
|
||||
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) {
|
||||
this.instances = res.data.data.instances || [];
|
||||
this.instance = this.instances[0] || null;
|
||||
}
|
||||
return res.data;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function useContinousProfilingStore(): Recordable {
|
||||
return continousProfilingStore(store);
|
||||
}
|
@ -92,6 +92,7 @@ export const dashboardStore = defineStore({
|
||||
metricTypes: [""],
|
||||
metrics: [""],
|
||||
};
|
||||
|
||||
if (type === "Widget") {
|
||||
newItem.metricMode = MetricModes.Expression;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
},
|
||||
|
131
src/store/modules/task-timeline.ts
Normal file
@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
this.taskList = res.data.data.queryEBPFTasks || [];
|
||||
// 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);
|
||||
}
|
@ -212,3 +212,6 @@ div.vis-tooltip {
|
||||
div:has(> a.menu-title) {
|
||||
display: none;
|
||||
}
|
||||
.el-input-number .el-input__inner {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
45
src/types/continous-profiling.d.ts
vendored
Normal 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;
|
||||
}
|
19
src/types/ebpf.d.ts
vendored
@ -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 {
|
||||
|
108
src/views/dashboard/configuration/ContinuousProfiling.vue
Normal 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>
|
@ -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,
|
||||
};
|
||||
|
@ -28,7 +28,7 @@ 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"
|
||||
@ -70,7 +70,7 @@ limitations under the License. -->
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="item mb-10" v-show="isExec">
|
||||
<div class="item mb-10" v-show="!isExpression">
|
||||
<span class="label">{{ t("aggregation") }}</span>
|
||||
<SelectSingle
|
||||
:value="currentMetric.calculation"
|
||||
@ -111,7 +111,7 @@ limitations under the License. -->
|
||||
import { SortOrder, CalculationOpts, MetricModes } from "../../../data";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import type { MetricConfigOpt } from "@/types/dashboard";
|
||||
import { ListChartTypes, ProtocolTypes, ExpressionResultType } from "../../../data";
|
||||
import { ListChartTypes, ProtocolTypes } from "../../../data";
|
||||
|
||||
/*global defineEmits, defineProps */
|
||||
const props = defineProps({
|
||||
@ -137,11 +137,7 @@ limitations under the License. -->
|
||||
const graph = dashboardStore.selectedGrid.graph || {};
|
||||
return (
|
||||
ListChartTypes.includes(graph.type) ||
|
||||
[
|
||||
ProtocolTypes.ReadLabeledMetricsValues,
|
||||
ProtocolTypes.ReadMetricsValues,
|
||||
ExpressionResultType.TIME_SERIES_VALUES,
|
||||
].includes(metricType.value)
|
||||
[ProtocolTypes.ReadLabeledMetricsValues, ProtocolTypes.ReadMetricsValues].includes(metricType.value)
|
||||
);
|
||||
});
|
||||
const isList = computed(() => {
|
||||
@ -153,7 +149,7 @@ limitations under the License. -->
|
||||
metricTypes.value[props.index],
|
||||
),
|
||||
);
|
||||
const isExec = computed(() => dashboardStore.selectedGrid.metricMode === MetricModes.General);
|
||||
|
||||
function updateConfig(index: number, param: { [key: string]: string }) {
|
||||
const key = Object.keys(param)[0];
|
||||
if (!key) {
|
||||
|
100
src/views/dashboard/controls/ContinuousProfiling.vue
Normal 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>
|
92
src/views/dashboard/controls/TaskTimeline.vue
Normal 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>
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -176,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,
|
||||
@ -203,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" },
|
||||
@ -234,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 = [
|
||||
|
@ -38,6 +38,7 @@
|
||||
color: #409eff;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.search {
|
||||
|
@ -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"
|
||||
@ -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();
|
||||
@ -317,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);
|
||||
@ -337,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);
|
||||
}
|
||||
@ -362,7 +362,7 @@ limitations under the License. -->
|
||||
|
||||
async function changePods(pod: Option[]) {
|
||||
selectorStore.setCurrentPod(pod[0] || null);
|
||||
if (dashboardStore.entity === EntityType[7].value) {
|
||||
if ([EntityType[7].value, EntityType[8].value].includes(dashboardStore.entity)) {
|
||||
selectorStore.setCurrentProcess(null);
|
||||
states.currentProcess = "";
|
||||
fetchProcess(true);
|
||||
@ -446,12 +446,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;
|
||||
@ -493,12 +499,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");
|
||||
}
|
||||
@ -560,7 +572,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;
|
||||
@ -691,6 +703,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:
|
||||
@ -722,8 +737,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;
|
||||
}
|
||||
|
51
src/views/dashboard/related/continuous-profiling/Content.vue
Normal 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>
|
@ -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>
|
@ -0,0 +1,185 @@
|
||||
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<div class="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="instances.length"
|
||||
@current-change="changePage"
|
||||
@prev-click="changePage"
|
||||
@next-click="changePage"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, 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";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
|
||||
/*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 instances = computed(() => {
|
||||
return continousProfilingStore.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);
|
||||
});
|
||||
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 = instances.value.filter((d: unknown, index: number) => {
|
||||
if (index >= (pageIndex - 1) * pageSize && index < pageIndex * pageSize) {
|
||||
return d;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => instances.value,
|
||||
() => {
|
||||
currentInstances.value = instances.value.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>
|
@ -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">{{ t("count") }}</div>
|
||||
<el-input-number size="small" class="profile-input" :min="0" v-model="item.count" @change="changeParam" />
|
||||
</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("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>
|
@ -0,0 +1,205 @@
|
||||
<!-- 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,
|
||||
}"
|
||||
>
|
||||
<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>
|
44
src/views/dashboard/related/continuous-profiling/data.ts
Normal 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" },
|
||||
];
|
@ -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) {
|
||||
|
@ -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[]>([]);
|
||||
|
@ -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("<", "<").replace(">", ">");
|
||||
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);
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ limitations under the License. -->
|
||||
defineProps({
|
||||
config: {
|
||||
type: Object as PropType<any>,
|
||||
default: () => ({ graph: {} }),
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
const networkProfilingStore = useNetworkProfilingStore();
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
40
src/views/dashboard/related/task-timeline/Content.vue
Normal 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="flex-v 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>
|
@ -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% - 100px);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.schedules {
|
||||
height: 90px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding-right: 10px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,177 @@
|
||||
<!-- 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 tmp = `
|
||||
<div>Task ID: ${data.taskId || ""}</div>
|
||||
<div>Service Name: ${data.serviceName || ""}</div>
|
||||
<div>Service Instance Name: ${data.serviceInstanceName || ""}</div>
|
||||
<div>Service Process Name: ${data.processName || ""}</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>Process Labels: ${data.processLabels.join("; ") || ""}</div>`;
|
||||
let str = "";
|
||||
for (const item of data.continuousProfilingCauses || []) {
|
||||
str += `<div>${item.type}: ${getURI(item.uri)}${item.uri.threshold}>=${item.uri.current}</div>`;
|
||||
}
|
||||
return tmp + str;
|
||||
},
|
||||
},
|
||||
};
|
||||
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 getURI(uri: { uriRegex: string; uriPath: string }) {
|
||||
return uri ? `(${uri.uriRegex || ""} | ${uri.uriPath || ""})` : "";
|
||||
}
|
||||
|
||||
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.currentPod,
|
||||
() => {
|
||||
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>
|