feat: associate metrics with trace widget on dashboards (#174)

This commit is contained in:
Fine0830 2022-10-25 11:36:49 +08:00 committed by GitHub
parent 78f0096c00
commit eda44db0cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1041 additions and 192 deletions

View File

@ -0,0 +1,16 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<svg t="1666624449554" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2649" width="48" height="48"><path d="M381.482667 673.877333a90.389333 90.389333 0 0 1 85.226666 60.245334H853.333333v64H465.28a90.389333 90.389333 0 0 1-167.573333 0H170.666667v-64h125.610666a90.389333 90.389333 0 0 1 85.205334-60.245334z m0 64a26.346667 26.346667 0 1 0 0 52.693334 26.346667 26.346667 0 0 0 0-52.693334z m261.034666-304.938666a90.389333 90.389333 0 0 1 85.205334 60.245333H853.333333v64h-127.04a90.389333 90.389333 0 0 1-167.573333 0H170.666667v-64h386.624a90.389333 90.389333 0 0 1 85.226666-60.245333z m0 64a26.346667 26.346667 0 1 0 0 52.693333 26.346667 26.346667 0 0 0 0-52.693333zM381.482667 192a90.389333 90.389333 0 0 1 85.226666 60.224H853.333333v64H465.28a90.389333 90.389333 0 0 1-167.573333 0H170.666667v-64h125.610666A90.389333 90.389333 0 0 1 381.482667 192z m0 64a26.346667 26.346667 0 1 0 0 52.693333 26.346667 26.346667 0 0 0 0-52.693333z" p-id="2650"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

15
src/assets/icons/copy.svg Normal file
View File

@ -0,0 +1,15 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<svg t="1664265269855" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4109" width="48" height="48"><path d="M866.461538 39.384615H354.461538c-43.323077 0-78.769231 35.446154-78.76923 78.769231v39.384616h472.615384c43.323077 0 78.769231 35.446154 78.769231 78.76923v551.384616h39.384615c43.323077 0 78.769231-35.446154 78.769231-78.769231V118.153846c0-43.323077-35.446154-78.769231-78.769231-78.769231z m-118.153846 275.692308c0-43.323077-35.446154-78.769231-78.76923-78.769231H157.538462c-43.323077 0-78.769231 35.446154-78.769231 78.769231v590.769231c0 43.323077 35.446154 78.769231 78.769231 78.769231h512c43.323077 0 78.769231-35.446154 78.76923-78.769231V315.076923z m-354.461538 137.846154c0 11.815385-7.876923 19.692308-19.692308 19.692308h-157.538461c-11.815385 0-19.692308-7.876923-19.692308-19.692308v-39.384615c0-11.815385 7.876923-19.692308 19.692308-19.692308h157.538461c11.815385 0 19.692308 7.876923 19.692308 19.692308v39.384615z m157.538461 315.076923c0 11.815385-7.876923 19.692308-19.692307 19.692308H216.615385c-11.815385 0-19.692308-7.876923-19.692308-19.692308v-39.384615c0-11.815385 7.876923-19.692308 19.692308-19.692308h315.076923c11.815385 0 19.692308 7.876923 19.692307 19.692308v39.384615z m78.769231-157.538462c0 11.815385-7.876923 19.692308-19.692308 19.692308H216.615385c-11.815385 0-19.692308-7.876923-19.692308-19.692308v-39.384615c0-11.815385 7.876923-19.692308 19.692308-19.692308h393.846153c11.815385 0 19.692308 7.876923 19.692308 19.692308v39.384615z" p-id="4110"></path></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -13,6 +13,5 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>info_outline</title>
<path d="M11.016 9v-2.016h1.969v2.016h-1.969zM12 20.016q3.281 0 5.648-2.367t2.367-5.648-2.367-5.648-5.648-2.367-5.648 2.367-2.367 5.648 2.367 5.648 5.648 2.367zM12 2.016q4.125 0 7.055 2.93t2.93 7.055-2.93 7.055-7.055 2.93-7.055-2.93-2.93-7.055 2.93-7.055 7.055-2.93zM11.016 17.016v-6h1.969v6h-1.969z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,15 @@
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<svg t="1664266918236" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5378" width="48" height="48"><path d="M571.178667 643.328a144 144 0 0 1-189.098667-193.450667l77.781333 77.866667a48 48 0 1 0 67.882667-67.84l-77.824-77.909333a144 144 0 0 1 193.450667 189.141333l226.517333 207.061333a64.896 64.896 0 1 1-91.690667 91.690667l-207.018666-226.56z m51.498666 134.656a288.298667 288.298667 0 0 1-38.656 12.928v95.488c0 5.290667-4.309333 9.6-9.642666 9.6h-124.757334a9.6 9.6 0 0 1-9.6-9.6v-95.488a286.293333 286.293333 0 0 1-74.325333-30.805333l-67.541333 67.541333a9.6 9.6 0 0 1-13.568 0L196.352 739.413333a9.6 9.6 0 0 1 0-13.568l67.541333-67.541333a286.293333 286.293333 0 0 1-30.805333-74.325333H137.6A9.6 9.6 0 0 1 128 574.378667v-124.757334c0-5.290667 4.309333-9.6 9.6-9.6h95.488c6.826667-26.453333 17.28-51.370667 30.805333-74.325333L196.352 298.154667a9.6 9.6 0 0 1 0-13.568L284.586667 196.352a9.6 9.6 0 0 1 13.568 0l67.541333 67.498667a287.146667 287.146667 0 0 1 74.325333-30.848V137.6c0-5.290667 4.266667-9.6 9.6-9.6h124.8c5.248 0 9.6 4.309333 9.6 9.6v95.488c26.368 6.826667 51.328 17.28 74.282667 30.805333l67.541333-67.541333a9.6 9.6 0 0 1 13.568 0l88.234667 88.234667a9.6 9.6 0 0 1 0 13.568l-67.498667 67.541333a287.146667 287.146667 0 0 1 30.848 74.282667h95.402667c5.290667 0 9.6 4.352 9.6 9.642666v124.757334c0 5.333333-4.266667 9.6-9.6 9.6h-95.488c-4.693333 18.133333-11.178667 35.754667-19.328 52.650666a9.6 9.6 0 0 1-15.018667 2.986667l-10.112-9.173333-38.314666-34.261334-12.16-10.88a9.6 9.6 0 0 1-2.688-10.24A192.298667 192.298667 0 0 0 512 320a192 192 0 1 0 63.018667 373.333333 9.6 9.6 0 0 1 10.24 2.645334l10.837333 12.074666 35.285333 39.338667 8.149334 9.130667a9.6 9.6 0 0 1-2.901334 15.061333 283.306667 283.306667 0 0 1-13.952 6.4z" p-id="5379"></path></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -15,6 +15,27 @@ limitations under the License. -->
<template>
<div class="chart" ref="chartRef" :style="`height:${height};width:${width};`">
<div v-if="!available" class="no-data">No Data</div>
<div class="menus" v-show="visMenus" ref="menus">
<div class="tools" @click="associateMetrics">
{{ t("associateMetrics") }}
</div>
<div
class="tools"
@click="viewTrace"
v-if="props.relatedTrace && props.relatedTrace.enableRelate"
>
{{ t("viewTrace") }}
</div>
</div>
<el-drawer
v-model="showTrace"
size="100%"
:destroy-on-close="true"
:before-close="() => (showTrace = false)"
:append-to-body="true"
>
<Trace :data="traceOptions" />
</el-drawer>
</div>
</template>
<script lang="ts" setup>
@ -28,15 +49,28 @@ import {
computed,
} from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { EventParams } from "@/types/app";
import { Filters, RelatedTrace } from "@/types/dashboard";
import { useECharts } from "@/hooks/useEcharts";
import { addResizeListener, removeResizeListener } from "@/utils/event";
import Trace from "@/views/dashboard/related/trace/Index.vue";
import associateProcessor from "@/hooks/useAssociateProcessor";
/*global Nullable, defineProps, defineEmits*/
const emits = defineEmits(["select"]);
const { t } = useI18n();
const chartRef = ref<Nullable<HTMLDivElement>>(null);
const menus = ref<Nullable<HTMLDivElement>>(null);
const visMenus = ref<boolean>(false);
const { setOptions, resize, getInstance } = useECharts(
chartRef as Ref<HTMLDivElement>
);
const currentParams = ref<Nullable<EventParams>>(null);
const showTrace = ref<boolean>(false);
const traceOptions = ref<{ type: string; filters?: unknown }>({
type: "Trace",
});
const props = defineProps({
height: { type: String, default: "100%" },
width: { type: String, default: "100%" },
@ -45,15 +79,10 @@ const props = defineProps({
default: () => ({}),
},
filters: {
type: Object as PropType<{
duration: {
startTime: string;
endTime: string;
};
isRange: boolean;
dataIndex?: number;
sourceId: string;
}>,
type: Object as PropType<Filters>,
},
relatedTrace: {
type: Object as PropType<RelatedTrace>,
},
});
const available = computed(
@ -66,14 +95,29 @@ const available = computed(
onMounted(async () => {
await setOptions(props.option);
chartRef.value && addResizeListener(unref(chartRef), resize);
instanceEvent();
});
function instanceEvent() {
setTimeout(() => {
const instance = getInstance();
if (!instance) {
return;
}
instance.on("click", (params: unknown) => {
emits("select", params);
instance.on("click", (params: EventParams) => {
currentParams.value = params;
if (!menus.value || !chartRef.value) {
return;
}
visMenus.value = true;
const w = chartRef.value.getBoundingClientRect().width || 0;
if (w - params.event.offsetX > 125) {
menus.value.style.left = params.event.offsetX + "px";
} else {
menus.value.style.left = params.event.offsetX - 125 + "px";
}
menus.value.style.top = params.event.offsetY + 5 + "px";
});
document.addEventListener(
"click",
@ -81,9 +125,7 @@ onMounted(async () => {
if (instance.isDisposed()) {
return;
}
instance.dispatchAction({
type: "hideTip",
});
visMenus.value = false;
instance.dispatchAction({
type: "updateAxisPointer",
currTrigger: "leave",
@ -92,7 +134,13 @@ onMounted(async () => {
true
);
}, 1000);
});
}
function associateMetrics() {
emits("select", currentParams.value);
visMenus.value = true;
updateOptions();
}
function updateOptions() {
const instance = getInstance();
@ -103,60 +151,26 @@ function updateOptions() {
return;
}
if (props.filters.isRange) {
const { eventAssociate } = associateProcessor(props);
const options = eventAssociate();
setOptions(options || props.option);
} else {
instance.dispatchAction({
type: "showTip",
type: "updateAxisPointer",
dataIndex: props.filters.dataIndex,
seriesIndex: 0,
});
}
}
function eventAssociate() {
if (!props.filters) {
return;
}
if (!props.filters.duration) {
return props.option;
}
if (!props.option.series[0]) {
return;
}
const list = props.option.series[0].data.map(
(d: (number | string)[]) => d[0]
);
if (!list.includes(props.filters.duration.endTime)) {
return;
}
const markArea = {
silent: true,
itemStyle: {
opacity: 0.3,
},
data: [
[
{
xAxis: props.filters.duration.startTime,
},
{
xAxis: props.filters.duration.endTime,
},
],
],
function viewTrace() {
const item = associateProcessor(props).traceFilters(currentParams.value);
traceOptions.value = {
...traceOptions.value,
filters: item,
};
const series = (window as any).structuredClone(props.option.series);
for (const [key, temp] of series.entries()) {
if (key === 0) {
temp.markArea = markArea;
}
}
const options = {
...props.option,
series,
};
return options;
showTrace.value = true;
visMenus.value = true;
}
watch(
@ -170,6 +184,7 @@ watch(
}
let options;
if (props.filters && props.filters.isRange) {
const { eventAssociate } = associateProcessor(props);
options = eventAssociate();
}
setOptions(options || props.option);
@ -201,4 +216,28 @@ onBeforeUnmount(() => {
.chart {
overflow: hidden;
}
.menus {
position: absolute;
display: block;
white-space: nowrap;
z-index: 9999999;
box-shadow: #ddd 1px 2px 10px;
transition: all cubic-bezier(0.075, 0.82, 0.165, 1) linear;
background-color: rgb(255, 255, 255);
border-radius: 4px;
color: rgb(51, 51, 51);
padding: 5px;
}
.tools {
padding: 5px;
color: #999;
cursor: pointer;
&:hover {
color: #409eff;
background-color: #eee;
}
}
</style>

View File

@ -32,6 +32,6 @@ export const queryInstances = `query queryInstances(${Instances.variable}) {${In
export const queryLayers = `query listLayer {${Layers.query}}`;
export const queryService = `query queryService(${getService.variable}) {${getService.query}}`;
export const queryInstance = `query queryInstance(${getInstance.variable}) {${getInstance.query}}`;
export const queryEndpoint = `query queryInstance(${getEndpoint.variable}) {${getEndpoint.query}}`;
export const queryEndpoint = `query queryEndpoint(${getEndpoint.variable}) {${getEndpoint.query}}`;
export const queryProcesses = `query queryProcesses(${Processes.variable}) {${Processes.query}}`;
export const queryProcess = `query queryProcess(${getProcess.variable}) {${getProcess.query}}`;

View File

@ -0,0 +1,138 @@
/**
* 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 { useAppStoreWithOut } from "@/store/modules/app";
import dateFormatStep from "@/utils/dateFormat";
import getLocalTime from "@/utils/localtime";
import { EventParams } from "@/types/app";
export default function associateProcessor(props: any) {
function eventAssociate() {
if (!props.filters) {
return;
}
if (!props.filters.duration) {
return props.option;
}
if (!props.option.series[0]) {
return;
}
const list = props.option.series[0].data.map(
(d: (number | string)[]) => d[0]
);
if (!list.includes(props.filters.duration.endTime)) {
return;
}
const markArea = {
silent: true,
itemStyle: {
opacity: 0.3,
},
data: [
[
{
xAxis: props.filters.duration.startTime,
},
{
xAxis: props.filters.duration.endTime,
},
],
],
};
const series = (window as any).structuredClone(props.option.series);
for (const [key, temp] of series.entries()) {
if (key === 0) {
temp.markArea = markArea;
}
}
const options = {
...props.option,
series,
};
return options;
}
function traceFilters(currentParams: Nullable<EventParams>) {
const appStore = useAppStoreWithOut();
if (!currentParams) {
return;
}
const start = appStore.intervalUnix[currentParams.dataIndex];
const { step } = appStore.durationRow;
let duration = undefined;
if (start) {
const end = start;
duration = {
start: dateFormatStep(
getLocalTime(appStore.utc, new Date(start)),
step,
true
),
end: dateFormatStep(
getLocalTime(appStore.utc, new Date(end)),
step,
true
),
step,
};
}
const relatedTrace = props.relatedTrace || {};
const status = relatedTrace.status;
const queryOrder = relatedTrace.queryOrder;
const latency = relatedTrace.latency;
const series = props.option.series || [];
const item: any = {
duration,
queryOrder,
status,
};
if (latency) {
const latencyList = series.map(
(d: { name: string; data: number[][] }, index: number) => {
const data = [
d.data[currentParams.dataIndex][1],
series[index + 1]
? series[index + 1].data[currentParams.dataIndex][1]
: Infinity,
];
return {
label:
d.name +
"--" +
(series[index + 1] ? series[index + 1].name : "Infinity"),
value: String(index),
data,
};
}
);
item.latency = latencyList;
}
const value = series.map(
(d: { name: string; data: number[][] }, index: number) => {
return {
label: d.name,
value: String(index),
data: d.data[currentParams.dataIndex][1],
date: d.data[currentParams.dataIndex][0],
};
}
);
item.metricValue = value;
return item;
}
return { eventAssociate, traceFilters };
}

View File

@ -146,6 +146,7 @@ const msg = {
pause: "Pause",
begin: "Start",
associateOptions: "Association Options",
associateMetrics: "Association Metrics",
widget: "Widget",
nameTip:
"The name only supports Chinese and English, horizontal lines and underscores. The length of the name is limited to 300 characters",
@ -156,6 +157,16 @@ const msg = {
postgreSQL: "PostgreSQL",
endpointTips: "The table shows up to 20 pieces of endpoints.",
apisix: "APISIX",
viewTrace: "View Related Traces",
relatedTraceOptions: "Related Trace Options",
setLatencyDuration: "Set Latency Range",
queryOrder: "Query Order",
latency: "Latency",
metricValues: "Metric Values",
queryConditions: "Query Conditions",
enableRelatedTrace: "Enable Related Trace",
maxTraceDuration: "Maximum Duration",
minTraceDuration: "Minimum Duration",
seconds: "Seconds",
hourTip: "Select Hour",
minuteTip: "Select Minute",
@ -348,6 +359,6 @@ const msg = {
"Notice: Please press Enter after inputting a key of content, exclude key of content(key=value).",
language: "Language",
gateway: "Gateway",
virtualMQ: "Virtual MQ"
virtualMQ: "Virtual MQ",
};
export default msg;

View File

@ -146,6 +146,7 @@ const msg = {
pause: "Pausa",
begin: "Inicio",
associateOptions: "Opciones de asociación",
associateMetrics: "Índice de correlación",
widget: "Dispositivo pequeño",
text: "Texto",
duplicateName: "Nombre duplicado",
@ -156,10 +157,20 @@ const msg = {
postgreSQL: "PostgreSQL",
endpointTips: "Aquí, la tabla muestra hasta 20 punto final.",
apisix: "APISIX",
queryOrder: "Orden de consulta",
latency: "Retraso",
metricValues: "Valor métrico",
seconds: "Segundos",
hourTip: "Seleccione Hora",
minuteTip: "Seleccione Minuto",
secondTip: "Seleccione Segundo",
viewTrace: "Ver trazas relacionadas",
relatedTraceOptions: "Opciones de seguimiento relacionadas",
setLatencyDuration: "Establecer el rango de retardo",
enableRelatedTrace: "Activar trazas relacionadas",
queryConditions: "Condiciones de consulta",
maxTraceDuration: "Duración máxima",
minTraceDuration: "Duración mínima",
second: "s",
yearSuffix: "Año",
monthsHead: "Ene_Feb_Mar_Abr_May_Jun_Jul_Ago_Set_Oct_Nov_Dic",

View File

@ -144,6 +144,7 @@ const msg = {
pause: "暂停",
begin: "开始",
associateOptions: "关联选项",
associateMetrics: "关联指标",
widget: "部件",
enableAssociate: "启用关联",
nameTip: "该名称仅支持中文和英文、横线和下划线, 并且限制长度为300个字符",
@ -153,6 +154,16 @@ const msg = {
postgreSQL: "PostgreSQL",
endpointTips: "这里最多展示20条endpoints。",
apisix: "APISIX",
viewTrace: "查看相关Trace",
relatedTraceOptions: "相关的Trace选项",
setLatencyDuration: "设置延时范围",
queryOrder: "查询顺序",
latency: "延迟",
metricValues: "指标值",
enableRelatedTrace: "启用相关Trace",
queryConditions: "查询条件",
maxTraceDuration: "最大持续时间",
minTraceDuration: "最小持续时间",
seconds: "秒",
hourTip: "选择小时",
minuteTip: "选择分钟",

View File

@ -88,7 +88,7 @@ export default [
notShow: true,
layer: "VIRTUAL_MQ",
},
},
},
],
},
];

View File

@ -71,7 +71,7 @@ export const appStore = defineStore({
step: this.duration.step,
};
},
intervalTime(): string[] {
intervalUnix(): number[] {
let interval = 946080000000;
switch (this.duration.step) {
case "MINUTE":
@ -97,12 +97,17 @@ export const appStore = defineStore({
this.utcMin * 60000;
const startUnix: number = this.duration.start.getTime();
const endUnix: number = this.duration.end.getTime();
const timeIntervals: string[] = [];
const timeIntervals: number[] = [];
for (let i = 0; i <= endUnix - startUnix; i += interval) {
const temp: string = dateFormatTime(
new Date(startUnix + i - utcSpace),
this.duration.step
);
timeIntervals.push(startUnix + i - utcSpace);
}
return timeIntervals;
},
intervalTime(): string[] {
const arr = this.intervalUnix;
const timeIntervals: string[] = [];
for (const item of arr) {
const temp: string = dateFormatTime(new Date(item), this.duration.step);
timeIntervals.push(temp);
}
return timeIntervals;

View File

@ -22,6 +22,7 @@ import graphql from "@/graphql";
import { AxiosResponse } from "axios";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import { QueryOrders } from "@/views/dashboard/data";
interface TraceState {
services: Service[];
@ -47,7 +48,7 @@ export const traceStore = defineStore({
conditions: {
queryDuration: useAppStoreWithOut().durationTime,
traceState: "ALL",
queryOrder: "BY_START_TIME",
queryOrder: QueryOrders[0].value,
paging: { pageNum: 1, pageSize: 20 },
},
traceSpanLogs: [],
@ -71,7 +72,7 @@ export const traceStore = defineStore({
queryDuration: useAppStoreWithOut().durationTime,
paging: { pageNum: 1, pageSize: 20 },
traceState: "ALL",
queryOrder: "BY_START_TIME",
queryOrder: QueryOrders[0].value,
};
},
async getServices(layer: string) {
@ -84,6 +85,36 @@ export const traceStore = defineStore({
this.services = res.data.data.services;
return res.data;
},
async getService(serviceId: string) {
if (!serviceId) {
return;
}
const res: AxiosResponse = await graphql.query("queryService").params({
serviceId,
});
return res.data;
},
async getInstance(instanceId: string) {
if (!instanceId) {
return;
}
const res: AxiosResponse = await graphql.query("queryInstance").params({
instanceId,
});
return res.data;
},
async getEndpoint(endpointId: string) {
if (!endpointId) {
return;
}
const res: AxiosResponse = await graphql.query("queryEndpoint").params({
endpointId,
});
return res.data;
},
async getInstances(id: string) {
const serviceId = this.selectorStore.currentService
? this.selectorStore.currentService.id

View File

@ -17,6 +17,7 @@
import "element-plus/es/components/message/style/css";
import "element-plus/es/components/message-box/style/css";
import "element-plus/es/components/notification/style/css";
import "element-plus/es/components/drawer/style/css";
import "./grid.scss";
import "./lib.scss";
import "./reset.scss";

View File

@ -153,6 +153,14 @@ pre {
margin-left: 5px;
}
.el-drawer__header {
margin-bottom: 0;
}
.el-drawer__body {
padding: 0;
}
.switch {
margin: 0 5px;
}

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

@ -32,3 +32,18 @@ export type Paging = {
pageNum: number;
pageSize: number;
};
export type EventParams = {
componentType: string;
seriesType: string;
seriesIndex: number;
seriesName: string;
name: string;
dataIndex: number;
data: unknown;
dataType: string;
value: number | Array;
color: string;
event: any;
dataIndex: number;
};

View File

@ -25,6 +25,7 @@ declare module '@vue/runtime-core' {
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSlider: typeof import('element-plus/es')['ElSlider']

View File

@ -1,3 +1,4 @@
import { DurationTime } from "@/types/app";
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
@ -39,19 +40,32 @@ export interface LayoutConfig {
id?: string;
associate?: { widgetId: string }[];
eventAssociate?: boolean;
filters?: {
dataIndex: number;
sourceId: string;
isRange?: boolean;
duration?: {
startTime: string;
endTime: string;
};
traceId?: string;
spanId?: string;
segmentId?: string;
};
filters?: Filters;
relatedTrace?: RelatedTrace;
}
export type RelatedTrace = {
duration: DurationTime;
status: string;
queryOrder: string;
latency: boolean;
enableRelate: boolean;
};
export type Filters = {
dataIndex: number;
sourceId: string;
isRange?: boolean;
duration?: {
startTime: string;
endTime: string;
};
traceId?: string;
spanId?: string;
segmentId?: string;
id?: string;
queryOrder?: string;
status?: string;
};
export type MetricConfigOpt = {
unit?: string;

View File

@ -36,6 +36,7 @@ limitations under the License. -->
metrics: dashboardStore.selectedGrid.metrics,
metricTypes: dashboardStore.selectedGrid.metricTypes,
metricConfig: dashboardStore.selectedGrid.metricConfig,
relatedTrace: dashboardStore.selectedGrid.relatedTrace,
}"
:needQuery="true"
/>
@ -65,6 +66,13 @@ limitations under the License. -->
>
<AssociateOptions />
</el-collapse-item>
<el-collapse-item
:title="t('relatedTraceOptions')"
name="5"
v-if="hasAssociate"
>
<RelatedTraceOptions />
</el-collapse-item>
</el-collapse>
</div>
<div class="footer">

View File

@ -0,0 +1,90 @@
<!-- 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">
<span class="label">{{ t("enableRelatedTrace") }}</span>
<el-switch
v-model="enableRelate"
active-text="Yes"
inactive-text="No"
@change="updateConfig({ enableRelate })"
/>
</div>
<div v-show="enableRelate">
<div class="item">
<span class="label">{{ t("status") }}</span>
<Selector
size="small"
:value="status"
:options="Status"
placeholder="Select a status"
@change="updateConfig({ status: $event[0].value })"
/>
</div>
<div class="item">
<span class="label">{{ t("queryOrder") }}</span>
<Selector
size="small"
:value="queryOrder"
:options="QueryOrders"
placeholder="Select a option"
@change="updateConfig({ queryOrder: $event[0].value })"
/>
</div>
<div class="item">
<span class="label">{{ t("setLatencyDuration") }}</span>
<el-switch
v-model="latency"
active-text="Yes"
inactive-text="No"
@change="updateConfig({ latency })"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import { Status, QueryOrders } from "../../data";
const { t } = useI18n();
const dashboardStore = useDashboardStore();
const traceOpt = dashboardStore.selectedGrid.relatedTrace || {};
const status = ref<string>(traceOpt.status || Status[0].value);
const queryOrder = ref<string>(traceOpt.queryOrder || QueryOrders[0].value);
const latency = ref<boolean>(traceOpt.setLatencyDuration || false);
const enableRelate = ref<boolean>(traceOpt.enableRelate || false);
function updateConfig(param: { [key: string]: unknown }) {
const relatedTrace = {
...dashboardStore.selectedGrid.relatedTrace,
...param,
};
dashboardStore.selectWidget({ ...dashboardStore.selectedGrid, relatedTrace });
}
</script>
<style lang="scss" scoped>
.label {
font-size: 13px;
font-weight: 500;
display: block;
margin-bottom: 5px;
}
.item {
margin-bottom: 10px;
}
</style>

View File

@ -19,10 +19,12 @@ import StyleOptions from "./graph-styles";
import WidgetOptions from "./WidgetOptions.vue";
import MetricOptions from "./metric/Index.vue";
import AssociateOptions from "./AssociateOptions.vue";
import RelatedTraceOptions from "./RelatedTraceOptions.vue";
export default {
...StyleOptions,
WidgetOptions,
MetricOptions,
AssociateOptions,
RelatedTraceOptions,
};

View File

@ -40,7 +40,7 @@ limitations under the License. -->
<span class="tab-icons">
<el-tooltip content="Copy Link" placement="bottom">
<i @click="copyLink">
<Icon size="middle" iconName="review-list" class="tab-icon" />
<Icon size="middle" iconName="copy" class="tab-icon" />
</i>
</el-tooltip>
</span>

View File

@ -64,6 +64,7 @@ limitations under the License. -->
id: data.id,
metricConfig: data.metricConfig,
filters: data.filters || {},
relatedTrace: data.relatedTrace || {},
}"
:needQuery="needQuery"
@click="clickHandle"

View File

@ -174,7 +174,7 @@ export const SortOrder = [
];
export const AllTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
{ name: "library_books", content: "Add Text", id: "addText" },
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
{ name: "merge", content: "Add Trace", id: "addTrace" },
@ -182,7 +182,7 @@ export const AllTools = [
];
export const ServiceTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
{ name: "library_books", content: "Add Text", id: "addText" },
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
{ name: "merge", content: "Add Trace", id: "addTrace" },
@ -194,7 +194,7 @@ export const ServiceTools = [
];
export const InstanceTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
{ name: "library_books", content: "Add Text", id: "addText" },
{ name: "merge", content: "Add Trace", id: "addTrace" },
{ name: "assignment", content: "Add Log", id: "addLog" },
@ -208,7 +208,7 @@ export const InstanceTools = [
];
export const EndpointTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
{ name: "library_books", content: "Add Text", id: "addText" },
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
{ name: "merge", content: "Add Trace", id: "addTrace" },
@ -217,25 +217,25 @@ export const EndpointTools = [
];
export const ProcessTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
{ 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" },
];
export const ServiceRelationTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
{ name: "library_books", content: "Add Text", id: "addText" },
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
];
export const EndpointRelationTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
{ name: "library_books", content: "Add Text", id: "addText" },
];
export const InstanceRelationTools = [
{ name: "playlist_add", content: "Add Widget", id: "addWidget" },
{ name: "all_inbox", content: "Add Tab", id: "addTab" },
{ name: "all_inbox", content: "Add Tabs", id: "addTab" },
{ name: "library_books", content: "Add Text", id: "addText" },
{ name: "device_hub", content: "Add Topology", id: "addTopology" },
];

View File

@ -24,7 +24,12 @@ limitations under the License. -->
<script lang="ts" setup>
import type { PropType } from "vue";
import Line from "./Line.vue";
import { AreaConfig, EventParams } from "@/types/dashboard";
import {
AreaConfig,
EventParams,
RelatedTrace,
Filters,
} from "@/types/dashboard";
/*global defineProps, defineEmits */
const emits = defineEmits(["click"]);
@ -37,15 +42,8 @@ defineProps({
config: {
type: Object as PropType<
AreaConfig & {
filters: {
sourceId: string;
duration: {
startTime: string;
endTime: string;
};
isRange: boolean;
dataIndex?: number;
};
filters: Filters;
relatedTrace: RelatedTrace;
} & { id: string }
>,
default: () => ({}),

View File

@ -18,7 +18,12 @@ limitations under the License. -->
<script lang="ts" setup>
import { computed } from "vue";
import type { PropType } from "vue";
import { BarConfig, EventParams } from "@/types/dashboard";
import {
BarConfig,
EventParams,
RelatedTrace,
Filters,
} from "@/types/dashboard";
/*global defineProps, defineEmits */
const emits = defineEmits(["click"]);
@ -32,15 +37,8 @@ const props = defineProps({
config: {
type: Object as PropType<
BarConfig & {
filters: {
sourceId: string;
duration: {
startTime: string;
endTime: string;
};
isRange: boolean;
dataIndex?: number;
};
filters: Filters;
relatedTrace: RelatedTrace;
} & { id: string }
>,
default: () => ({}),
@ -107,16 +105,12 @@ function getOption() {
return {
color,
tooltip: {
trigger: "axis",
zlevel: 1000,
z: 60,
confine: true,
textStyle: {
fontSize: 13,
trigger: "none",
axisPointer: {
type: "cross",
color: "#333",
backgroundColor: "rgba(255, 255, 255, 0.8)",
},
enterable: true,
extraCssText: "max-height: 300px; overflow: auto; border: none",
},
legend: {
type: "scroll",
@ -136,6 +130,12 @@ function getOption() {
bottom: 5,
containLabel: true,
},
axisPointer: {
label: {
color: "#333",
backgroundColor: "rgba(255, 255, 255, 0.8)",
},
},
xAxis: {
type: "category",
axisTick: {

View File

@ -13,12 +13,22 @@ 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>
<Graph :option="option" @select="clickEvent" :filters="config.filters" />
<Graph
:option="option"
@select="clickEvent"
:filters="config.filters"
:relatedTrace="config.relatedTrace"
/>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import type { PropType } from "vue";
import { LineConfig, EventParams } from "@/types/dashboard";
import {
LineConfig,
EventParams,
RelatedTrace,
Filters,
} from "@/types/dashboard";
/*global defineProps, defineEmits */
const emits = defineEmits(["click"]);
@ -32,15 +42,8 @@ const props = defineProps({
config: {
type: Object as PropType<
LineConfig & {
filters: {
sourceId: string;
duration: {
startTime: string;
endTime: string;
};
isRange: boolean;
dataIndex?: number;
};
filters: Filters;
relatedTrace: RelatedTrace;
} & { id: string }
>,
default: () => ({
@ -115,13 +118,19 @@ function getOption() {
break;
}
const tooltip = {
trigger: "axis",
textStyle: {
fontSize: 12,
trigger: "none",
axisPointer: {
type: "cross",
color: "#333",
backgroundColor: "rgba(255, 255, 255, 0.8)",
},
enterable: true,
confine: true,
// trigger: "axis",
// textStyle: {
// fontSize: 12,
// color: "#333",
// },
// enterable: true,
// confine: true,
extraCssText: "max-height: 300px; overflow: auto; border: none;",
};
const tips = {
@ -151,6 +160,12 @@ function getOption() {
color: props.theme === "dark" ? "#fff" : "#333",
},
},
axisPointer: {
label: {
color: "#333",
backgroundColor: "rgba(255, 255, 255, 0.8)",
},
},
grid: {
top: keys.length === 1 ? 15 : 55,
left: 0,

View File

@ -23,14 +23,19 @@ limitations under the License. -->
{{ i.name }}
</span>
</div>
<div class="copy">
<Icon
iconName="review-list"
size="middle"
class="cp"
@click="handleClick(i.name)"
/>
</div>
<el-popover placement="bottom" trigger="click">
<template #reference>
<div class="operation-icon cp ml-10">
<Icon iconName="ellipsis_v" size="middle" />
</div>
</template>
<div class="operation" @click="handleClick(i.name)">
<span>{{ t("copy") }}</span>
</div>
<div class="operation" @click="viewTrace(i)">
<span>{{ t("viewTrace") }}</span>
</div>
</el-popover>
</div>
<el-progress
:stroke-width="6"
@ -39,28 +44,45 @@ limitations under the License. -->
:show-text="false"
/>
</div>
<el-drawer
v-model="showTrace"
size="100%"
:destroy-on-close="true"
:before-close="() => (showTrace = false)"
:append-to-body="true"
>
<Trace :data="traceOptions" />
</el-drawer>
</div>
<div class="center no-data" v-else>No Data</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { computed, ref } from "vue";
import copy from "@/utils/copy";
import { TextColors } from "@/views/dashboard/data";
import Trace from "@/views/dashboard/related/trace/Index.vue";
import { QueryOrders, Status } from "../data";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<{
[key: string]: { name: string; value: number; traceIds: string[] }[];
[key: string]: { name: string; value: number; id: string }[];
}>,
default: () => ({}),
},
config: {
type: Object as PropType<{ color: string }>,
type: Object as PropType<{ color: string; metrics: string[] }>,
default: () => ({ color: "purple" }),
},
intervalTime: { type: Array as PropType<string[]>, default: () => [] },
});
const { t } = useI18n();
const showTrace = ref<boolean>(false);
const traceOptions = ref<{ type: string; filters?: unknown }>({
type: "Trace",
});
const key = computed(() => Object.keys(props.data)[0] || "");
const available = computed(
() =>
@ -78,6 +100,21 @@ const maxValue = computed(() => {
function handleClick(i: string) {
copy(i);
}
function viewTrace(item: { name: string; id: string; value: unknown }) {
const filters = {
...item,
queryOrder: QueryOrders[1].value,
status: Status[2].value,
metricValue: [
{ label: props.config.metrics[0], data: item.value, value: item.name },
],
};
traceOptions.value = {
...traceOptions.value,
filters,
};
showTrace.value = true;
}
</script>
<style lang="scss" scoped>
.top-list {
@ -109,10 +146,6 @@ function handleClick(i: string) {
text-overflow: ellipsis;
}
.copy {
width: 30px;
}
.calls {
font-size: 12px;
padding: 0 5px;
@ -141,4 +174,22 @@ function handleClick(i: string) {
-webkit-box-pack: center;
-webkit-box-align: center;
}
.operation-icon {
color: #333;
}
.operation {
padding: 5px 0;
color: #333;
cursor: pointer;
position: relative;
text-align: center;
font-size: 12px;
&:hover {
color: #409eff;
background-color: #eee;
}
}
</style>

View File

@ -34,7 +34,7 @@ limitations under the License. -->
</el-button>
</div>
</h5>
<div class="mb-5 blue sm">
<div class="mb-5 blue">
<Selector
size="small"
:value="
@ -46,12 +46,7 @@ limitations under the License. -->
@change="changeTraceId"
class="trace-detail-ids"
/>
<Icon
size="sm"
class="icon grey link-hover cp ml-5"
iconName="review-list"
@click="handleClick"
/>
<Icon class="cp ml-5" iconName="copy" @click="handleClick" />
</div>
<div class="flex-h item">
<div>

View File

@ -95,17 +95,15 @@ limitations under the License. -->
import { ref, reactive, watch, onUnmounted } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { Option } from "@/types/app";
import { Status } from "../../data";
import { Option, DurationTime } from "@/types/app";
import { useTraceStore } from "@/store/modules/trace";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import ConditionTags from "@/views/components/ConditionTags.vue";
import { ElMessage } from "element-plus";
import { EntityType } from "../../data";
import { EntityType, QueryOrders, Status } from "../../data";
import { LayoutConfig } from "@/types/dashboard";
import { DurationTime } from "@/types/app";
/*global defineProps, Recordable */
const props = defineProps({
@ -115,28 +113,29 @@ const props = defineProps({
default: () => ({ graph: {} }),
},
});
const traceId = ref<string>(
(props.data.filters && props.data.filters.traceId) || ""
);
const filters = reactive<Recordable>(props.data.filters || {});
const traceId = ref<string>(filters.traceId || "");
const { t } = useI18n();
const appStore = useAppStoreWithOut();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const traceStore = useTraceStore();
const duration = ref<DurationTime>(
(props.data.filters && props.data.filters.duration) || appStore.durationTime
);
const duration = ref<DurationTime>(filters.duration || appStore.durationTime);
const minTraceDuration = ref<number>();
const maxTraceDuration = ref<number>();
const tagsList = ref<string[]>([]);
const tagsMap = ref<Option[]>([]);
const state = reactive<Recordable>({
status: { label: "All", value: "ALL" },
status: filters.status === "ERROR" ? Status[2] : Status[0],
instance: { value: "0", label: "All" },
endpoint: { value: "0", label: "All" },
service: { value: "", label: "" },
});
if (filters.queryOrder) {
traceStore.setTraceCondition({
queryOrder: filters.queryOrder,
});
}
if (props.needQuery) {
init();
}
@ -164,7 +163,7 @@ async function getServices() {
ElMessage.error(resp.errors);
return;
}
state.service = traceStore.services[0];
state.service = getCurrentNode(traceStore.services) || traceStore.services[0];
getEndpoints(state.service.id);
getInstances(state.service.id);
}
@ -175,7 +174,8 @@ async function getEndpoints(id?: string, keyword?: string) {
ElMessage.error(resp.errors);
return;
}
state.endpoint = traceStore.endpoints[0];
state.endpoint =
getCurrentNode(traceStore.endpoints) || traceStore.endpoints[0];
}
async function getInstances(id?: string) {
const resp = await traceStore.getInstances(id);
@ -183,9 +183,39 @@ async function getInstances(id?: string) {
ElMessage.error(resp.errors);
return;
}
state.instance = traceStore.instances[0];
state.instance =
getCurrentNode(traceStore.instances) || traceStore.instances[0];
}
function searchTraces() {
function getCurrentNode(arr: { id: string }[]) {
let item;
if (!props.data.filters) {
return item;
}
if (props.data.filters.id) {
item = arr.find((d: { id: string }) => d.id === props.data.filters?.id);
}
return item;
}
function setCondition() {
let param: any = {
traceState: state.status.value || "ALL",
tags: tagsMap.value.length ? tagsMap.value : undefined,
queryOrder: traceStore.conditions.queryOrder || QueryOrders[1].value,
queryDuration: duration.value,
minTraceDuration: Number(minTraceDuration.value),
maxTraceDuration: Number(maxTraceDuration.value),
traceId: traceId.value || undefined,
paging: { pageNum: 1, pageSize: 20 },
};
if (props.data.filters && props.data.filters.id) {
param = {
...param,
serviceId: selectorStore.currentService.id,
endpointId: state.endpoint.id || undefined,
serviceInstanceId: state.instance.id || undefined,
};
return param;
}
let endpoint = "",
instance = "";
if (dashboardStore.entity === EntityType[2].value) {
@ -194,21 +224,18 @@ function searchTraces() {
if (dashboardStore.entity === EntityType[3].value) {
instance = selectorStore.currentPod.id;
}
traceStore.setTraceCondition({
param = {
...param,
serviceId: selectorStore.currentService
? selectorStore.currentService.id
: state.service.id,
traceId: traceId.value || undefined,
endpointId: endpoint || state.endpoint.id || undefined,
serviceInstanceId: instance || state.instance.id || undefined,
traceState: state.status.value || "ALL",
queryDuration: duration.value,
minTraceDuration: Number(minTraceDuration.value),
maxTraceDuration: Number(maxTraceDuration.value),
queryOrder: traceStore.conditions.queryOrder || "BY_DURATION",
tags: tagsMap.value.length ? tagsMap.value : undefined,
paging: { pageNum: 1, pageSize: 20 },
});
};
return param;
}
function searchTraces() {
traceStore.setTraceCondition(setCondition());
queryTraces();
}
async function queryTraces() {
@ -263,17 +290,19 @@ watch(
}
}
);
// Event widget associate with trace widget
watch(
() => props.data.filters,
(newJson, oldJson) => {
if (props.data.filters) {
if (JSON.stringify(newJson) === JSON.stringify(oldJson)) {
return;
}
traceId.value = props.data.filters.traceId || "";
duration.value = props.data.filters.duration || appStore.durationTime;
init();
if (!props.data.filters) {
return;
}
if (JSON.stringify(newJson) === JSON.stringify(oldJson)) {
return;
}
traceId.value = props.data.filters.traceId || "";
duration.value = props.data.filters.duration || appStore.durationTime;
init();
}
);
</script>

View File

@ -0,0 +1,267 @@
<!-- 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="conditions flex-h">
<el-radio-group v-model="conditions" @change="changeCondition">
<el-radio-button
v-for="(item, index) in items"
:label="item.label"
:key="item.label + index"
border
>
{{ t(item.label) }}
</el-radio-button>
</el-radio-group>
<Selector
v-if="conditions === 'latency' && filters.latency.length > 1"
:value="filters.latency[0].value"
:options="filters.latency"
placeholder="Select a option"
@change="changeLatency"
class="ml-10"
/>
<el-popover trigger="hover" width="250" placement="bottom" effect="light">
<template #reference>
<div class="cp conditions-popup">
<Icon iconName="conditions" size="middle" />
</div>
</template>
<div>
<div class="title">{{ t("queryConditions") }}</div>
<div
v-for="key in Object.keys(FiltersKeys)"
:key="key"
v-show="traceStore.conditions[FiltersKeys[key]]"
>
<span v-if="key !== 'duration'">
{{ t(key) }}: {{ traceStore.conditions[FiltersKeys[key]] }}
</span>
</div>
</div>
</el-popover>
<el-popover trigger="hover" width="250" placement="bottom" effect="light">
<template #reference>
<div class="cp metric-value">
<Icon iconName="info_outline" size="middle" />
</div>
</template>
<div>
<div class="title">{{ t("metricValues") }}</div>
<div v-for="metric in filters.metricValue" :key="metric.value">
{{ metric.label }}: {{ metric.data }}
</div>
</div>
</el-popover>
</div>
<div class="flex-h">
<ConditionTags :type="'TRACE'" @update="updateTags" />
<div class="search-btn">
<el-button size="small" type="primary" @click="queryTraces">
{{ t("search") }}
</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onUnmounted } from "vue";
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { Option, DurationTime } from "@/types/app";
import { useTraceStore } from "@/store/modules/trace";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { useSelectorStore } from "@/store/modules/selectors";
import ConditionTags from "@/views/components/ConditionTags.vue";
import { ElMessage } from "element-plus";
import { EntityType, QueryOrders, Status } from "../../data";
import { LayoutConfig } from "@/types/dashboard";
const FiltersKeys: { [key: string]: string } = {
status: "traceState",
queryOrder: "queryOrder",
duration: "queryDuration",
minTraceDuration: "minTraceDuration",
maxTraceDuration: "maxTraceDuration",
};
/*global defineProps, Recordable */
const props = defineProps({
needQuery: { type: Boolean, default: true },
data: {
type: Object as PropType<LayoutConfig>,
default: () => ({ graph: {} }),
},
});
const { t } = useI18n();
const filters = reactive<Recordable>(props.data.filters || {});
const appStore = useAppStoreWithOut();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const traceStore = useTraceStore();
const tagsList = ref<string[]>([]);
const tagsMap = ref<Option[]>([]);
const duration = ref<DurationTime>(filters.duration || appStore.durationTime);
const state = reactive<Recordable>({
instance: "",
endpoint: "",
service: "",
});
const conditions = ref<string>("");
const items = ref<{ label: string; value: string }[]>([]);
const currentLatency = ref<number[]>(
filters.latency ? filters.latency[0].data : []
);
init();
async function init() {
for (const d of Object.keys(filters)) {
if (filters[d] && ["status", "queryOrder", "latency"].includes(d)) {
items.value.push({ label: d, value: FiltersKeys[d] });
}
}
conditions.value = (items.value[0] && items.value[0].label) || "";
if (!filters.id) {
state.service = selectorStore.currentService.id;
if (dashboardStore.entity === EntityType[2].value) {
state.instance = selectorStore.currentPod.id;
}
if (dashboardStore.entity === EntityType[3].value) {
state.endpoint = selectorStore.currentPod.id;
}
await queryTraces();
return;
}
if (dashboardStore.entity === EntityType[1].value) {
await getService();
}
if (dashboardStore.entity === EntityType[0].value) {
state.service = selectorStore.currentService.id;
await getInstance();
if (!state.instance) {
await getEndpoint();
}
}
await queryTraces();
}
function changeCondition() {
if (conditions.value === "latency") {
currentLatency.value = filters.latency ? filters.latency[0].data : [];
}
queryTraces();
}
function changeLatency(options: any[]) {
currentLatency.value = options[0].data;
queryTraces();
}
async function getService() {
const resp = await traceStore.getService(filters.id);
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
state.service = (resp.data.service && resp.data.service) || "";
}
async function getEndpoint() {
const resp = await traceStore.getEndpoint(filters.id);
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
state.endpoint = (resp.data.endpoint && resp.data.endpoint.id) || "";
}
async function getInstance() {
const resp = await traceStore.getInstance(filters.id);
if (resp.errors) {
ElMessage.error(resp.errors);
return;
}
state.instance = (resp.data.instance && resp.data.instance.id) || "";
}
function setCondition() {
let params: any = {
traceState: Status[0].value,
queryOrder: QueryOrders[0].value,
queryDuration: duration.value,
minTraceDuration: isNaN(currentLatency.value[0])
? undefined
: currentLatency.value[0] === currentLatency.value[1]
? currentLatency.value[0] - 10
: currentLatency.value[0],
maxTraceDuration:
isNaN(currentLatency.value[1]) || currentLatency.value[1] === Infinity
? undefined
: currentLatency.value[1],
tags: tagsMap.value.length ? tagsMap.value : undefined,
paging: { pageNum: 1, pageSize: 20 },
serviceId: state.service || undefined,
endpointId: state.endpoint || undefined,
serviceInstanceId: state.instance || undefined,
};
for (const k of items.value) {
if (k.label === conditions.value && FiltersKeys[k.label]) {
params[k.value] = filters[k.label];
}
}
if (!isNaN(params.minTraceDuration)) {
params.queryOrder = QueryOrders[1].value;
}
return params;
}
async function queryTraces() {
traceStore.setTraceCondition(setCondition());
const res = await traceStore.getTraces();
if (res && res.errors) {
ElMessage.error(res.errors);
}
}
function updateTags(data: { tagsMap: Array<Option>; tagsList: string[] }) {
tagsList.value = data.tagsList;
tagsMap.value = data.tagsMap;
}
onUnmounted(() => {
traceStore.resetState();
});
</script>
<style lang="scss" scoped>
.row {
margin-bottom: 5px;
position: relative;
}
.conditions {
margin-bottom: 10px;
}
.search-btn {
margin-top: 2px;
}
.metric-value {
padding: 0 5px;
line-height: 32px;
}
.conditions-popup {
padding-left: 10px;
line-height: 32px;
}
.title {
margin-bottom: 10px;
font-weight: bold;
}
</style>

View File

@ -0,0 +1,63 @@
<!-- 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="trace-wrapper flex-v">
<div class="header">
<Header :data="data" />
</div>
<div class="trace flex-h">
<TraceList />
<TraceDetail />
</div>
</div>
</template>
<script lang="ts" setup>
import { provide } from "vue";
import type { PropType } from "vue";
import Header from "./Header.vue";
import TraceList from "./TraceList.vue";
import TraceDetail from "./Detail.vue";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<any>,
default: () => ({ graph: {} }),
},
});
provide("options", props.data);
</script>
<style lang="scss" scoped>
.trace-wrapper {
width: 100%;
height: 100%;
font-size: 12px;
position: relative;
overflow: auto;
}
.header {
padding: 10px;
font-size: 12px;
border-bottom: 1px solid #dcdfe6;
min-width: 1200px;
}
.trace {
width: 100%;
overflow: auto;
min-width: 1200px;
}
</style>

View File

@ -58,8 +58,8 @@ limitations under the License. -->
<span class="b">{{ i.endpointNames[0] }}</span>
</div>
<div class="grey ell sm">
<span class="tag mr-10 sm">{{ i.duration }} ms</span
>{{ dateFormat(parseInt(i.start, 10)) }}
<span class="tag mr-10 sm"> {{ i.duration }} ms </span>
{{ dateFormat(parseInt(i.start, 10)) }}
</div>
</td>
</tr>