feat: vis timeline

This commit is contained in:
Fine 2023-06-06 16:18:02 +08:00
parent 4ef6ac8db2
commit 51cf096d90
10 changed files with 405 additions and 39 deletions

View File

@ -20,11 +20,10 @@ import { useAppStoreWithOut } from "@/store/modules/app";
import { useNetworkProfilingStore } from "@/store/modules/network-profiling"; import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
import type { StrategyItem, CheckItems } from "@/types/continous-profiling"; import type { StrategyItem, CheckItems } from "@/types/continous-profiling";
import type { EBPFTaskList, EBPFProfilingSchedule, AnalyzationTrees } from "@/types/ebpf"; import type { EBPFTaskList, EBPFProfilingSchedule, AnalyzationTrees } from "@/types/ebpf";
import type { Instance, Process } from "@/types/selector"; import type { Instance } from "@/types/selector";
import { store } from "@/store"; import { store } from "@/store";
import graphql from "@/graphql"; import graphql from "@/graphql";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import { EBPFProfilingTriggerType } from "../data";
import dateFormatStep from "@/utils/dateFormat"; import dateFormatStep from "@/utils/dateFormat";
import getLocalTime from "@/utils/localtime"; import getLocalTime from "@/utils/localtime";
@ -146,43 +145,6 @@ export const continousProfilingStore = defineStore({
} }
return res.data; return res.data;
}, },
async getContinousTaskList(params: {
serviceId: string;
serviceInstanceId: string;
targets: string[];
triggerType: string;
}) {
if (!params.serviceId) {
return new Promise((resolve) => resolve({}));
}
const res: AxiosResponse = await graphql.query("getEBPFTasks").params(params);
this.errorTip = "";
if (res.data.errors) {
return res.data;
}
this.taskList = res.data.data.queryEBPFTasks || [];
if (!this.taskList.length) {
const networkProfilingStore = useNetworkProfilingStore();
networkProfilingStore.seNodes([]);
networkProfilingStore.setLinks([]);
this.eBPFSchedules = [];
this.analyzeTrees = [];
this.selectedTask = {};
return;
}
// this.selectedTask = this.taskList[0] || {};
// this.setselectedTask(this.selectedTask);
// await this.getGraphData();
return res.data;
},
async getGraphData() {
if (this.selectedStrategy.type === "NETWORK") {
await this.getTopology();
} else {
await this.preAnalyzeTask();
}
},
async getTopology() { async getTopology() {
const networkProfilingStore = useNetworkProfilingStore(); const networkProfilingStore = useNetworkProfilingStore();
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();

View File

@ -0,0 +1,74 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineStore } from "pinia";
import { store } from "@/store";
import graphql from "@/graphql";
import type { AxiosResponse } from "axios";
import type { EBPFTaskList } from "@/types/ebpf";
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
// import { EBPFProfilingTriggerType } from "../data";
interface taskTimelineState {
loading: boolean;
taskList: EBPFTaskList[];
}
export const taskTimelineStore = defineStore({
id: "taskTimeline",
state: (): taskTimelineState => ({
loading: false,
taskList: [],
}),
actions: {
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 || [];
if (!this.taskList.length) {
const networkProfilingStore = useNetworkProfilingStore();
networkProfilingStore.seNodes([]);
networkProfilingStore.setLinks([]);
this.eBPFSchedules = [];
this.analyzeTrees = [];
this.selectedTask = {};
return;
}
// this.selectedTask = this.taskList[0] || {};
// this.setselectedTask(this.selectedTask);
// await this.getGraphData();
return res.data;
},
},
});
export function useTaskTimelineStore(): Recordable {
return taskTimelineStore(store);
}

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/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);
}
</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

@ -28,6 +28,7 @@ import NetworkProfiling from "./NetworkProfiling.vue";
import ContinuousProfiling from "./ContinuousProfiling.vue"; import ContinuousProfiling from "./ContinuousProfiling.vue";
import TimeRange from "./TimeRange.vue"; import TimeRange from "./TimeRange.vue";
import ThirdPartyApp from "./ThirdPartyApp.vue"; import ThirdPartyApp from "./ThirdPartyApp.vue";
import TaskTimeline from "./TaskTimeline.vue";
export default { export default {
Tab, Tab,
@ -44,4 +45,5 @@ export default {
ContinuousProfiling, ContinuousProfiling,
TimeRange, TimeRange,
ThirdPartyApp, ThirdPartyApp,
TaskTimeline,
}; };

View File

@ -27,6 +27,7 @@ import NetworkProfiling from "./NetworkProfiling.vue";
import ContinuousProfiling from "./ContinuousProfiling.vue"; import ContinuousProfiling from "./ContinuousProfiling.vue";
import TimeRange from "./TimeRange.vue"; import TimeRange from "./TimeRange.vue";
import ThirdPartyApp from "./ThirdPartyApp.vue"; import ThirdPartyApp from "./ThirdPartyApp.vue";
import TaskTimeline from "./TaskTimeline.vue";
export default { export default {
Widget, Widget,
@ -42,4 +43,5 @@ export default {
TimeRange, TimeRange,
ThirdPartyApp, ThirdPartyApp,
ContinuousProfiling, ContinuousProfiling,
TaskTimeline,
}; };

View File

@ -242,6 +242,12 @@ export const ProcessTools = [
{ name: "library_books", content: "Add Text", id: "addText" }, { name: "library_books", content: "Add Text", id: "addText" },
{ name: "add_iframe", content: "Add Iframe", id: "addIframe" }, { 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: "add_iframe", content: "Add Iframe", id: "addIframe" },
];
export const ServiceRelationTools = [ export const ServiceRelationTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" }, { name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tabs", id: "addTab" }, { name: "all_inbox", content: "Add Tabs", id: "addTab" },

View File

@ -145,6 +145,7 @@ limitations under the License. -->
InstanceRelationTools, InstanceRelationTools,
ServiceRelationTools, ServiceRelationTools,
ProcessTools, ProcessTools,
ProcessRelationTools,
} from "../data"; } from "../data";
import { useSelectorStore } from "@/store/modules/selectors"; import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
@ -702,6 +703,9 @@ limitations under the License. -->
toolIcons.value = EndpointRelationTools; toolIcons.value = EndpointRelationTools;
break; break;
case EntityType[7].value: case EntityType[7].value:
toolIcons.value = ProcessRelationTools;
break;
case EntityType[8].value:
toolIcons.value = ProcessTools; toolIcons.value = ProcessTools;
break; break;
default: default:

View File

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

View File

@ -0,0 +1,33 @@
<!-- 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"> profiling </div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
/* global defineProps */
defineProps({
config: {
type: Object as PropType<any>,
default: () => ({ graph: {} }),
},
});
</script>
<style lang="scss" scoped>
.content {
width: 100%;
}
</style>

View File

@ -0,0 +1,152 @@
<!-- 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 } 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],
triggerType: EBPFProfilingTriggerType.CONTINUOUS_PROFILING,
});
if (res.errors) {
ElMessage.error(res.errors);
}
}
function visTimeline() {
if (!timeline.value) {
return;
}
if (visGraph.value) {
visGraph.value.destroy();
}
const h = timeline.value.getBoundingClientRect().height;
const events = taskTimelineStore.events.map((d: EBPFTaskList, index: number) => {
return {
id: index + 1,
content: d.triggerType,
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(events);
const options: any = {
height: h,
width: "100%",
locale: "en",
groupHeightMode: "fitItems",
autoResize: false,
tooltip: {
overflowMethod: "cap",
template(item: Event | any) {
const data = item.data || {};
let tmp = `<div>ID: ${data.uuid || ""}</div>
<div>Name: ${data.name || ""}</div>
<div>Event Type: ${data.type || ""}</div>
<div>Start Time: ${data.startTime ? visDate(data.startTime) : ""}</div>
<div>End Time: ${data.endTime ? visDate(data.endTime) : ""}</div>
<div>Message: ${data.message || ""}</div>
<div>Service: ${data.source.service || ""}</div>`;
return tmp;
},
},
};
visGraph.value = new Timeline(timeline.value, items, options);
visGraph.value.on("select", (properties: { items: number[] }) => {
dashboardStore.selectWidget(props.data);
});
}
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);
}
}
watch(
() => taskTimelineStore.taskList,
() => {
visTimeline();
},
);
</script>
<style lang="scss" scoped>
.task-timeline {
width: calc(100% - 5px);
margin: 0 5px 5px 0;
height: 100%;
min-height: 150px;
}
.message {
max-width: 400px;
text-overflow: ellipsis;
overflow: hidden;
}
</style>