This is an automated email from the ASF dual-hosted git repository.
wusheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-booster-ui.git
The following commit(s) were added to refs/heads/main by this push:
new a834cdb2 feat: Implement Trace page (#500)
a834cdb2 is described below
commit a834cdb2eb9e065646e6ad41ff461356b4276d11
Author: Fine0830 <[email protected]>
AuthorDate: Mon Sep 29 17:36:31 2025 +0800
feat: Implement Trace page (#500)
---
src/assets/icons/link.svg | 16 ++++
src/hooks/useTheme.ts | 104 +++++++++++++++++++++
src/layout/Index.vue | 20 +++-
src/layout/components/NavBar.vue | 60 +-----------
src/locales/lang/en.ts | 1 +
src/locales/lang/es.ts | 1 +
src/locales/lang/zh.ts | 1 +
src/router/__tests__/index.spec.ts | 42 +++++++++
src/router/__tests__/trace.spec.ts | 55 +++++++++++
src/router/constants.ts | 2 +
src/router/index.ts | 2 +
src/router/{index.ts => trace.ts} | 55 +++++------
src/utils/__tests__/copy.spec.ts | 23 ++---
src/utils/copy.ts | 7 +-
src/utils/validateAndSanitizeUrl.ts | 10 +-
src/views/dashboard/Trace.vue | 42 +++++++++
.../related/trace/components/Table/TableItem.vue | 2 +-
.../trace/components/TraceQuery/TimelineTool.vue | 10 +-
.../trace/components/TraceQuery/TraceContent.vue | 26 +++++-
19 files changed, 367 insertions(+), 112 deletions(-)
diff --git a/src/assets/icons/link.svg b/src/assets/icons/link.svg
new file mode 100644
index 00000000..09c80683
--- /dev/null
+++ b/src/assets/icons/link.svg
@@ -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 class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg"><path d="M750.4 775.6h-119.6c-28
0-50.6-22.6-50.6-50.6s22.6-50.6 50.6-50.6h119.6c2.4 0 5 0 7.4-0.2 2.4-0.2 5-0.4
7.4-0.6 2.4-0.2 5-0.6 7.4-1 2.4-0.4 5-0.8 7.4-1.2 2.4-0.4 4.8-1 7.2-1.6 2.4-0.6
4.8-1.2 7.2-2 2.4-0.8 4.8-1.6 7-2.4 2.4-0.8 4.6-1.8 7-2.6 2.2-1 4.6-2
6.8-3s4.4-2.2 6.6-3.4c2.2-1.2 4.4-2.4 6.4-3.6 2.2-1.2 4.2-2.6 6.4-4 2-1.4
4.2-2.8 6-4.2 2-1.4 4-3 5.8-4.6 2-1.6 3.8-3.2 5.6-4.8 1.8-1.6 3. [...]
\ No newline at end of file
diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts
new file mode 100644
index 00000000..b3a750a6
--- /dev/null
+++ b/src/hooks/useTheme.ts
@@ -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,
+ };
+}
diff --git a/src/layout/Index.vue b/src/layout/Index.vue
index 902d95e4..ef8f27f9 100644
--- a/src/layout/Index.vue
+++ b/src/layout/Index.vue
@@ -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 {
diff --git a/src/layout/components/NavBar.vue b/src/layout/components/NavBar.vue
index 7dc6ab75..9b03affe 100644
--- a/src/layout/components/NavBar.vue
+++ b/src/layout/components/NavBar.vue
@@ -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) || {};
}
diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts
index 8e8da3b1..792a5ba3 100644
--- a/src/locales/lang/en.ts
+++ b/src/locales/lang/en.ts
@@ -412,5 +412,6 @@ const msg = {
totalSpans: "Total Spans",
spanName: "Span name",
parentId: "Parent ID",
+ shareTrace: "Share This Trace",
};
export default msg;
diff --git a/src/locales/lang/es.ts b/src/locales/lang/es.ts
index be08d7e0..156bdd43 100644
--- a/src/locales/lang/es.ts
+++ b/src/locales/lang/es.ts
@@ -412,5 +412,6 @@ const msg = {
totalSpans: "Total Lapso",
spanName: "Nombre de Lapso",
parentId: "ID Padre",
+ shareTrace: "Compartir Traza",
};
export default msg;
diff --git a/src/locales/lang/zh.ts b/src/locales/lang/zh.ts
index d0e1dc0e..c68a406d 100644
--- a/src/locales/lang/zh.ts
+++ b/src/locales/lang/zh.ts
@@ -410,5 +410,6 @@ const msg = {
totalSpans: "总跨度",
spanName: "跨度名称",
parentId: "父ID",
+ shareTrace: "分享Trace",
};
export default msg;
diff --git a/src/router/__tests__/index.spec.ts
b/src/router/__tests__/index.spec.ts
index 5cae65f2..6dc887c9 100644
--- a/src/router/__tests__/index.spec.ts
+++ b/src/router/__tests__/index.spec.ts
@@ -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", () => {
diff --git a/src/router/__tests__/trace.spec.ts
b/src/router/__tests__/trace.spec.ts
new file mode 100644
index 00000000..65c9c2af
--- /dev/null
+++ b/src/router/__tests__/trace.spec.ts
@@ -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);
+ });
+});
diff --git a/src/router/constants.ts b/src/router/constants.ts
index 84ab35b3..c822481c 100644
--- a/src/router/constants.ts
+++ b/src/router/constants.ts
@@ -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;
diff --git a/src/router/index.ts b/src/router/index.ts
index 523d97d0..dc4393a0 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -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,
];
/**
diff --git a/src/router/index.ts b/src/router/trace.ts
similarity index 51%
copy from src/router/index.ts
copy to src/router/trace.ts
index 523d97d0..5ff996ac 100644
--- a/src/router/index.ts
+++ b/src/router/trace.ts
@@ -15,38 +15,27 @@
* limitations under the License.
*/
import type { AppRouteRecordRaw } from "@/types/router";
-import { createRouter, createWebHistory } from "vue-router";
-import { applyGuards } from "./guards";
-import { routesDashboard } from "./dashboard";
-import { routesMarketplace } from "./marketplace";
-import { routesAlarm } from "./alarm";
-import routesLayers from "./layer";
-import { routesSettings } from "./settings";
-import { routesNotFound } from "./notFound";
+import { ROUTE_NAMES, ROUTE_PATHS, META_KEYS } from "./constants";
+import Layout from "@/layout/Index.vue";
+import Trace from "@/views/dashboard/Trace.vue";
-/**
- * Combine all route configurations
- */
-export const routes: AppRouteRecordRaw[] = [
- ...routesMarketplace,
- ...routesLayers,
- ...routesAlarm,
- ...routesDashboard,
- ...routesSettings,
- ...routesNotFound,
+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,
+ },
+ },
+ ],
+ },
];
-
-/**
- * Create router instance
- */
-const router = createRouter({
- history: createWebHistory(import.meta.env.BASE_URL),
- routes: routes as any,
-});
-
-/**
- * Apply navigation guards
- */
-applyGuards(router, routes);
-
-export default router;
diff --git a/src/utils/__tests__/copy.spec.ts b/src/utils/__tests__/copy.spec.ts
index d6864368..20dd9e46 100644
--- a/src/utils/__tests__/copy.spec.ts
+++ b/src/utils/__tests__/copy.spec.ts
@@ -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
diff --git a/src/utils/copy.ts b/src/utils/copy.ts
index d3a5c446..76bfe0cf 100644
--- a/src/utils/copy.ts
+++ b/src/utils/copy.ts
@@ -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",
diff --git a/src/utils/validateAndSanitizeUrl.ts
b/src/utils/validateAndSanitizeUrl.ts
index ab9edfa3..088e21cd 100644
--- a/src/utils/validateAndSanitizeUrl.ts
+++ b/src/utils/validateAndSanitizeUrl.ts
@@ -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: "",
diff --git a/src/views/dashboard/Trace.vue b/src/views/dashboard/Trace.vue
new file mode 100644
index 00000000..705e6875
--- /dev/null
+++ b/src/views/dashboard/Trace.vue
@@ -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>
diff --git a/src/views/dashboard/related/trace/components/Table/TableItem.vue
b/src/views/dashboard/related/trace/components/Table/TableItem.vue
index 12fe8d39..541bc8c0 100644
--- a/src/views/dashboard/related/trace/components/Table/TableItem.vue
+++ b/src/views/dashboard/related/trace/components/Table/TableItem.vue
@@ -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"
}}
diff --git
a/src/views/dashboard/related/trace/components/TraceQuery/TimelineTool.vue
b/src/views/dashboard/related/trace/components/TraceQuery/TimelineTool.vue
index 8db99b36..b967cee1 100644
--- a/src/views/dashboard/related/trace/components/TraceQuery/TimelineTool.vue
+++ b/src/views/dashboard/related/trace/components/TraceQuery/TimelineTool.vue
@@ -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");
diff --git
a/src/views/dashboard/related/trace/components/TraceQuery/TraceContent.vue
b/src/views/dashboard/related/trace/components/TraceQuery/TraceContent.vue
index f96d3d39..593a9b99 100644
--- a/src/views/dashboard/related/trace/components/TraceQuery/TraceContent.vue
+++ b/src/views/dashboard/related/trace/components/TraceQuery/TraceContent.vue
@@ -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>