test: implement unit tests for hooks and refactor some types (#493)

This commit is contained in:
Fine0830
2025-08-21 12:09:32 +07:00
committed by GitHub
parent a8c5ec8dd2
commit 1b6f011f0e
25 changed files with 3140 additions and 285 deletions

View File

@@ -24,7 +24,7 @@ limitations under the License. -->
</el-tooltip>
</div>
</div>
<div class="widget-chart" :style="{ height: config.height - 60 + 'px' }">
<div class="widget-chart" :style="{ height: (config.height || 0) - 60 + 'px' }">
<component
:is="graph.type"
:intervalTime="appStoreWithOut.intervalTime"
@@ -55,10 +55,12 @@ limitations under the License. -->
import { useRoute } from "vue-router";
import { useSelectorStore } from "@/store/modules/selectors";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useDashboardQueryProcessor } from "@/hooks/useExpressionsProcessor";
import { useDashboardQueryProcessor, DashboardWidgetConfig } from "@/hooks/useExpressionsProcessor";
import graphs from "./graphs";
import { EntityType } from "./data";
import timeFormat from "@/utils/timeFormat";
import { LayoutConfig } from "@/types/dashboard";
import { ExpressionsSourceResult } from "@/hooks/useExpressionsProcessor";
export default defineComponent({
name: "WidgetPage",
@@ -70,9 +72,11 @@ limitations under the License. -->
const appStoreWithOut = useAppStoreWithOut();
const selectorStore = useSelectorStore();
const route = useRoute();
const config = computed<any>(() => JSON.parse(decodeURIComponent(route.params.config as string) as string));
const config = computed<LayoutConfig>(() =>
JSON.parse(decodeURIComponent(route.params.config as string) as string),
);
const graph = computed(() => config.value.graph || {});
const source = ref<unknown>({});
const source = ref<ExpressionsSourceResult | {}>({});
const loading = ref<boolean>(false);
const dashboardStore = useDashboardStore();
const title = computed(() => (config.value.widget && config.value.widget.title) || "");
@@ -86,7 +90,7 @@ limitations under the License. -->
const { auto, autoPeriod } = config.value;
if (auto) {
await setDuration();
appStoreWithOut.setReloadTimer(setInterval(setDuration, autoPeriod * 1000));
appStoreWithOut.setReloadTimer(setInterval(setDuration, (autoPeriod ?? 0) * 1000));
} else {
const duration = JSON.parse(route.params.duration as string);
appStoreWithOut.setDuration(duration);
@@ -95,7 +99,7 @@ limitations under the License. -->
await queryMetrics();
}
async function setDuration() {
const dates: Date[] = [new Date(new Date().getTime() - config.value.auto), new Date()];
const dates: Date[] = [new Date(new Date().getTime() - (config.value.auto ?? 0)), new Date()];
appStoreWithOut.setDuration(timeFormat(dates));
}
@@ -130,17 +134,17 @@ limitations under the License. -->
}
async function queryMetrics() {
loading.value = true;
const metrics: { [key: string]: { source: { [key: string]: unknown }; typesOfMQE: string[] } } =
await useDashboardQueryProcessor([
{
metrics: config.value.expressions || [],
metricConfig: config.value.metricConfig || [],
subExpressions: config.value.subExpressions || [],
id: config.value.i,
},
]);
const params = metrics[config.value.i];
loading.value = false;
const metrics = await useDashboardQueryProcessor([
{
metrics: config.value.expressions || [],
metricConfig: config.value.metricConfig || [],
subExpressions: (config.value.subExpressions || []) as string[],
id: config.value.i,
},
] as DashboardWidgetConfig[]);
const params: ExpressionsSourceResult = (metrics as Record<string, ExpressionsSourceResult>)[
config.value.i as string
];
source.value = params.source || {};
typesOfMQE.value = params.typesOfMQE;
}

View File

@@ -165,7 +165,7 @@ limitations under the License. -->
{ metricConfig: metricConfig.value || [], expressions, subExpressions },
EntityType[2].value,
);
currentEndpoints.value = params.data;
currentEndpoints.value = params.data as Endpoint[];
colMetrics.value = params.names;
colSubMetrics.value = params.subNames;
metricConfig.value = params.metricConfigArr;

View File

@@ -91,6 +91,7 @@ limitations under the License. -->
import getDashboard from "@/hooks/useDashboardsSession";
import type { MetricConfigOpt } from "@/types/dashboard";
import ColumnGraph from "./components/ColumnGraph.vue";
import type { PodWithMetrics } from "@/hooks/useExpressionsProcessor";
/*global defineProps */
const props = defineProps({
@@ -176,11 +177,11 @@ limitations under the License. -->
if (expressions.length && expressions[0]) {
const params = await useExpressionsQueryPodsMetrics(
currentInstances,
currentInstances as PodWithMetrics[],
{ metricConfig: metricConfig.value || [], expressions, subExpressions },
EntityType[3].value,
);
instances.value = params.data;
instances.value = params.data as Instance[];
colMetrics.value = params.names;
colSubMetrics.value = params.subNames;
typesOfMQE.value = params.metricTypesArr;

View File

@@ -78,14 +78,14 @@ limitations under the License. -->
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import type { Service } from "@/types/selector";
import { useExpressionsQueryPodsMetrics } from "@/hooks/useExpressionsProcessor";
import { useExpressionsQueryPodsMetrics, PodWithMetrics } from "@/hooks/useExpressionsProcessor";
import { EntityType } from "../data";
import router from "@/router";
import getDashboard from "@/hooks/useDashboardsSession";
import type { MetricConfigOpt } from "@/types/dashboard";
import ColumnGraph from "./components/ColumnGraph.vue";
interface ServiceWithGroup extends Service {
export interface ServiceWithGroup extends Service {
merge: boolean;
group: string;
}
@@ -219,11 +219,11 @@ limitations under the License. -->
if (expressions.length && expressions[0]) {
const params = await useExpressionsQueryPodsMetrics(
currentServices,
currentServices as PodWithMetrics[],
{ metricConfig: metricConfig.value || [], expressions, subExpressions },
EntityType[0].value,
);
services.value = params.data;
services.value = params.data as ServiceWithGroup[];
colMetrics.value = params.names;
colSubMetrics.value = params.subNames;
metricConfig.value = params.metricConfigArr;

View File

@@ -19,15 +19,15 @@ limitations under the License. -->
v-for="(item, index) in columns"
:key="index"
:class="item.label"
@click="selectLog(item.label, data[item.label])"
@click="selectLog(item.label, getDataValue(item.label))"
>
<span v-if="item.label === 'tags'" :class="level.toLowerCase()"> > </span>
<span class="blue" v-else-if="item.label === 'traceId'">
<el-tooltip content="Trace Link" v-if="!noLink && data[item.label]">
<el-tooltip content="Trace Link" v-if="!noLink && getDataValue(item.label)">
<Icon iconName="merge" />
</el-tooltip>
</span>
<span v-else v-html="highlightKeywords(data[item.label])"></span>
<span v-else v-html="highlightKeywords(getDataValue(item.label))"></span>
</div>
</div>
</template>
@@ -40,10 +40,11 @@ limitations under the License. -->
import type { LayoutConfig, DashboardItem } from "@/types/dashboard";
import { WidgetType } from "@/views/dashboard/data";
import { useLogStore } from "@/store/modules/log";
import type { LogItem } from "@/types/log";
/*global defineProps, defineEmits */
const props = defineProps({
data: { type: Object as any, default: () => ({}) },
data: { type: Object as PropType<LogItem>, default: () => ({}) },
noLink: { type: Boolean, default: true },
config: { type: Object as PropType<LayoutConfig>, default: () => ({}) },
});
@@ -64,6 +65,10 @@ limitations under the License. -->
return `${content}`.replace(regex, (match) => `<span style="color: red">${match}</span>`);
};
function getDataValue(label: string) {
return props.data[label as keyof LogItem] as string;
}
function selectLog(label: string, value: string) {
if (label === "traceId") {
if (!value) {

View File

@@ -62,20 +62,25 @@ export function layout(levels: Node[][], calls: Call[], radius: number) {
export function computeCallPos(calls: Call[], radius: number) {
for (const [index, call] of calls.entries()) {
const centrePoints = [call.sourceObj.x, call.sourceObj.y, call.targetObj.x, call.targetObj.y];
const centrePoints = [
call.sourceObj?.x || 0,
call.sourceObj?.y || 0,
call.targetObj?.x || 0,
call.targetObj?.y || 0,
];
for (const [idx, link] of calls.entries()) {
if (
index < idx &&
call.id !== link.id &&
call.sourceObj.x === link.targetObj.x &&
call.sourceObj.y === link.targetObj.y &&
call.targetObj.x === link.sourceObj.x &&
call.targetObj.y === link.sourceObj.y
call.sourceObj?.x === link.targetObj?.x &&
call.sourceObj?.y === link.targetObj?.y &&
call.targetObj?.x === link.sourceObj?.x &&
call.targetObj?.y === link.sourceObj?.y
) {
if (call.targetObj.y === call.sourceObj.y) {
if (call.targetObj?.y === call.sourceObj?.y) {
centrePoints[1] = centrePoints[1] - 8;
centrePoints[3] = centrePoints[3] - 8;
} else if (call.targetObj.x === call.sourceObj.x) {
} else if (call.targetObj?.x === call.sourceObj?.x) {
centrePoints[0] = centrePoints[0] - 8;
centrePoints[2] = centrePoints[2] - 8;
} else {
@@ -127,14 +132,14 @@ function findMostFrequent(arr: Call[]) {
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
count[item.sourceObj.id] = (count[item.sourceObj.id] || 0) + 1;
if (count[item.sourceObj.id] > maxCount) {
maxCount = count[item.sourceObj.id];
count[item.sourceObj?.id || ""] = (count[item.sourceObj?.id || ""] || 0) + 1;
if (count[item.sourceObj?.id || ""] > maxCount) {
maxCount = count[item.sourceObj?.id || ""];
maxItem = item.sourceObj;
}
count[item.targetObj.id] = (count[item.targetObj.id] || 0) + 1;
if (count[item.targetObj.id] > maxCount) {
maxCount = count[item.targetObj.id];
count[item.targetObj?.id || ""] = (count[item.targetObj?.id || ""] || 0) + 1;
if (count[item.targetObj?.id || ""] > maxCount) {
maxCount = count[item.targetObj?.id || ""];
maxItem = item.targetObj;
}
}
@@ -156,7 +161,7 @@ export function computeLevels(calls: Call[], nodeList: Node[], arr: Node[][]) {
const index = nodes.findIndex((n: Node) => n.type === "USER");
let key = index;
if (index < 0) {
key = nodes.findIndex((n: Node) => n.id === node.id);
key = nodes.findIndex((n: Node) => n.id === node?.id);
}
levels.push([nodes[key]]);
nodes = nodes.filter((_: unknown, index: number) => index !== key);

View File

@@ -109,7 +109,7 @@ limitations under the License. -->
return ` <div class="mb-5"><span class="grey">${opt.label || m}: </span>${metric?.value} ${opt.unit || ""}</div>`;
});
const html = [
`<div>${data.sourceObj.serviceName} -> ${data.targetObj.serviceName}</div>`,
`<div>${data.sourceObj?.serviceName} -> ${data.targetObj?.serviceName}</div>`,
...htmlServer,
...htmlClient,
].join(" ");

View File

@@ -386,7 +386,10 @@ limitations under the License. -->
}
function handleLinkClick(event: MouseEvent, d: Call) {
event.stopPropagation();
if (!d.sourceObj.layers.includes(dashboardStore.layerId) || !d.targetObj.layers.includes(dashboardStore.layerId)) {
if (
!d.sourceObj?.layers?.includes(dashboardStore.layerId) ||
!d.targetObj?.layers?.includes(dashboardStore.layerId)
) {
return;
}
topologyStore.setNode(null);
@@ -406,7 +409,10 @@ limitations under the License. -->
return;
}
dashboardStore.setEntity(dashboard.entity);
const path = `/dashboard/related/${dashboard.layer}/${e}Relation/${d.sourceObj.id}/${d.targetObj.id}/${dashboard.name}`;
if (!d.sourceObj || !d.targetObj) {
return;
}
const path = `/dashboard/related/${dashboard.layer}/${e}Relation/${d.sourceObj?.id}/${d.targetObj?.id}/${dashboard.name}`;
const routeUrl = router.resolve({ path });
window.open(routeUrl.href, "_blank");
dashboardStore.setEntity(origin);

View File

@@ -49,12 +49,12 @@ limitations under the License. -->
<span
v-if="
[FiltersKeys.minTraceDuration, FiltersKeys.maxTraceDuration].includes(key) &&
!isNaN(traceStore.conditions[FiltersKeys[key]])
!isNaN(getConditionValue(key) as number)
"
>
{{ t(key) }}: {{ traceStore.conditions[FiltersKeys[key]] }}
{{ t(key) }}: {{ getConditionValue(key) }}
</span>
<span v-else-if="key !== 'duration'"> {{ t(key) }}: {{ traceStore.conditions[FiltersKeys[key]] }} </span>
<span v-else-if="key !== 'duration'"> {{ t(key) }}: {{ getConditionValue(key) }} </span>
</div>
</div>
</el-popover>
@@ -101,6 +101,16 @@ limitations under the License. -->
minTraceDuration: "minTraceDuration",
maxTraceDuration: "maxTraceDuration",
};
// Type-safe function to get condition value
const getConditionValue = (key: string): string | number | undefined => {
const conditionKey = FiltersKeys[key];
if (!conditionKey) return undefined;
// Type assertion for dynamic properties that are added at runtime
return (traceStore.conditions as Recordable)[conditionKey];
};
/*global defineProps, Recordable */
const props = defineProps({
needQuery: { type: Boolean, default: true },