feat: Implement task timeline and policy list widget for continous profiling (#280)

This commit is contained in:
Fine0830
2023-06-12 16:17:38 +08:00
committed by GitHub
parent 7738695601
commit 22db68646c
48 changed files with 2088 additions and 45 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,6 +38,7 @@
color: #409eff;
display: inline-block;
width: 100%;
text-decoration: underline;
}
.search {

View File

@@ -27,7 +27,7 @@ limitations under the License. -->
class="selectors"
/>
</div>
<div class="selectors-item" v-if="key === 3 || key === 4 || key === 5">
<div class="selectors-item" v-if="key === 3 || key === 4 || key === 5 || key === 6">
<span class="label">
{{ ["EndpointRelation", "Endpoint"].includes(dashboardStore.entity) ? "$Endpoint" : "$ServiceInstance" }}
</span>
@@ -42,7 +42,7 @@ limitations under the License. -->
:isRemote="['EndpointRelation', 'Endpoint'].includes(dashboardStore.entity)"
/>
</div>
<div class="selectors-item" v-if="key === 5">
<div class="selectors-item" v-if="key === 5 || key === 6">
<span class="label"> $Process </span>
<Selector
v-model="states.currentProcess"
@@ -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;
}

View File

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

View File

@@ -0,0 +1,134 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="policy-list">
<el-collapse v-model="activeNames">
<el-collapse-item v-for="(_, index) in policyList" :key="index" :name="String(index)">
<template #title>
<div>
<span class="title">{{ `Policy - ${index + 1}` }}</span>
<Icon
class="mr-5 cp"
iconName="remove_circle_outline"
size="middle"
v-show="policyList.length !== 1"
@click="removePolicy($event, index)"
/>
<Icon
class="cp"
v-show="index === policyList.length - 1"
iconName="add_circle_outlinecontrol_point"
size="middle"
@click="createPolicy"
/>
</div>
</template>
<Policy :policyList="policyList" @edit="changePolicy" :order="index" />
</el-collapse-item>
</el-collapse>
<div>
<el-button @click="save" type="primary" class="save-btn">
{{ t("save") }}
</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import Policy from "./Policy.vue";
import type { StrategyItem, CheckItems } from "@/types/continous-profiling";
/* global defineEmits, defineProps */
const props = defineProps({
policyList: {
type: Array as PropType<StrategyItem[]>,
default: () => [],
},
});
const emits = defineEmits(["save"]);
const { t } = useI18n();
const activeNames = ref(["0"]);
const policyList = ref<StrategyItem[]>([...props.policyList]);
function changePolicy(params: StrategyItem, order: number) {
policyList.value = policyList.value.map((d: StrategyItem, index: number) => {
if (order === index) {
return params;
}
return d;
});
}
function removePolicy(e: PointerEvent, key: number) {
e.stopPropagation();
if (policyList.value.length === 1) {
return;
}
policyList.value = policyList.value.filter((_, index: number) => index !== key);
}
function createPolicy(e: PointerEvent) {
e.stopPropagation();
policyList.value.push({
type: "",
checkItems: [
{
type: "",
threshold: "",
period: NaN,
count: NaN,
},
],
});
activeNames.value = [String(policyList.value.length - 1)];
}
function save() {
const params = [];
for (const d of policyList.value) {
const checkItems = d.checkItems.filter(
(item: CheckItems) => item.type && item.threshold && item.period && item.count,
);
if (d.type && checkItems.length) {
const v = {
targetType: d.type,
checkItems,
};
params.push(v);
}
}
emits("save", params);
}
</script>
<style lang="scss" scoped>
.policy-list {
margin: 0 auto;
width: 300px;
}
.save-btn {
width: 300px;
margin-top: 10px;
}
.title {
display: inline-block;
margin-right: 5px;
}
</style>

View File

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

View File

@@ -0,0 +1,226 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div>
<div class="label">{{ t("targetTypes") }}</div>
<Selector
class="profile-input"
size="small"
:value="states.type"
:options="TargetTypes"
placeholder="Select a type"
@change="changeType"
/>
</div>
<div v-for="(item, index) in states.checkItems" :key="index">
<div class="item-title">
<span class="title">{{ `Item - ${index + 1}` }}</span>
<Icon
class="ml-5 cp"
iconName="remove_circle_outline"
size="middle"
v-show="states.checkItems.length !== 1"
@click="removeItem($event, index)"
/>
<Icon
class="ml-5 cp"
v-show="index === states.checkItems.length - 1"
iconName="add_circle_outlinecontrol_point"
size="middle"
@click="createItem"
/>
</div>
<div>
<div class="label">{{ t("monitorType") }}</div>
<Selector
class="profile-input"
size="small"
:value="item.type"
:options="MonitorType"
placeholder="Select a type"
@change="changeMonitorType($event, index)"
/>
</div>
<div>
<div class="label">{{ 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>

View File

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

View File

@@ -0,0 +1,44 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const MonitorType: any = [
{ label: "PROCESS_CPU", value: "PROCESS_CPU" },
{ label: "PROCESS_THREAD_COUNT", value: "PROCESS_THREAD_COUNT" },
{ label: "SYSTEM_LOAD", value: "SYSTEM_LOAD" },
{ label: "HTTP_ERROR_RATE", value: "HTTP_ERROR_RATE" },
{ label: "HTTP_AVG_RESPONSE_TIME", value: "HTTP_AVG_RESPONSE_TIME" },
];
export const TargetTypes = [
{ label: "ON_CPU", value: "ON_CPU" },
{ label: "OFF_CPU", value: "OFF_CPU" },
{ label: "NETWORK", value: "NETWORK" },
];
export const ComponentType = "CONTINOUS_PROFILING";
export const HeaderLabels = [
{ value: "triggeredCount", label: "Triggered Count", width: 150 },
{ value: "lastTriggerTime", label: "Last Trigger Time", width: 170 },
];
export const HeaderChildLabels = [
{ value: "detectType", label: "Detect Type", width: 100 },
{ value: "triggeredCount", label: "Triggered Count", width: 120 },
{ value: "lastTriggerTime", label: "Last Trigger Time", width: 160 },
{ value: "labels", label: "Labels" },
];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -77,6 +77,7 @@ limitations under the License. -->
import getLocalTime from "@/utils/localtime";
import { useAppStoreWithOut } from "@/store/modules/app";
import NewTask from "./NewTask.vue";
import { EBPFProfilingTriggerType } from "@/store/data";
/*global Nullable */
const { t } = useI18n();
@@ -185,6 +186,7 @@ limitations under the License. -->
serviceId,
serviceInstanceId,
targets: ["NETWORK"],
triggerType: EBPFProfilingTriggerType.FIXED_TIME,
});
if (res.errors) {

View File

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

View File

@@ -0,0 +1,83 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="content" v-if="taskTimelineStore.selectedTask.targetType === TargetTypes[2].value">
<process-topology v-if="networkProfilingStore.nodes.length" :config="config" />
</div>
<div
class="content"
v-if="[TargetTypes[1].value, TargetTypes[0].value].includes(taskTimelineStore.selectedTask.targetType)"
>
<div class="schedules">
<EBPFSchedules />
</div>
<div class="item">
<EBPFStack />
</div>
</div>
<div class="text" v-if="!taskTimelineStore.selectedTask.targetType">
{{ t("noData") }}
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { useTaskTimelineStore } from "@/store/modules/task-timeline";
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
import { TargetTypes } from "../../continuous-profiling/data";
import ProcessTopology from "@/views/dashboard/related/network-profiling/components/ProcessTopology.vue";
import EBPFSchedules from "@/views/dashboard/related/ebpf/components/EBPFSchedules.vue";
import EBPFStack from "@/views/dashboard/related/ebpf/components/EBPFStack.vue";
/*global defineProps */
defineProps({
config: {
type: Object as PropType<any>,
default: () => ({}),
},
});
const { t } = useI18n();
const taskTimelineStore = useTaskTimelineStore();
const networkProfilingStore = useNetworkProfilingStore();
</script>
<style lang="scss" scoped>
.content {
width: 100%;
height: calc(100% - 30px);
flex-grow: 2;
min-width: 700px;
overflow: hidden;
position: relative;
}
.text {
width: 100%;
text-align: center;
margin-top: 100px;
}
.item {
width: 100%;
overflow: auto;
height: calc(100% - 100px);
padding-bottom: 10px;
}
.schedules {
height: 90px;
border-bottom: 1px solid #ccc;
padding-right: 10px;
}
</style>

View File

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