feat: Implement the network profiling widget (#132)

This commit is contained in:
Fine0830
2022-08-23 13:41:05 +08:00
committed by GitHub
parent ffabc7c7a7
commit a4fc5192ac
45 changed files with 1899 additions and 139 deletions

View File

@@ -0,0 +1,97 @@
<!-- 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">Network 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="removeWidget">
<span>{{ t("delete") }}</span>
</div>
</el-popover>
<Content />
</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/network-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>
.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

@@ -24,6 +24,7 @@ import Text from "./Text.vue";
import Ebpf from "./Ebpf.vue";
import DemandLog from "./DemandLog.vue";
import Event from "./Event.vue";
import NetworkProfiling from "./NetworkProfiling.vue";
import TimeRange from "./TimeRange.vue";
export default {
@@ -37,5 +38,6 @@ export default {
Ebpf,
DemandLog,
Event,
NetworkProfiling,
TimeRange,
};

View File

@@ -23,6 +23,7 @@ import Text from "./Text.vue";
import Ebpf from "./Ebpf.vue";
import DemandLog from "./DemandLog.vue";
import Event from "./Event.vue";
import NetworkProfiling from "./NetworkProfiling.vue";
import TimeRange from "./TimeRange.vue";
export default {
@@ -35,5 +36,6 @@ export default {
Ebpf,
DemandLog,
Event,
NetworkProfiling,
TimeRange,
};

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
export const dragIgnoreFrom =
"svg.d3-trace-tree, .dragger, .micro-topo-chart, .schedules, .vis-item, .vis-timeline";
"svg.d3-trace-tree, .dragger, .micro-topo-chart, .schedules, .vis-item, .vis-timeline, .process-svg";
export const PodsChartTypes = ["EndpointList", "InstanceList"];
@@ -198,6 +198,11 @@ export const InstanceTools = [
{ name: "assignment", content: "Add Log", id: "addLog" },
{ name: "demand", content: "Add On Demand Log", id: "addDemandLog" },
{ name: "event", content: "Add Event", id: "addEvent" },
{
name: "timeline",
content: "Add Network Profiling",
id: "addNetworkProfiling",
},
];
export const EndpointTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },

View File

@@ -287,7 +287,7 @@ async function setSourceSelector() {
await selectorStore.getService(String(params.serviceId));
states.currentService = selectorStore.currentService.value;
const e = String(params.entity).split("Relation")[0];
await fetchPods(e, selectorStore.currentService.id, false);
await fetchPods(e, selectorStore.currentService.id, true);
if (!(selectorStore.pods.length && selectorStore.pods[0])) {
selectorStore.setCurrentPod(null);
states.currentPod = "";
@@ -295,32 +295,25 @@ async function setSourceSelector() {
return;
}
const pod = params.podId || selectorStore.pods[0].id;
let currentPod;
if (states.currentPod) {
currentPod = selectorStore.pods.find(
(d: { label: string }) => d.label === states.currentPod
);
} else {
currentPod = selectorStore.pods.find((d: { id: string }) => d.id === pod);
}
const currentPod = selectorStore.pods.find(
(d: { id: string }) => d.id === pod
);
if (!currentPod) {
selectorStore.setCurrentProcess(null);
states.currentProcess = "";
return;
}
selectorStore.setCurrentPod(currentPod);
states.currentPod = currentPod.label;
const process =
params.processId ||
(selectorStore.processes.length && selectorStore.processes[0].id);
let currentProcess;
if (states.currentProcess) {
currentProcess = selectorStore.processes.find(
(d: { label: string }) => d.label === states.currentProcess
);
} else {
currentProcess = selectorStore.processes.find(
(d: { id: string }) => d.id === process
);
if (!(selectorStore.processes.length && selectorStore.processes[0])) {
selectorStore.setCurrentProcess(null);
states.currentProcess = "";
return;
}
const process = params.processId || selectorStore.processes[0].id;
const currentProcess = selectorStore.processes.find(
(d: { id: string }) => d.id === process
);
if (currentProcess) {
selectorStore.setCurrentProcess(currentProcess);
states.currentProcess = currentProcess.label;
@@ -333,7 +326,7 @@ async function setDestSelector() {
await fetchPods(
String(params.entity),
selectorStore.currentDestService.id,
false
true
);
if (!(selectorStore.destPods.length && selectorStore.destPods[0])) {
selectorStore.setCurrentDestPod(null);
@@ -341,36 +334,27 @@ async function setDestSelector() {
return;
}
const destPod = params.destPodId || selectorStore.destPods[0].id;
let currentDestPod = { label: "" };
if (states.currentDestPod) {
currentDestPod = selectorStore.pods.find(
(d: { label: string }) => d.label === states.currentDestPod
);
} else {
currentDestPod = selectorStore.destPods.find(
(d: { id: string }) => d.id === destPod
);
}
const currentDestPod = selectorStore.destPods.find(
(d: { id: string }) => d.id === destPod
);
if (!currentDestPod) {
states.currentDestProcess = "";
selectorStore.setCurrentProcess(null);
return;
}
selectorStore.setCurrentDestPod(currentDestPod);
states.currentDestPod = currentDestPod.label;
const destProcess = params.destProcessId || selectorStore.destProcesses[0].id;
let currentDestProcess;
if (states.currentDestProcess) {
currentDestProcess = selectorStore.destProcesses.find(
(d: { label: string }) => d.label === states.currentProcess
);
} else {
currentDestProcess = selectorStore.destProcesses.find(
(d: { id: string }) => d.id === destProcess
);
}
if (currentDestProcess) {
selectorStore.setCurrentProcess(currentDestProcess);
states.currentProcess = currentDestProcess.label;
const currentDestProcess = selectorStore.destProcesses.find(
(d: { id: string }) => d.id === destProcess
);
if (!currentDestProcess) {
states.currentDestProcess = "";
selectorStore.setCurrentProcess(null);
return;
}
selectorStore.setCurrentProcess(currentDestProcess);
states.currentDestProcess = currentDestProcess.label;
}
async function getServices() {
@@ -562,6 +546,9 @@ function setTabControls(id: string) {
case "addEvent":
dashboardStore.addTabControls("Event");
break;
case "addNetworkProfiling":
dashboardStore.addTabControls("NetworkProfiling");
break;
case "addTimeRange":
dashboardStore.addTabControls("TimeRange");
break;
@@ -603,6 +590,9 @@ function setControls(id: string) {
case "addEvent":
dashboardStore.addControl("Event");
break;
case "addNetworkProfiling":
dashboardStore.addControl("NetworkProfiling");
break;
case "addTimeRange":
dashboardStore.addControl("TimeRange");
break;

View File

@@ -58,7 +58,6 @@ limitations under the License. -->
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import type { PropType } from "vue";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";

View File

@@ -15,14 +15,15 @@
* limitations under the License.
*/
export default (d3: any, graph: any) =>
export default (d3: any, graph: any, diff: number[]) =>
d3
.zoom()
.scaleExtent([0.3, 10])
.on("zoom", (d: any) => {
graph
.attr("transform", d3.zoomTransform(graph.node()))
.attr(
`translate(${d.transform.x},${d.transform.y})scale(${d.transform.k})`
);
.on("zoom", (event: any) => {
graph.attr(
"transform",
`translate(${event.transform.x + diff[0]},${
event.transform.y + diff[1]
})scale(${event.transform.k})`
);
});

View File

@@ -103,13 +103,13 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
import { Option } from "@/types/app";
import { TableHeader, AggregateTypes } from "./data";
import { useEbpfStore } from "@/store/modules/ebpf";
import { EBPFProfilingSchedule, Process } from "@/types/ebpf";
import { ElMessage, ElTable } from "element-plus";
import { dateFormat } from "@/utils/dateFormat";
const { t } = useI18n();
const ebpfStore = useEbpfStore();
@@ -123,8 +123,6 @@ const selectedLabels = ref<string[]>(["0"]);
const searchText = ref<string>("");
const aggregateType = ref<string>(AggregateTypes[0].value);
const duration = ref<string[]>([]);
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
const attributes = (attr: { name: string; value: string }[]) => {
return attr
.map((d: { name: string; value: string }) => `${d.name}=${d.value}`)

View File

@@ -72,17 +72,15 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import { ref } from "vue";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
import { useEbpfStore } from "@/store/modules/ebpf";
import { EBPFTaskList } from "@/types/ebpf";
import { ElMessage } from "element-plus";
import TaskDetails from "../../components/TaskDetails.vue";
import { dateFormat } from "@/utils/dateFormat";
const { t } = useI18n();
const ebpfStore = useEbpfStore();
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
const viewDetail = ref<boolean>(false);
async function changeTask(item: EBPFTaskList) {

View File

@@ -39,8 +39,8 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import { ref } from "vue";
import dayjs from "dayjs";
import { BrowserLogConstants } from "./data";
import { dateFormat } from "@/utils/dateFormat";
/*global defineProps, defineEmits, NodeListOf */
const props = defineProps({
@@ -50,9 +50,6 @@ const columns = BrowserLogConstants;
const emit = defineEmits(["select"]);
const logItem = ref<any>(null);
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
function showSelectSpan() {
const items: NodeListOf<any> = document.querySelectorAll(".log-item");

View File

@@ -43,8 +43,8 @@ limitations under the License. -->
import { computed } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import dayjs from "dayjs";
import { Option } from "@/types/app";
import { dateFormat } from "@/utils/dateFormat";
/*global defineProps */
const props = defineProps({
@@ -60,8 +60,6 @@ const logTags = computed(() => {
return `${d.key} = ${d.value}`;
});
});
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
</script>
<style lang="scss" scoped>
.content {

View File

@@ -39,11 +39,11 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import { computed, inject } from "vue";
import dayjs from "dayjs";
import { ServiceLogConstants } from "./data";
import getDashboard from "@/hooks/useDashboardsSession";
import { useDashboardStore } from "@/store/modules/dashboard";
import { LayoutConfig } from "@/types/dashboard";
import { dateFormat } from "@/utils/dateFormat";
/*global defineProps, defineEmits, Recordable */
const props = defineProps({
@@ -64,8 +64,6 @@ const tags = computed(() => {
)
);
});
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
function selectLog(label: string, value: string) {
if (label === "traceId") {

View File

@@ -0,0 +1,59 @@
<!-- 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">
<Tasks />
<div
class="vis-graph ml-5"
v-if="networkProfilingStore.nodes.length"
v-loading="networkProfilingStore.loadNodes"
>
<process-topology />
</div>
<div class="text" v-else v-loading="networkProfilingStore.loadNodes">
{{ t("noData") }}
</div>
</div>
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import Tasks from "./components/Tasks.vue";
import ProcessTopology from "./components/ProcessTopology.vue";
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
const networkProfilingStore = useNetworkProfilingStore();
const { t } = useI18n();
</script>
<style lang="scss" scoped>
.content {
height: calc(100% - 30px);
width: 100%;
}
.vis-graph {
height: 100%;
flex-grow: 2;
min-width: 700px;
overflow: auto;
position: relative;
width: calc(100% - 330px);
}
.text {
width: calc(100% - 330px);
text-align: center;
margin-top: 30px;
}
</style>

View File

@@ -0,0 +1,144 @@
/**
* 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.
*/
class Orientation {
public f0 = 0;
public f1 = 0;
public f2 = 0;
public f3 = 0;
public b0? = 0;
public b1? = 0;
public b2? = 0;
public b3? = 0;
public start_angle? = 0;
constructor(
f0: number,
f1: number,
f2: number,
f3: number,
b0: number,
b1: number,
b2: number,
b3: number,
start_angle: number
) {
this.f0 = f0;
this.f1 = f1;
this.f2 = f2;
this.f3 = f3;
this.b0 = b0;
this.b1 = b1;
this.b2 = b2;
this.b3 = b3;
this.start_angle = start_angle;
}
}
const SQRT3 = Math.sqrt(3.0);
class Layout {
static Pointy = new Orientation(
SQRT3,
SQRT3 / 2.0,
0.0,
3.0 / 2.0,
SQRT3 / 3.0,
-1.0 / 3.0,
0.0,
2.0 / 3.0,
0.5
);
static Flat = new Orientation(
3.0 / 2.0,
0.0,
SQRT3 / 2.0,
SQRT3,
2.0 / 3.0,
0.0,
-1.0 / 3.0,
SQRT3 / 3.0,
0.0
);
static spacing(radius: number, isPointy = false): number[] {
return isPointy
? [SQRT3 * radius, 2 * radius * (3 / 4)]
: [2 * radius * (3 / 4), SQRT3 * radius];
}
private radius = 1;
private orientation: Orientation = { f0: 0, f1: 0, f2: 0, f3: 0 };
private origin = [0, 0];
constructor(radius: number, origin = [0, 0], orientation?: Orientation) {
this.radius = radius; //Layout.spacing( radius, ( orientation === Layout.Pointy ) );
this.orientation = orientation || Layout.Flat;
this.origin = origin;
}
// Same as HexToPixel, Except it takes raw coords instead of hex object.
axialToPixel(ax: number, ay: number): number[] {
const M = this.orientation;
const x = (M.f0 * ax + M.f1 * ay) * this.radius;
const y = (M.f2 * ax + M.f3 * ay) * this.radius;
return [x + this.origin[0], y + this.origin[1]];
}
hexToPixel(h: { x: number; y: number }): number[] {
const M = this.orientation;
const x = (M.f0 * h.x + M.f1 * h.y) * this.radius;
const y = (M.f2 * h.x + M.f3 * h.y) * this.radius;
return [x + this.origin[0], y + this.origin[1]];
}
}
class Hex extends Int16Array {
constructor(x: number, y: number, z = null) {
super(3);
this.xyz(x, y, z);
}
xyz(x: number, y: number, z: number | null = null): Hex {
if (z == null) z = -x - y;
if (x + y + z != 0) {
console.log("Bad Axial Coordinate : : q %d r %d s %d", x, y, z);
}
this[0] = x;
this[1] = y;
this[2] = z;
return this;
}
get x(): number {
return this[0];
}
get y(): number {
return this[1];
}
get z(): number {
return this[2];
}
get len(): number {
return Math.floor(
(Math.abs(this[0]) + Math.abs(this[1]) + Math.abs(this[2])) / 2
);
}
}
export { Hex, Orientation, Layout };

View File

@@ -0,0 +1,126 @@
/**
* 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 linkElement = (graph: any) => {
const linkEnter = graph
.append("path")
.attr("class", "topo-call")
.attr("marker-end", "url(#arrow)")
.attr("stroke", "#97B0F8")
.attr("d", (d: any) => {
const controlPos = computeControlPoint(
[d.source.x, d.source.y - 5],
[d.target.x, d.target.y - 5],
0.5
);
return (
"M" +
d.source.x +
" " +
(d.source.y - 5) +
" " +
"Q" +
controlPos[0] +
" " +
controlPos[1] +
" " +
d.target.x +
" " +
(d.target.y - 5)
);
});
return linkEnter;
};
export const anchorElement = (graph: any, funcs: any, tip: any) => {
const linkEnter = graph
.append("circle")
.attr("class", "topo-line-anchor")
.attr("r", 5)
.attr("fill", "#97B0F8")
.attr("transform", (d: any) => {
const controlPos = computeControlPoint(
[d.source.x, d.source.y - 5],
[d.target.x, d.target.y - 5],
0.5
);
const p = quadraticBezier(
0.5,
{ x: d.source.x, y: d.source.y - 5 },
{ x: controlPos[0], y: controlPos[1] },
{ x: d.target.x, y: d.target.y - 5 }
);
return `translate(${p[0]}, ${p[1]})`;
})
.on("mouseover", function (event: unknown, d: unknown) {
tip.html(funcs.tipHtml).show(d, this);
})
.on("mouseout", function () {
tip.hide(this);
})
.on("click", (event: unknown, d: unknown) => {
funcs.handleLinkClick(event, d);
});
return linkEnter;
};
export const arrowMarker = (graph: any) => {
const defs = graph.append("defs");
const arrow = defs
.append("marker")
.attr("id", "arrow")
.attr("class", "topo-line-arrow")
.attr("markerUnits", "strokeWidth")
.attr("markerWidth", "8")
.attr("markerHeight", "8")
.attr("viewBox", "0 0 12 12")
.attr("refX", "10")
.attr("refY", "6")
.attr("orient", "auto");
const arrowPath = "M2,2 L10,6 L2,10 L6,6 L2,2";
arrow.append("path").attr("d", arrowPath).attr("fill", "#97B0F8");
return arrow;
};
// Control Point coordinates of quadratic Bezier curve
function computeControlPoint(ps: number[], pe: number[], arc = 0.5) {
const deltaX = pe[0] - ps[0];
const deltaY = pe[1] - ps[1];
const theta = Math.atan(deltaY / deltaX);
const len = (Math.sqrt(deltaX * deltaX + deltaY * deltaY) / 2) * arc;
const newTheta = theta - Math.PI / 2;
return [
(ps[0] + pe[0]) / 2 + len * Math.cos(newTheta),
(ps[1] + pe[1]) / 2 + len * Math.sin(newTheta),
];
}
// Point coordinates of quadratic Bezier curve
/**
* @param t [0, 1]
* @param ps start position
* @param pc control position
* @param pe end position
* @returns a position in the line
*/
function quadraticBezier(
t: number,
ps: { x: number; y: number },
pc: { x: number; y: number },
pe: { x: number; y: number }
) {
const x = (1 - t) * (1 - t) * ps.x + 2 * t * (1 - t) * pc.x + t * t * pe.x;
const y = (1 - t) * (1 - t) * ps.y + 2 * t * (1 - t) * pc.y + t * t * pe.y;
return [x, y];
}

View File

@@ -0,0 +1,54 @@
/**
* 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 icons from "@/assets/img/icons";
import { Node } from "@/types/topology";
export default (d3: any, graph: any, funcs: any, tip: any) => {
const nodeEnter = graph
.append("g")
.call(
d3
.drag()
.on("start", funcs.dragstart)
.on("drag", funcs.dragged)
.on("end", funcs.dragended)
)
.on("mouseover", function (event: unknown, d: Node) {
tip.html(funcs.tipHtml).show(d, this);
})
.on("mouseout", function () {
tip.hide(this);
});
nodeEnter
.append("image")
.attr("width", 35)
.attr("height", 35)
.attr("x", (d: any) => d.x - 15)
.attr("y", (d: any) => d.y - 15)
.attr("style", "cursor: move;")
.attr("xlink:href", icons.CUBE);
nodeEnter
.append("text")
.attr("fill", "#000")
.attr("text-anchor", "middle")
.attr("x", (d: any) => d.x + 5)
.attr("y", (d: any) => d.y + 28)
.text((d: { name: string }) =>
d.name.length > 10 ? `${d.name.substring(0, 10)}...` : d.name
);
return nodeEnter;
};

View File

@@ -0,0 +1,503 @@
<!-- 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="chart" class="process-topo"></div>
<el-popover placement="bottom" :width="295" trigger="click">
<template #reference>
<div
class="switch-icon-edit ml-5"
title="Settings"
@click="setConfig"
v-if="dashboardStore.editMode"
>
<Icon size="middle" iconName="setting_empty" />
</div>
</template>
<Settings @update="updateSettings" />
</el-popover>
<TimeLine @get="getDates" />
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { ref, onMounted, watch } from "vue";
import * as d3 from "d3";
import { useI18n } from "vue-i18n";
import { ElMessage } from "element-plus";
import router from "@/router";
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import d3tip from "d3-tip";
import { linkElement, anchorElement, arrowMarker } from "./Graph/linkProcess";
import nodeElement from "./Graph/nodeProcess";
import { Call } from "@/types/topology";
import zoom from "../../components/utils/zoom";
import { ProcessNode } from "@/types/ebpf";
import { useThrottleFn } from "@vueuse/core";
import Settings from "./Settings.vue";
import { EntityType } from "@/views/dashboard/data";
import getDashboard from "@/hooks/useDashboardsSession";
import { Layout } from "./Graph/layout";
import TimeLine from "./TimeLine.vue";
import { useAppStoreWithOut } from "@/store/modules/app";
/*global Nullable, defineProps */
const props = defineProps({
config: {
type: Object as PropType<any>,
default: () => ({ graph: {} }),
},
});
const { t } = useI18n();
const appStore = useAppStoreWithOut();
const dashboardStore = useDashboardStore();
const selectorStore = useSelectorStore();
const networkProfilingStore = useNetworkProfilingStore();
const height = ref<number>(100);
const width = ref<number>(100);
const svg = ref<Nullable<any>>(null);
const chart = ref<Nullable<HTMLDivElement>>(null);
const tip = ref<Nullable<HTMLDivElement>>(null);
const graph = ref<any>(null);
const node = ref<any>(null);
const link = ref<any>(null);
const anchor = ref<any>(null);
const arrow = ref<any>(null);
const oldVal = ref<{ width: number; height: number }>({ width: 0, height: 0 });
const config = ref<any>({});
const diff = ref<number[]>([220, 200]);
const radius = 210;
const dates = ref<Nullable<{ start: number; end: number }>>(null);
onMounted(() => {
init();
oldVal.value = (chart.value && chart.value.getBoundingClientRect()) || {
width: 0,
height: 0,
};
});
async function init() {
svg.value = d3.select(chart.value).append("svg").attr("class", "process-svg");
if (!networkProfilingStore.nodes.length) {
return;
}
drawGraph();
createLayout();
}
function drawGraph() {
const dom = chart.value?.getBoundingClientRect() || {
height: 20,
width: 0,
};
height.value = (dom.height || 40) - 20;
width.value = dom.width;
svg.value.attr("height", height.value).attr("width", width.value);
tip.value = (d3tip as any)().attr("class", "d3-tip").offset([-8, 0]);
const outNodes = networkProfilingStore.nodes.filter(
(d: ProcessNode) => d.serviceInstanceId !== selectorStore.currentPod.id
);
if (outNodes.length) {
diff.value[0] = (dom.width - radius * 4) / 2 + radius;
} else {
diff.value[0] = (dom.width - radius * 2) / 2 + radius;
}
graph.value = svg.value
.append("g")
.attr("class", "svg-graph")
.attr("transform", `translate(${diff.value[0]}, ${diff.value[1]})`);
graph.value.call(tip.value);
node.value = graph.value.append("g").selectAll(".topo-node");
link.value = graph.value.append("g").selectAll(".topo-call");
anchor.value = graph.value.append("g").selectAll(".topo-line-anchor");
arrow.value = graph.value.append("g").selectAll(".topo-line-arrow");
svg.value.call(zoom(d3, graph.value, diff.value));
svg.value.on("click", (event: any) => {
event.stopPropagation();
event.preventDefault();
networkProfilingStore.setNode(null);
networkProfilingStore.setLink(null);
dashboardStore.selectWidget(props.config);
});
useThrottleFn(resize, 500)();
}
function hexGrid(n = 1, radius = 1, origin = [0, 0]) {
let x, y, yn, p;
const gLayout = new Layout(radius, origin);
const pos = [];
for (x = -n; x <= n; x++) {
y = Math.max(-n, -x - n); // 0
yn = Math.min(n, -x + n); // 1
for (y; y <= yn; y++) {
p = gLayout.axialToPixel(x, y);
pos.push(p);
}
}
return pos;
}
function createPolygon(radius: number, sides = 6, offset = 0) {
const poly: number[][] = [];
let i, rad;
for (i = 0; i < sides; i++) {
rad = Math.PI * 2 * (i / sides);
poly.push([
Math.cos(rad + offset) * radius,
Math.sin(rad + offset) * radius,
]);
}
return poly;
}
function getArcPoint(angle: number, radius: number) {
const origin = [0, 0];
const x1 = radius + origin[0] * Math.cos((angle * Math.PI) / 180);
const y1 = origin[1] + radius * Math.sin((angle * Math.PI) / 180);
return [x1, y1];
}
function createLayout() {
if (!node.value || !link.value) {
return;
}
const dom: any = (chart.value && chart.value.getBoundingClientRect()) || {
width: 0,
height: 0,
};
if (isNaN(dom.width) || dom.width < 1) {
return;
}
const p = {
count: 1,
radius, // layout hexagons radius 300
};
const polygon = createPolygon(p.radius, 6, 0);
const origin = [0, 0];
const vertices: any = []; // a hexagon vertices
for (let v = 0; v < polygon.length; v++) {
vertices.push([origin[0] + polygon[v][0], origin[1] + polygon[v][1]]);
}
const linePath = d3.line();
linePath.curve(d3.curveLinearClosed);
const hexPolygon = graph.value.append("g");
hexPolygon
.append("path")
.attr("d", linePath(vertices))
.attr("stroke", "#D5DDF6")
.attr("stroke-width", 2)
.style("fill", "none");
hexPolygon
.append("text")
.attr("fill", "#000")
.attr("text-anchor", "middle")
.attr("x", 0)
.attr("y", p.radius)
.text(() => selectorStore.currentPod.label);
const nodeArr = networkProfilingStore.nodes.filter(
(d: ProcessNode) => d.isReal || d.name === "UNKNOWN_LOCAL"
);
const count = nodeArr.length;
// layout
const centers = hexGrid(p.count, 68, origin); // cube centers
const cubeCenters = [];
if (count > 7) {
for (let i = 0; i < centers.length; i++) {
// const polygon = createPolygon(68, 6, 0);
// const vertices: any = []; // a hexagon vertices
// for (let v = 0; v < polygon.length; v++) {
// vertices.push([
// centers[i][0] + polygon[v][0],
// centers[i][1] + polygon[v][1],
// ]);
// }
// const linePath = d3.line();
// linePath.curve(d3.curveLinearClosed);
// graph.value
// .append("path")
// .attr("d", linePath(vertices))
// .attr("stroke", "#ccc")
// .attr("stroke-width", 1)
// .style("fill", "none");
let c = hexGrid(1, 20, centers[i]);
if (count < 15) {
c = [c[0], c[5]];
} else if (count < 22) {
c = [c[0], c[2], c[5]];
}
cubeCenters.push(...c);
}
shuffleArray(cubeCenters);
}
// for (let i = 0; i < cubeCenters.length; i++) {
// const polygon = createPolygon(20, 6, 0);
// const vertices: any = []; // a hexagon vertices
// for (let v = 0; v < polygon.length; v++) {
// vertices.push([
// cubeCenters[i][0] + polygon[v][0],
// cubeCenters[i][1] + polygon[v][1],
// ]);
// }
// const linePath = d3.line();
// linePath.curve(d3.curveLinearClosed);
// graph.value
// .append("path")
// .attr("d", linePath(vertices))
// .attr("stroke", "#ccc")
// .attr("stroke-width", 1)
// .style("fill", "none");
// }
let cubes = count > 7 ? cubeCenters : centers;
for (let v = 0; v < count; v++) {
const x = cubes[v][0];
const y = cubes[v][1];
nodeArr[v].x = x;
nodeArr[v].y = y;
}
const outNodes = networkProfilingStore.nodes.filter(
(d: ProcessNode) => !(d.isReal || d.name === "UNKNOWN_LOCAL")
);
let angle = 10;
let r = 230;
for (let v = 0; v < outNodes.length; v++) {
const pos = getArcPoint(angle, r); // angle is [-120, 120]
outNodes[v].x = pos[0];
outNodes[v].y = pos[1];
angle = angle + 20;
if (angle * (v + 1) > 120) {
angle = -10;
r = r + 60;
}
if (angle * (v + 1) < -120) {
r = r + 60;
angle = 10;
}
}
drawTopology([...nodeArr, ...outNodes]);
}
function drawTopology(nodeArr: any[]) {
node.value = node.value.data(nodeArr, (d: ProcessNode) => d.id);
node.value.exit().remove();
node.value = nodeElement(
d3,
node.value.enter(),
{
tipHtml: (data: ProcessNode) => {
return ` <div class="mb-5"><span class="grey">name: </span>${data.name}</div>`;
},
},
tip.value
).merge(node.value);
// line element
const obj = {} as any;
const calls = networkProfilingStore.calls.reduce((prev: any[], next: any) => {
if (
!(
obj[next.sourceId + next.targetId] && obj[next.targetId + next.sourceId]
)
) {
obj[next.sourceId + next.targetId] = true;
obj[next.targetId + next.sourceId] = true;
prev.push(next);
}
return prev;
}, []);
link.value = link.value.data(calls, (d: Call) => d.id);
link.value.exit().remove();
link.value = linkElement(link.value.enter()).merge(link.value);
anchor.value = anchor.value.data(calls, (d: Call) => d.id);
anchor.value.exit().remove();
anchor.value = anchorElement(
anchor.value.enter(),
{
handleLinkClick: handleLinkClick,
tipHtml: (data: Call) => {
const html = `<div><span class="grey">${t(
"detectPoint"
)}:</span>${data.detectPoints.join(" | ")}</div>`;
return html;
},
},
tip.value
).merge(anchor.value);
// arrow marker
arrow.value = arrow.value.data(calls, (d: Call) => d.id);
arrow.value.exit().remove();
arrow.value = arrowMarker(arrow.value.enter()).merge(arrow.value);
}
function shuffleArray(array: number[][]) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
function handleLinkClick(event: any, d: Call) {
event.stopPropagation();
networkProfilingStore.setNode(null);
networkProfilingStore.setLink(d);
if (!config.value.linkDashboard) {
return;
}
const { dashboard } = getDashboard({
name: config.value.linkDashboard,
layer: dashboardStore.layerId,
entity: EntityType[7].value,
});
if (!dashboard) {
ElMessage.error(
`The dashboard named ${config.value.linkDashboard} doesn't exist`
);
return;
}
let times: any = {};
if (dates.value) {
times = dates.value;
} else {
const { taskStartTime, fixedTriggerDuration } =
networkProfilingStore.selectedNetworkTask;
const startTime =
fixedTriggerDuration > 1800
? taskStartTime + fixedTriggerDuration * 1000 - 30 * 60 * 1000
: taskStartTime;
times = {
start: startTime,
end: taskStartTime + fixedTriggerDuration * 1000,
};
}
const param = JSON.stringify({
...times,
step: appStore.duration.step,
utc: appStore.utc,
});
const path = `/dashboard/${dashboard.layer}/${EntityType[7].value}/${d.source.serviceId}/${d.source.serviceInstanceId}/${d.source.id}/${d.target.serviceId}/${d.target.serviceInstanceId}/${d.target.id}/${dashboard.name}/duration/${param}`;
const routeUrl = router.resolve({ path });
window.open(routeUrl.href, "_blank");
}
function updateSettings(param: unknown) {
config.value = param;
}
function setConfig() {
dashboardStore.selectWidget(props.config);
}
function getDates(times: any) {
dates.value = times;
}
function resize() {
const observer = new ResizeObserver((entries) => {
const entry = entries[0];
const cr = entry.contentRect;
if (
Math.abs(cr.width - oldVal.value.width) < 5 &&
Math.abs(cr.height - oldVal.value.height) < 5
) {
return;
}
freshNodes();
oldVal.value = { width: cr.width, height: cr.height };
});
if (chart.value) {
observer.observe(chart.value);
}
}
async function freshNodes() {
svg.value.selectAll(".svg-graph").remove();
if (!networkProfilingStore.nodes.length) {
return;
}
drawGraph();
createLayout();
}
watch(
() => networkProfilingStore.nodes,
() => {
freshNodes();
}
);
</script>
<style lang="scss">
.process-topo {
width: 100%;
height: 100%;
min-height: 150px;
min-width: 300px;
overflow: auto;
}
.process-svg {
width: 100%;
height: calc(100% - 10px);
cursor: move;
}
.switch-icon-edit {
cursor: pointer;
transition: all 0.5ms linear;
border: 1px solid #ccc;
color: #666;
display: inline-block;
padding: 5px;
border-radius: 3px;
position: absolute;
top: 10px;
right: 10px;
}
.range {
right: 50px;
}
.topo-line-anchor {
cursor: pointer;
}
.topo-call {
stroke-linecap: round;
stroke-width: 2px;
stroke-dasharray: 13 7;
fill: none;
animation: topo-dash 0.5s linear infinite;
}
@keyframes topo-dash {
from {
stroke-dashoffset: 20;
}
to {
stroke-dashoffset: 0;
}
}
.time-ranges {
width: 100%;
padding: 10px;
}
.query {
margin-left: 510px;
}
</style>

View File

@@ -0,0 +1,88 @@
<!-- 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="label">{{ t("linkDashboard") }}</div>
<Selector
:value="linkDashboard"
:options="linkDashboards"
size="small"
placeholder="Please input a dashboard name for calls"
@change="changeLinkDashboard"
class="inputs"
:clearable="true"
/>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import { DashboardItem } from "@/types/dashboard";
import { useDashboardStore } from "@/store/modules/dashboard";
import { EntityType } from "@/views/dashboard/data";
/*global defineEmits */
const emits = defineEmits(["update"]);
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const linkDashboards = ref<
(DashboardItem & { label: string; value: string })[]
>([]);
const { selectedGrid } = dashboardStore;
const linkDashboard = ref<string>(selectedGrid.linkDashboard || "");
onMounted(() => {
getDashboards();
});
function getDashboards() {
const list = JSON.parse(sessionStorage.getItem("dashboards") || "[]");
linkDashboards.value = list.reduce(
(
prev: (DashboardItem & { label: string; value: string })[],
d: DashboardItem
) => {
if (
d.layer === dashboardStore.layerId &&
d.entity === EntityType[7].value
) {
prev.push({ ...d, label: d.name, value: d.name });
}
return prev;
},
[]
);
}
function changeLinkDashboard(opt: { value: string }[]) {
linkDashboard.value = opt[0].value;
const p = {
...dashboardStore.selectedGrid,
linkDashboard: opt[0].value,
};
dashboardStore.selectWidget(p);
dashboardStore.setConfigs(p);
emits("update", p);
}
</script>
<style lang="scss" scoped>
.label {
font-size: 12px;
margin-top: 10px;
}
.inputs {
margin-top: 8px;
width: 270px;
margin-bottom: 30px;
}
</style>

View File

@@ -0,0 +1,282 @@
<!-- 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">
<div class="profile-task-wrapper flex-v">
<div class="profile-t-tool">
<span>{{ t("taskList") }}</span>
<span v-if="inProcess" class="new-task cp" @click="createTask">
<Icon
:style="{ color: '#ccc' }"
iconName="library_add"
size="middle"
/>
</span>
<el-popconfirm
title="Are you sure to create a task?"
@confirm="createTask"
v-else
>
<template #reference>
<span class="new-task cp">
<Icon iconName="library_add" size="middle" />
</span>
</template>
</el-popconfirm>
</div>
<div class="profile-t-wrapper">
<div
class="no-data"
v-show="!networkProfilingStore.networkTasks.length"
>
{{ t("noData") }}
</div>
<table class="profile-t">
<tr
class="profile-tr cp"
v-for="(i, index) in networkProfilingStore.networkTasks"
@click="changeTask(i)"
:key="index"
>
<td
class="profile-td"
:class="{
selected:
networkProfilingStore.selectedNetworkTask.taskId === i.taskId,
}"
>
<div class="ell">
<span class="mr-10 sm">
{{ dateFormat(i.taskStartTime) }}
</span>
<span class="mr-10 sm">
{{
dateFormat(i.taskStartTime + i.fixedTriggerDuration * 1000)
}}
</span>
<span class="ml-10" @click="viewDetail = true">
<Icon iconName="view" size="middle" />
</span>
<span class="ml-5" v-if="index === 0 && inProcess">
<Icon iconName="retry" :loading="true" size="middle" />
</span>
</div>
</td>
</tr>
</table>
</div>
</div>
</div>
<el-dialog
v-model="viewDetail"
:destroy-on-close="true"
fullscreen
@closed="viewDetail = false"
>
<TaskDetails :details="networkProfilingStore.selectedNetworkTask" />
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
import { useSelectorStore } from "@/store/modules/selectors";
import { EBPFTaskList } from "@/types/ebpf";
import { ElMessage } from "element-plus";
import TaskDetails from "../../components/TaskDetails.vue";
import dateFormatStep, { dateFormat } from "@/utils/dateFormat";
import getLocalTime from "@/utils/localtime";
import { useAppStoreWithOut } from "@/store/modules/app";
const { t } = useI18n();
const selectorStore = useSelectorStore();
const networkProfilingStore = useNetworkProfilingStore();
const appStore = useAppStoreWithOut();
const viewDetail = ref<boolean>(false);
/*global Nullable */
const intervalFn = ref<Nullable<any>>(null);
const inProcess = ref<boolean>(true);
fetchTasks();
async function changeTask(item: EBPFTaskList) {
networkProfilingStore.setSelectedNetworkTask(item);
intervalFn.value && clearInterval(intervalFn.value);
getTopology();
}
async function getTopology() {
const { taskStartTime, fixedTriggerDuration, taskId } =
networkProfilingStore.selectedNetworkTask;
const serviceInstanceId =
(selectorStore.currentPod && selectorStore.currentPod.id) || "";
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,
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);
}
const task = networkProfilingStore.networkTasks[0] || {};
if (task.taskId === taskId) {
inProcess.value =
task.taskStartTime + task.fixedTriggerDuration * 1000 >
new Date().getTime()
? true
: false;
}
if (!inProcess.value) {
intervalFn.value && clearInterval(intervalFn.value);
}
return resp;
}
async function createTask() {
if (inProcess.value) {
return;
}
const serviceId =
(selectorStore.currentService && selectorStore.currentService.id) || "";
const serviceInstanceId =
(selectorStore.currentPod && selectorStore.currentPod.id) || "";
if (!serviceId) {
return;
}
if (!serviceInstanceId) {
return;
}
const res = await networkProfilingStore.createNetworkTask({
serviceId,
serviceInstanceId,
});
if (res.errors) {
ElMessage.error(res.errors);
return;
}
await fetchTasks();
}
async function enableInterval() {
const res = await networkProfilingStore.keepNetworkProfiling(
networkProfilingStore.selectedNetworkTask.taskId
);
if (res.errors) {
return ElMessage.error(res.errors);
}
if (networkProfilingStore.aliveNetwork) {
intervalFn.value = setInterval(getTopology, 60000);
}
}
async function fetchTasks() {
intervalFn.value && clearInterval(intervalFn.value);
const serviceId =
(selectorStore.currentService && selectorStore.currentService.id) || "";
const serviceInstanceId =
(selectorStore.currentPod && selectorStore.currentPod.id) || "";
const res = await networkProfilingStore.getTaskList({
serviceId,
serviceInstanceId,
targets: ["NETWORK"],
});
if (res.errors) {
return ElMessage.error(res.errors);
}
await getTopology();
if (inProcess.value) {
enableInterval();
}
}
watch(
() => selectorStore.currentPod,
() => {
fetchTasks();
}
);
</script>
<style lang="scss" scoped>
.profile-task-list {
width: 330px;
height: calc(100% - 10px);
overflow: auto;
border-right: 1px solid rgba(0, 0, 0, 0.1);
}
.item span {
height: 21px;
}
.profile-td {
padding: 10px 0 10px 10px;
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
&.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: rgba(0, 0, 0, 0.04);
}
}
.profile-t-tool {
padding: 5px 10px;
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
background: #f3f4f9;
width: 100%;
}
.new-task {
float: right;
}
</style>

View File

@@ -0,0 +1,178 @@
<!-- 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>
<el-popover
placement="bottom"
:width="600"
trigger="click"
@after-enter="showTimeLine"
>
<template #reference>
<div class="switch-icon-edit">
<Icon size="middle" iconName="time_range" />
</div>
</template>
<div ref="timeRange" class="time-ranges"></div>
<el-button
class="query"
size="small"
type="primary"
@click="updateTopology"
>
{{ t("query") }}
</el-button>
</el-popover>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { ElMessage } from "element-plus";
import { useNetworkProfilingStore } from "@/store/modules/network-profiling";
import { useSelectorStore } from "@/store/modules/selectors";
import { DataSet, Timeline } from "vis-timeline/standalone";
import "vis-timeline/styles/vis-timeline-graph2d.css";
import dateFormatStep from "@/utils/dateFormat";
import getLocalTime from "@/utils/localtime";
import { useAppStoreWithOut } from "@/store/modules/app";
/*global Nullable, defineEmits */
const emits = defineEmits(["get"]);
const { t } = useI18n();
const selectorStore = useSelectorStore();
const appStore = useAppStoreWithOut();
const networkProfilingStore = useNetworkProfilingStore();
const timeRange = ref<Nullable<HTMLDivElement>>(null);
const visGraph = ref<Nullable<any>>(null);
const task = ref<any[]>([]);
const isUpdate = ref<boolean>(false);
function showTimeLine() {
visTimeline();
}
function visTimeline() {
if (!timeRange.value) {
return;
}
if (!networkProfilingStore.selectedNetworkTask.taskId) {
return;
}
const { taskStartTime, fixedTriggerDuration, targetType, taskId } =
networkProfilingStore.selectedNetworkTask;
if (task.value[0] && task.value[0].data.taskId === taskId) {
if (isUpdate.value) {
return;
}
}
if (visGraph.value) {
visGraph.value.destroy();
}
isUpdate.value = false;
let startTime = taskStartTime;
if (fixedTriggerDuration > 1800) {
startTime = taskStartTime + fixedTriggerDuration * 1000 - 30 * 60 * 1000;
}
const d = networkProfilingStore.networkTasks[0] || {};
let endTime = taskStartTime + fixedTriggerDuration * 1000;
if (
taskStartTime + fixedTriggerDuration * 1000 > new Date().getTime() &&
taskId === d.taskId
) {
endTime = new Date().getTime();
}
task.value = [
{
id: 1,
content: "",
start: new Date(startTime),
end: new Date(endTime),
data: networkProfilingStore.selectedNetworkTask,
className: targetType,
},
];
const items: any = new DataSet(task.value);
items.on("update", (event: string, properties: any) => {
task.value = properties.data;
});
const itemsAlwaysDraggable =
fixedTriggerDuration > 1800
? {
item: true,
range: true,
}
: undefined;
const editable =
fixedTriggerDuration > 1800
? {
updateTime: true,
}
: false;
const options = {
height: 150,
width: "100%",
locale: "en",
editable,
zoomMin: 1000 * 60,
zoomMax: 1000 * 60 * 60 * 24,
};
const opt = itemsAlwaysDraggable
? { ...options, itemsAlwaysDraggable }
: options;
visGraph.value = new Timeline(timeRange.value, items, opt);
}
async function updateTopology() {
isUpdate.value = true;
emits("get", {
start: task.value[0].start.getTime(),
end: task.value[0].end.getTime(),
});
const serviceInstanceId =
(selectorStore.currentPod && selectorStore.currentPod.id) || "";
const resp = await networkProfilingStore.getProcessTopology({
serviceInstanceId,
duration: {
start: dateFormatStep(
getLocalTime(appStore.utc, new Date(task.value[0].start)),
appStore.duration.step,
true
),
end: dateFormatStep(
getLocalTime(appStore.utc, new Date(task.value[0].end)),
appStore.duration.step,
true
),
step: appStore.duration.step,
},
});
if (resp.errors) {
ElMessage.error(resp.errors);
}
return resp;
}
</script>
<style lang="scss" scoped>
.switch-icon-edit {
cursor: pointer;
transition: all 0.5ms linear;
border: 1px solid #ccc;
color: #666;
display: inline-block;
padding: 5px;
border-radius: 3px;
position: absolute;
top: 10px;
right: 50px;
}
</style>

View File

@@ -53,16 +53,14 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import { ref } from "vue";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
import { useProfileStore } from "@/store/modules/profile";
import { Trace } from "@/types/trace";
import { ElMessage } from "element-plus";
import { dateFormat } from "@/utils/dateFormat";
const { t } = useI18n();
const profileStore = useProfileStore();
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
const selectedKey = ref<string>("");
async function selectTrace(item: Trace) {

View File

@@ -40,7 +40,9 @@ limitations under the License. -->
</a>
</div>
<div class="grey ell sm">
<span class="mr-10 sm">{{ dateFormat(i.startTime) }}</span>
<span class="mr-10 sm">
{{ dateFormat(i.startTime) }}
</span>
<span class="mr-10 sm">
{{ dateFormat(i.startTime + i.duration * 60 * 1000) }}
</span>
@@ -122,18 +124,16 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import { ref } from "vue";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
import { useSelectorStore } from "@/store/modules/selectors";
import { useProfileStore } from "@/store/modules/profile";
import { TaskLog, TaskListItem } from "@/types/profile";
import { ElMessage } from "element-plus";
import { dateFormat } from "@/utils/dateFormat";
const { t } = useI18n();
const profileStore = useProfileStore();
const selectorStore = useSelectorStore();
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
const viewDetail = ref<boolean>(false);
const service = ref<string>("");
const selectedTask = ref<TaskListItem | Record<string, never>>({});

View File

@@ -94,17 +94,10 @@ import {
import { useI18n } from "vue-i18n";
import * as d3 from "d3";
import d3tip from "d3-tip";
import zoom from "../../components/D3Graph/zoom";
import {
simulationInit,
simulationSkip,
} from "../../components/D3Graph/simulation";
import nodeElement from "../../components/D3Graph/nodeElement";
import {
linkElement,
anchorElement,
arrowMarker,
} from "../../components/D3Graph/linkElement";
import zoom from "../../components/utils/zoom";
import { simulationInit, simulationSkip } from "./utils/simulation";
import nodeElement from "./utils/nodeElement";
import { linkElement, anchorElement, arrowMarker } from "./utils/linkElement";
import { Node, Call } from "@/types/topology";
import { useSelectorStore } from "@/store/modules/selectors";
import { useTopologyStore } from "@/store/modules/topology";
@@ -200,7 +193,7 @@ async function init() {
link.value = graph.value.append("g").selectAll(".topo-line");
anchor.value = graph.value.append("g").selectAll(".topo-line-anchor");
arrow.value = graph.value.append("g").selectAll(".topo-line-arrow");
svg.value.call(zoom(d3, graph.value));
svg.value.call(zoom(d3, graph.value, [-100, -100]));
svg.value.on("click", (event: any) => {
event.stopPropagation();
event.preventDefault();

View File

@@ -16,12 +16,12 @@
*/
export const simulationInit = (
d3: any,
dataNodes: any,
dataLinks: any,
nodes: any,
links: any,
ticked: any
) => {
const simulation = d3
.forceSimulation(dataNodes)
.forceSimulation(nodes)
.force(
"collide",
d3.forceCollide().radius(() => 60)
@@ -31,7 +31,7 @@ export const simulationInit = (
.force("charge", d3.forceManyBody().strength(-520))
.force(
"link",
d3.forceLink(dataLinks).id((d: { id: string }) => d.id)
d3.forceLink(links).id((d: { id: string }) => d.id)
)
.force(
"center",

View File

@@ -120,7 +120,6 @@ limitations under the License. -->
</div>
</template>
<script lang="ts">
import dayjs from "dayjs";
import { ref, defineComponent, inject } from "vue";
import { useI18n } from "vue-i18n";
import { useTraceStore } from "@/store/modules/trace";
@@ -130,6 +129,8 @@ import graphs from "./components/index";
import { ElMessage } from "element-plus";
import getDashboard from "@/hooks/useDashboardsSession";
import { LayoutConfig } from "@/types/dashboard";
import { dateFormat } from "@/utils/dateFormat";
import { useAppStoreWithOut } from "@/store/modules/app";
export default defineComponent({
name: "TraceDetail",
@@ -137,6 +138,7 @@ export default defineComponent({
...graphs,
},
setup() {
const appStore = useAppStoreWithOut();
/*global Recordable */
const options: Recordable<LayoutConfig> = inject("options") || {};
const { t } = useI18n();
@@ -144,8 +146,6 @@ export default defineComponent({
const loading = ref<boolean>(false);
const traceId = ref<string>("");
const displayMode = ref<string>("List");
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
function handleClick() {
copy(traceId.value || traceStore.currentTrace.traceIds[0].value);
@@ -180,6 +180,7 @@ export default defineComponent({
handleClick,
t,
searchTraceLogs,
appStore,
loading,
traceId,
};

View File

@@ -70,7 +70,6 @@ limitations under the License. -->
</template>
<script lang="ts" setup>
import dayjs from "dayjs";
import { ref, computed } from "vue";
import { useI18n } from "vue-i18n";
import { useTraceStore } from "@/store/modules/trace";
@@ -78,6 +77,7 @@ import { ElMessage } from "element-plus";
import { QueryOrders } from "../../data";
import { Option } from "@/types/app";
import { Trace } from "@/types/trace";
import { dateFormat } from "@/utils/dateFormat";
const { t } = useI18n();
const traceStore = useTraceStore();
@@ -89,8 +89,6 @@ const total = computed(() =>
? pageSize.value * traceStore.conditions.paging.pageNum + 1
: pageSize.value * traceStore.conditions.paging.pageNum
);
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
function searchTrace() {
loading.value = true;

View File

@@ -86,10 +86,10 @@ limitations under the License. -->
import { inject } from "vue";
import { useI18n } from "vue-i18n";
import type { PropType } from "vue";
import dayjs from "dayjs";
import copy from "@/utils/copy";
import getDashboard from "@/hooks/useDashboardsSession";
import { LayoutConfig } from "@/types/dashboard";
import { dateFormat } from "@/utils/dateFormat";
/*global defineProps, Recordable */
const options: Recordable<LayoutConfig> = inject("options") || {};
@@ -97,8 +97,6 @@ const props = defineProps({
currentSpan: { type: Object as PropType<any>, default: () => ({}) },
});
const { t } = useI18n();
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
async function getTaceLogs() {
const { associationWidget } = getDashboard();
associationWidget(

View File

@@ -138,11 +138,12 @@ limitations under the License. -->
</div>
</template>
<script lang="ts">
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
import { ref, computed, defineComponent } from "vue";
import type { PropType } from "vue";
import SpanDetail from "../D3Graph/SpanDetail.vue";
import { dateFormat } from "@/utils/dateFormat";
import { useAppStoreWithOut } from "@/store/modules/app";
const props = {
data: { type: Object as PropType<any>, default: () => ({}) },
@@ -156,11 +157,10 @@ export default defineComponent({
emits: ["select"],
components: { SpanDetail },
setup(props, { emit }) {
const appStore = useAppStoreWithOut();
const displayChildren = ref<boolean>(true);
const showDetail = ref<boolean>(false);
const { t } = useI18n();
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
const selfTime = computed(() => (props.data.dur ? props.data.dur : 0));
const execTime = computed(() =>
props.data.endTime - props.data.startTime
@@ -239,6 +239,7 @@ export default defineComponent({
selectedItem,
viewSpan,
t,
appStore,
};
},
});