mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2025-10-14 03:09:18 +00:00
feat: Implement Trace page (#500)
This commit is contained in:
16
src/assets/icons/link.svg
Normal file
16
src/assets/icons/link.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.5 KiB |
104
src/hooks/useTheme.ts
Normal file
104
src/hooks/useTheme.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 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 { ref, computed } from "vue";
|
||||
import { Themes } from "@/constants/data";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
|
||||
export function useTheme() {
|
||||
const appStore = useAppStoreWithOut();
|
||||
const theme = ref<boolean>(true);
|
||||
const themeSwitchRef = ref<HTMLElement>();
|
||||
|
||||
// Initialize theme from localStorage or system preference
|
||||
function initializeTheme() {
|
||||
const savedTheme = window.localStorage.getItem("theme-is-dark");
|
||||
let isDark = true; // default to dark theme
|
||||
|
||||
if (savedTheme === "false") {
|
||||
isDark = false;
|
||||
} else if (savedTheme === "") {
|
||||
// read the theme preference from system setting if there is no user setting
|
||||
isDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
}
|
||||
|
||||
theme.value = isDark;
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
// Apply theme to DOM and store
|
||||
function applyTheme() {
|
||||
const root = document.documentElement;
|
||||
|
||||
if (theme.value) {
|
||||
root.classList.add(Themes.Dark);
|
||||
root.classList.remove(Themes.Light);
|
||||
appStore.setTheme(Themes.Dark);
|
||||
} else {
|
||||
root.classList.add(Themes.Light);
|
||||
root.classList.remove(Themes.Dark);
|
||||
appStore.setTheme(Themes.Light);
|
||||
}
|
||||
|
||||
window.localStorage.setItem("theme-is-dark", String(theme.value));
|
||||
}
|
||||
|
||||
// Handle theme change with transition animation
|
||||
function handleChangeTheme() {
|
||||
const x = themeSwitchRef.value?.offsetLeft ?? 0;
|
||||
const y = themeSwitchRef.value?.offsetTop ?? 0;
|
||||
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
|
||||
|
||||
// compatibility handling
|
||||
if (!document.startViewTransition) {
|
||||
applyTheme();
|
||||
return;
|
||||
}
|
||||
|
||||
// api: https://developer.chrome.com/docs/web-platform/view-transitions
|
||||
const transition = document.startViewTransition(() => {
|
||||
applyTheme();
|
||||
});
|
||||
|
||||
transition.ready.then(() => {
|
||||
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`];
|
||||
document.documentElement.animate(
|
||||
{
|
||||
clipPath: !theme.value ? clipPath.reverse() : clipPath,
|
||||
},
|
||||
{
|
||||
duration: 500,
|
||||
easing: "ease-in",
|
||||
pseudoElement: !theme.value ? "::view-transition-old(root)" : "::view-transition-new(root)",
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Computed properties
|
||||
const isDark = computed(() => theme.value);
|
||||
const isLight = computed(() => !theme.value);
|
||||
|
||||
return {
|
||||
theme,
|
||||
themeSwitchRef,
|
||||
isDark,
|
||||
isLight,
|
||||
initializeTheme,
|
||||
applyTheme,
|
||||
handleChangeTheme,
|
||||
};
|
||||
}
|
@@ -14,15 +14,31 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License. -->
|
||||
<template>
|
||||
<div class="app-wrapper flex-h">
|
||||
<SideBar />
|
||||
<SideBar v-if="notTraceRoute" />
|
||||
<div class="main-container">
|
||||
<NavBar />
|
||||
<NavBar v-if="notTraceRoute" />
|
||||
<AppMain />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { AppMain, SideBar, NavBar } from "./components";
|
||||
import { useRoute } from "vue-router";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
|
||||
const route = useRoute();
|
||||
const { initializeTheme } = useTheme();
|
||||
|
||||
// Check if current route matches the trace route pattern
|
||||
const notTraceRoute = computed(() => {
|
||||
return !route.path.startsWith("/traces/");
|
||||
});
|
||||
|
||||
// Initialize theme to preserve theme when NavBar is hidden
|
||||
onMounted(() => {
|
||||
initializeTheme();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.app-wrapper {
|
||||
|
@@ -85,7 +85,6 @@ limitations under the License. -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Themes } from "@/constants/data";
|
||||
import router from "@/router";
|
||||
import { useAppStoreWithOut, InitializationDurationRow } from "@/store/modules/app";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
@@ -98,50 +97,26 @@ limitations under the License. -->
|
||||
import { ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
|
||||
const { t, te } = useI18n();
|
||||
const appStore = useAppStoreWithOut();
|
||||
const dashboardStore = useDashboardStore();
|
||||
const traceStore = useTraceStore();
|
||||
const route = useRoute();
|
||||
const { theme, themeSwitchRef, initializeTheme, handleChangeTheme } = useTheme();
|
||||
const pathNames = ref<{ path?: string; name: string; selected: boolean }[][]>([]);
|
||||
const showTimeRangeTips = ref<boolean>(false);
|
||||
const pageTitle = ref<string>("");
|
||||
const theme = ref<boolean>(true);
|
||||
const themeSwitchRef = ref<HTMLElement>();
|
||||
const coldStage = ref<boolean>(false);
|
||||
|
||||
const savedTheme = window.localStorage.getItem("theme-is-dark");
|
||||
if (savedTheme === "false") {
|
||||
theme.value = false;
|
||||
}
|
||||
if (savedTheme === "") {
|
||||
// read the theme preference from system setting if there is no user setting
|
||||
theme.value = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
}
|
||||
|
||||
changeTheme();
|
||||
initializeTheme();
|
||||
resetDuration();
|
||||
getVersion();
|
||||
getNavPaths();
|
||||
setTTL();
|
||||
traceStore.getHasQueryTracesV2Support();
|
||||
|
||||
function changeTheme() {
|
||||
const root = document.documentElement;
|
||||
|
||||
if (theme.value) {
|
||||
root.classList.add(Themes.Dark);
|
||||
root.classList.remove(Themes.Light);
|
||||
appStore.setTheme(Themes.Dark);
|
||||
} else {
|
||||
root.classList.add(Themes.Light);
|
||||
root.classList.remove(Themes.Dark);
|
||||
appStore.setTheme(Themes.Light);
|
||||
}
|
||||
window.localStorage.setItem("theme-is-dark", String(theme.value));
|
||||
}
|
||||
|
||||
function changeDataMode() {
|
||||
appStore.setColdStageMode(coldStage.value);
|
||||
if (coldStage.value) {
|
||||
@@ -160,35 +135,6 @@ limitations under the License. -->
|
||||
appStore.setDuration(InitializationDurationRow);
|
||||
}
|
||||
|
||||
function handleChangeTheme() {
|
||||
const x = themeSwitchRef.value?.offsetLeft ?? 0;
|
||||
const y = themeSwitchRef.value?.offsetTop ?? 0;
|
||||
const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));
|
||||
// compatibility handling
|
||||
if (!document.startViewTransition) {
|
||||
changeTheme();
|
||||
return;
|
||||
}
|
||||
// api: https://developer.chrome.com/docs/web-platform/view-transitions
|
||||
const transition = document.startViewTransition(() => {
|
||||
changeTheme();
|
||||
});
|
||||
|
||||
transition.ready.then(() => {
|
||||
const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`];
|
||||
document.documentElement.animate(
|
||||
{
|
||||
clipPath: !theme.value ? clipPath.reverse() : clipPath,
|
||||
},
|
||||
{
|
||||
duration: 500,
|
||||
easing: "ease-in",
|
||||
pseudoElement: !theme.value ? "::view-transition-old(root)" : "::view-transition-new(root)",
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getName(list: any[]) {
|
||||
return list.find((d: any) => d.selected) || {};
|
||||
}
|
||||
|
@@ -412,5 +412,6 @@ const msg = {
|
||||
totalSpans: "Total Spans",
|
||||
spanName: "Span name",
|
||||
parentId: "Parent ID",
|
||||
shareTrace: "Share This Trace",
|
||||
};
|
||||
export default msg;
|
||||
|
@@ -412,5 +412,6 @@ const msg = {
|
||||
totalSpans: "Total Lapso",
|
||||
spanName: "Nombre de Lapso",
|
||||
parentId: "ID Padre",
|
||||
shareTrace: "Compartir Traza",
|
||||
};
|
||||
export default msg;
|
||||
|
@@ -410,5 +410,6 @@ const msg = {
|
||||
totalSpans: "总跨度",
|
||||
spanName: "跨度名称",
|
||||
parentId: "父ID",
|
||||
shareTrace: "分享Trace",
|
||||
};
|
||||
export default msg;
|
||||
|
@@ -121,6 +121,39 @@ vi.mock("../notFound", () => ({
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock("../trace", () => ({
|
||||
routesTrace: [
|
||||
{
|
||||
name: "Trace",
|
||||
path: "",
|
||||
meta: {
|
||||
title: "Trace",
|
||||
i18nKey: "trace",
|
||||
icon: "timeline",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
notShow: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: "ViewTrace",
|
||||
path: "/traces/:traceId",
|
||||
meta: {
|
||||
title: "Trace View",
|
||||
i18nKey: "traceView",
|
||||
icon: "timeline",
|
||||
hasGroup: false,
|
||||
activate: true,
|
||||
breadcrumb: true,
|
||||
notShow: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// Mock guards
|
||||
vi.mock("../guards", () => ({
|
||||
applyGuards: vi.fn(),
|
||||
@@ -144,6 +177,7 @@ describe("Router Index - Route Structure", () => {
|
||||
expect.objectContaining({ name: "Dashboard" }),
|
||||
expect.objectContaining({ name: "Settings" }),
|
||||
expect.objectContaining({ name: "NotFound" }),
|
||||
expect.objectContaining({ name: "Trace" }),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -186,6 +220,14 @@ describe("Router Index - Route Structure", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should include trace routes", () => {
|
||||
expect(routes).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "Trace",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Route Export", () => {
|
||||
|
55
src/router/__tests__/trace.spec.ts
Normal file
55
src/router/__tests__/trace.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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 { describe, it, expect, vi } from "vitest";
|
||||
import { ROUTE_NAMES, ROUTE_PATHS, META_KEYS } from "../constants";
|
||||
|
||||
// Mock Vue SFC imports used by the route module
|
||||
vi.mock("@/layout/Index.vue", () => ({ default: {} }));
|
||||
vi.mock("@/views/dashboard/Trace.vue", () => ({ default: {} }));
|
||||
|
||||
// Import after mocks
|
||||
import { routesTrace } from "../trace";
|
||||
|
||||
describe("Trace Routes", () => {
|
||||
it("should export trace routes array", () => {
|
||||
expect(routesTrace).toBeDefined();
|
||||
expect(Array.isArray(routesTrace)).toBe(true);
|
||||
expect(routesTrace).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should have correct root trace route structure", () => {
|
||||
const rootRoute = routesTrace[0];
|
||||
|
||||
expect(rootRoute.name).toBe(ROUTE_NAMES.TRACE);
|
||||
expect(rootRoute.path).toBe("");
|
||||
expect(rootRoute.meta?.[META_KEYS.NOT_SHOW]).toBe(false);
|
||||
|
||||
expect(rootRoute.children).toBeDefined();
|
||||
expect(rootRoute.children).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should have child view trace route with correct path and meta", () => {
|
||||
const rootRoute = routesTrace[0];
|
||||
const childRoute = rootRoute.children?.[0];
|
||||
|
||||
expect(childRoute).toBeDefined();
|
||||
expect(childRoute?.name).toBe("ViewTrace");
|
||||
expect(childRoute?.path).toBe(ROUTE_PATHS.TRACE);
|
||||
expect(childRoute?.meta?.[META_KEYS.NOT_SHOW]).toBe(false);
|
||||
});
|
||||
});
|
@@ -23,6 +23,7 @@ export const ROUTE_NAMES = {
|
||||
SETTINGS: "Settings",
|
||||
NOT_FOUND: "NotFound",
|
||||
LAYER: "Layer",
|
||||
TRACE: "Trace",
|
||||
} as const;
|
||||
|
||||
// Route Paths
|
||||
@@ -39,6 +40,7 @@ export const ROUTE_PATHS = {
|
||||
},
|
||||
ALARM: "/alerting",
|
||||
SETTINGS: "/settings",
|
||||
TRACE: "/traces/:traceId",
|
||||
NOT_FOUND: "/:pathMatch(.*)*",
|
||||
} as const;
|
||||
|
||||
|
@@ -23,6 +23,7 @@ import { routesAlarm } from "./alarm";
|
||||
import routesLayers from "./layer";
|
||||
import { routesSettings } from "./settings";
|
||||
import { routesNotFound } from "./notFound";
|
||||
import { routesTrace } from "./trace";
|
||||
|
||||
/**
|
||||
* Combine all route configurations
|
||||
@@ -34,6 +35,7 @@ export const routes: AppRouteRecordRaw[] = [
|
||||
...routesDashboard,
|
||||
...routesSettings,
|
||||
...routesNotFound,
|
||||
...routesTrace,
|
||||
];
|
||||
|
||||
/**
|
||||
|
41
src/router/trace.ts
Normal file
41
src/router/trace.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 type { AppRouteRecordRaw } from "@/types/router";
|
||||
import { ROUTE_NAMES, ROUTE_PATHS, META_KEYS } from "./constants";
|
||||
import Layout from "@/layout/Index.vue";
|
||||
import Trace from "@/views/dashboard/Trace.vue";
|
||||
|
||||
export const routesTrace: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: "",
|
||||
name: ROUTE_NAMES.TRACE,
|
||||
meta: {
|
||||
[META_KEYS.NOT_SHOW]: false,
|
||||
},
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: ROUTE_PATHS.TRACE,
|
||||
name: "ViewTrace",
|
||||
component: Trace,
|
||||
meta: {
|
||||
[META_KEYS.NOT_SHOW]: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
@@ -72,7 +72,7 @@ describe("copy utility function", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error notification for HTTP protocol", () => {
|
||||
it("should show error notification for HTTP protocol in production", () => {
|
||||
const testText = "test text to copy";
|
||||
|
||||
// Set protocol to HTTP
|
||||
@@ -81,7 +81,10 @@ describe("copy utility function", () => {
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const originalEnv = process.env.NODE_ENV;
|
||||
process.env.NODE_ENV = "production";
|
||||
copy(testText);
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
|
||||
expect(ElNotification).toHaveBeenCalledWith({
|
||||
title: "Warning",
|
||||
@@ -127,20 +130,15 @@ describe("copy utility function", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle empty string", async () => {
|
||||
it("should do nothing when text is empty", async () => {
|
||||
const testText = "";
|
||||
mockClipboard.writeText.mockResolvedValue(undefined);
|
||||
|
||||
copy(testText);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(mockClipboard.writeText).toHaveBeenCalledWith("");
|
||||
expect(ElNotification).toHaveBeenCalledWith({
|
||||
title: "Success",
|
||||
message: "Copied",
|
||||
type: "success",
|
||||
});
|
||||
expect(mockClipboard.writeText).not.toHaveBeenCalled();
|
||||
expect(ElNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle long text", async () => {
|
||||
@@ -205,7 +203,7 @@ describe("copy utility function", () => {
|
||||
expect(ElNotification).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("should handle HTTP protocol and clipboard not available", () => {
|
||||
it("should handle HTTP protocol and clipboard not available in production", () => {
|
||||
const testText = "test text";
|
||||
|
||||
// Set protocol to HTTP
|
||||
@@ -214,7 +212,10 @@ describe("copy utility function", () => {
|
||||
writable: true,
|
||||
});
|
||||
|
||||
const originalEnv = process.env.NODE_ENV;
|
||||
process.env.NODE_ENV = "production";
|
||||
copy(testText);
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
|
||||
// Should show HTTP error, not clipboard error
|
||||
expect(ElNotification).toHaveBeenCalledWith({
|
||||
@@ -224,7 +225,7 @@ describe("copy utility function", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle file protocol", () => {
|
||||
it("should handle file protocol when clipboard is unavailable", () => {
|
||||
const testText = "test text";
|
||||
|
||||
// Set protocol to file and ensure clipboard is not available
|
||||
|
@@ -18,7 +18,12 @@
|
||||
import { ElNotification } from "element-plus";
|
||||
|
||||
export default (text: string): void => {
|
||||
if (location.protocol === "http:") {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
// Clipboard functionality is restricted in production HTTP environments for security reasons.
|
||||
// In development, clipboard is allowed even over HTTP to ease testing.
|
||||
if (process.env.NODE_ENV === "production" && location.protocol === "http:") {
|
||||
ElNotification({
|
||||
title: "Warning",
|
||||
message: "Clipboard is not supported in HTTP environments",
|
||||
|
@@ -16,12 +16,15 @@
|
||||
*/
|
||||
|
||||
// URL validation function to prevent XSS
|
||||
export function validateAndSanitizeUrl(inputUrl: string): { isValid: boolean; sanitizedUrl: string; error: string } {
|
||||
if (!inputUrl.trim()) {
|
||||
export function validateAndSanitizeUrl(url: string): { isValid: boolean; sanitizedUrl: string; error: string } {
|
||||
if (!url.trim()) {
|
||||
return { isValid: true, sanitizedUrl: "", error: "" };
|
||||
}
|
||||
|
||||
try {
|
||||
let inputUrl = url;
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
inputUrl = `${location.origin}${url}`;
|
||||
}
|
||||
// Create URL object to validate the URL format
|
||||
const urlObj = new URL(inputUrl);
|
||||
|
||||
@@ -55,6 +58,7 @@ export function validateAndSanitizeUrl(inputUrl: string): { isValid: boolean; sa
|
||||
error: "",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
isValid: false,
|
||||
sanitizedUrl: "",
|
||||
|
42
src/views/dashboard/Trace.vue
Normal file
42
src/views/dashboard/Trace.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<!-- 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 style="padding: 20px">
|
||||
<TraceContent v-if="traceStore.currentTrace" :trace="traceStore.currentTrace" />
|
||||
<div style="text-align: center; padding: 20px" v-if="!traceStore.loading && !traceStore.currentTrace"
|
||||
>No trace found</div
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRoute } from "vue-router";
|
||||
import { computed, onMounted, provide } from "vue";
|
||||
import { useTraceStore } from "@/store/modules/trace";
|
||||
import TraceContent from "@/views/dashboard/related/trace/components/TraceQuery/TraceContent.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const traceStore = useTraceStore();
|
||||
const traceId = computed(() => route.params.traceId as string);
|
||||
provide("options", {});
|
||||
onMounted(() => {
|
||||
if (traceId.value) {
|
||||
traceStore.setTraceCondition({
|
||||
traceId: traceId.value,
|
||||
});
|
||||
traceStore.fetchV2Traces();
|
||||
}
|
||||
});
|
||||
</script>
|
@@ -100,7 +100,7 @@ limitations under the License. -->
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="start-time">
|
||||
{{ dateFormat(data.startTime) }}
|
||||
{{ data.startTime ? dateFormat(data.startTime) : "" }}
|
||||
</div>
|
||||
<div class="exec-ms">
|
||||
{{ data.endTime - data.startTime ? data.endTime - data.startTime : "0" }}
|
||||
|
@@ -16,7 +16,7 @@ limitations under the License. -->
|
||||
<div class="timeline-tool flex-h">
|
||||
<div class="flex-h trace-type item">
|
||||
<el-radio-group v-model="spansGraphType" size="small">
|
||||
<el-radio-button v-for="option in GraphTypeOptions" :key="option.value" :value="option.value">
|
||||
<el-radio-button v-for="option in reorderedOptions" :key="option.value" :value="option.value">
|
||||
<Icon :iconName="option.icon" />
|
||||
{{ t(option.label) }}
|
||||
</el-radio-button>
|
||||
@@ -39,7 +39,13 @@ limitations under the License. -->
|
||||
(e: "updateSpansGraphType", value: string): void;
|
||||
}>();
|
||||
const { t } = useI18n();
|
||||
const spansGraphType = ref<string>(GraphTypeOptions[2].value);
|
||||
const reorderedOptions = [
|
||||
GraphTypeOptions[2], // Table
|
||||
GraphTypeOptions[0], // List
|
||||
GraphTypeOptions[1], // Tree
|
||||
GraphTypeOptions[3], // Statistics
|
||||
];
|
||||
const spansGraphType = ref<string>(reorderedOptions[0].value);
|
||||
|
||||
function onToggleMinTimeline() {
|
||||
emit("toggleMinTimeline");
|
||||
|
@@ -17,7 +17,13 @@ limitations under the License. -->
|
||||
<div class="trace-info">
|
||||
<div class="flex-h" style="justify-content: space-between">
|
||||
<h3>{{ trace.label }}</h3>
|
||||
<div>
|
||||
<div class="flex-h">
|
||||
<div class="mr-5 cp">
|
||||
<el-button size="small" @click="viewTrace">
|
||||
{{ t("shareTrace") }}
|
||||
<Icon class="ml-5" size="small" iconName="link" />
|
||||
</el-button>
|
||||
</div>
|
||||
<el-dropdown @command="handleDownload" trigger="click">
|
||||
<el-button size="small">
|
||||
{{ t("download") }}
|
||||
@@ -44,9 +50,12 @@ limitations under the License. -->
|
||||
<span class="grey mr-5">{{ t("totalSpans") }}</span>
|
||||
<span class="value">{{ trace.spans?.length || 0 }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="trace-id-container flex-h" style="align-items: center">
|
||||
<span class="grey mr-5">{{ t("traceID") }}</span>
|
||||
<span class="value">{{ trace.traceId }}</span>
|
||||
<span class="value ml-5 cp" @click="handleCopyTraceId">
|
||||
<Icon size="middle" iconName="copy" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,6 +100,8 @@ limitations under the License. -->
|
||||
import graphs from "../VisGraph/index";
|
||||
import { WidgetType } from "@/views/dashboard/data";
|
||||
import { GraphTypeOptions } from "../VisGraph/constant";
|
||||
import copy from "@/utils/copy";
|
||||
import router from "@/router";
|
||||
|
||||
interface Props {
|
||||
trace: Trace;
|
||||
@@ -148,6 +159,17 @@ limitations under the License. -->
|
||||
ElMessage.error("Failed to download file");
|
||||
}
|
||||
}
|
||||
function viewTrace() {
|
||||
if (!traceStore.currentTrace?.traceId) return;
|
||||
const traceUrl = `/traces/${traceStore.currentTrace.traceId}`;
|
||||
const routeUrl = router.resolve({ path: traceUrl });
|
||||
|
||||
window.open(routeUrl.href, "_blank");
|
||||
}
|
||||
function handleCopyTraceId() {
|
||||
if (!traceStore.currentTrace?.traceId) return;
|
||||
copy(traceStore.currentTrace?.traceId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
Reference in New Issue
Block a user