refactor: optimize the router system and implement unit tests for router (#495)

This commit is contained in:
Fine0830
2025-08-28 21:11:46 +08:00
committed by GitHub
parent f069c8a081
commit a7972af3b4
20 changed files with 2327 additions and 170 deletions

View File

@@ -22,6 +22,7 @@
"test:hooks": "vitest --environment jsdom src/hooks/**/*.spec.ts", "test:hooks": "vitest --environment jsdom src/hooks/**/*.spec.ts",
"test:stores": "vitest --environment jsdom src/store/**/*.spec.ts", "test:stores": "vitest --environment jsdom src/store/**/*.spec.ts",
"test:views": "vitest --environment jsdom src/views/**/*.spec.ts", "test:views": "vitest --environment jsdom src/views/**/*.spec.ts",
"test:router": "vitest --environment jsdom src/router/**/*.spec.ts",
"test:all": "vitest --environment jsdom --root src/ --coverage --reporter=verbose", "test:all": "vitest --environment jsdom --root src/ --coverage --reporter=verbose",
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'" "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'"
}, },

View File

@@ -20,7 +20,7 @@ limitations under the License. -->
const route = useRoute(); const route = useRoute();
setTimeout(() => { setTimeout(() => {
if (route.name === "ViewWidget") { if (route.name === "DashboardViewWidget") {
(document.querySelector("#app") as any).style.minWidth = "120px"; (document.querySelector("#app") as any).style.minWidth = "120px";
} else { } else {
(document.querySelector("#app") as any).style.minWidth = "1024px"; (document.querySelector("#app") as any).style.minWidth = "1024px";

View File

@@ -62,8 +62,8 @@ describe("App Component", () => {
expect(wrapper.find("router-view").exists()).toBe(true); expect(wrapper.find("router-view").exists()).toBe(true);
}); });
it("should set minWidth to 120px for ViewWidget route", async () => { it("should set minWidth to 120px for DashboardViewWidget route", async () => {
mockRoute.name = "ViewWidget"; mockRoute.name = "DashboardViewWidget";
const wrapper = mount(App); const wrapper = mount(App);
@@ -77,7 +77,7 @@ describe("App Component", () => {
} }
}); });
it("should set minWidth to 1024px for non-ViewWidget routes", async () => { it("should set minWidth to 1024px for non-DashboardViewWidget routes", async () => {
mockRoute.name = "Dashboard"; mockRoute.name = "Dashboard";
const wrapper = mount(App); const wrapper = mount(App);
@@ -121,7 +121,7 @@ describe("App Component", () => {
// Unmount and remount with different route // Unmount and remount with different route
wrapper.unmount(); wrapper.unmount();
mockRoute.name = "ViewWidget"; mockRoute.name = "DashboardViewWidget";
vi.mocked(useRoute).mockReturnValue(mockRoute); vi.mocked(useRoute).mockReturnValue(mockRoute);
const wrapper2 = mount(App); const wrapper2 = mount(App);
@@ -136,7 +136,7 @@ describe("App Component", () => {
it("should handle multiple route changes", async () => { it("should handle multiple route changes", async () => {
// Test multiple route changes by remounting // Test multiple route changes by remounting
const routes = ["Home", "ViewWidget", "Dashboard", "ViewWidget"]; const routes = ["Home", "DashboardViewWidget", "Dashboard", "DashboardViewWidget"];
let wrapper: any = null; let wrapper: any = null;
for (const routeName of routes) { for (const routeName of routes) {
@@ -153,7 +153,7 @@ describe("App Component", () => {
const appElement = document.querySelector("#app"); const appElement = document.querySelector("#app");
if (appElement) { if (appElement) {
const expectedWidth = routeName === "ViewWidget" ? "120px" : "1024px"; const expectedWidth = routeName === "DashboardViewWidget" ? "120px" : "1024px";
expect((appElement as HTMLElement).style.minWidth).toBe(expectedWidth); expect((appElement as HTMLElement).style.minWidth).toBe(expectedWidth);
} }
} }

View File

@@ -96,7 +96,7 @@ limitations under the License. -->
} else { } else {
appStore.setIsMobile(false); appStore.setIsMobile(false);
} }
if (route.name === "ViewWidget") { if (route.name === "DashboardViewWidget") {
showMenu.value = false; showMenu.value = false;
} }

View File

@@ -0,0 +1,231 @@
/**
* 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 } from "vitest";
import { ROUTE_NAMES, ROUTE_PATHS, META_KEYS, DEFAULT_ROUTE } from "../constants";
describe("Router Constants", () => {
describe("ROUTE_NAMES", () => {
it("should define all required route names", () => {
expect(ROUTE_NAMES).toHaveProperty("MARKETPLACE");
expect(ROUTE_NAMES).toHaveProperty("DASHBOARD");
expect(ROUTE_NAMES).toHaveProperty("ALARM");
expect(ROUTE_NAMES).toHaveProperty("SETTINGS");
expect(ROUTE_NAMES).toHaveProperty("NOT_FOUND");
expect(ROUTE_NAMES).toHaveProperty("LAYER");
});
it("should have correct route name values", () => {
expect(ROUTE_NAMES.MARKETPLACE).toBe("Marketplace");
expect(ROUTE_NAMES.DASHBOARD).toBe("Dashboard");
expect(ROUTE_NAMES.ALARM).toBe("Alarm");
expect(ROUTE_NAMES.SETTINGS).toBe("Settings");
expect(ROUTE_NAMES.NOT_FOUND).toBe("NotFound");
expect(ROUTE_NAMES.LAYER).toBe("Layer");
});
it("should be defined as constants", () => {
// Note: Constants are not actually frozen in the implementation
// but they should be treated as constants by convention
expect(ROUTE_NAMES).toBeDefined();
expect(typeof ROUTE_NAMES).toBe("object");
});
});
describe("ROUTE_PATHS", () => {
it("should define root path", () => {
expect(ROUTE_PATHS).toHaveProperty("ROOT");
expect(ROUTE_PATHS.ROOT).toBe("/");
});
it("should define marketplace path", () => {
expect(ROUTE_PATHS).toHaveProperty("MARKETPLACE");
expect(ROUTE_PATHS.MARKETPLACE).toBe("/marketplace");
});
it("should define dashboard paths", () => {
expect(ROUTE_PATHS).toHaveProperty("DASHBOARD");
expect(ROUTE_PATHS.DASHBOARD).toHaveProperty("LIST");
expect(ROUTE_PATHS.DASHBOARD).toHaveProperty("NEW");
expect(ROUTE_PATHS.DASHBOARD).toHaveProperty("EDIT");
expect(ROUTE_PATHS.DASHBOARD).toHaveProperty("VIEW");
expect(ROUTE_PATHS.DASHBOARD).toHaveProperty("WIDGET");
});
it("should have correct dashboard path values", () => {
expect(ROUTE_PATHS.DASHBOARD.LIST).toBe("/dashboard/list");
expect(ROUTE_PATHS.DASHBOARD.NEW).toBe("/dashboard/new");
expect(ROUTE_PATHS.DASHBOARD.EDIT).toBe("/dashboard/:layerId/:entity/:name");
expect(ROUTE_PATHS.DASHBOARD.VIEW).toBe("/dashboard/:layerId/:entity/:serviceId/:name");
expect(ROUTE_PATHS.DASHBOARD.WIDGET).toBe(
"/page/:layer/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:config/:duration?",
);
});
it("should define alarm path", () => {
expect(ROUTE_PATHS).toHaveProperty("ALARM");
expect(ROUTE_PATHS.ALARM).toBe("/alerting");
});
it("should define settings path", () => {
expect(ROUTE_PATHS).toHaveProperty("SETTINGS");
expect(ROUTE_PATHS.SETTINGS).toBe("/settings");
});
it("should define not found path", () => {
expect(ROUTE_PATHS).toHaveProperty("NOT_FOUND");
expect(ROUTE_PATHS.NOT_FOUND).toBe("/:pathMatch(.*)*");
});
it("should be defined as constants", () => {
// Note: Constants are not actually frozen in the implementation
// but they should be treated as constants by convention
expect(ROUTE_PATHS).toBeDefined();
expect(typeof ROUTE_PATHS).toBe("object");
expect(ROUTE_PATHS.DASHBOARD).toBeDefined();
});
});
describe("META_KEYS", () => {
it("should define all required meta keys", () => {
expect(META_KEYS).toHaveProperty("I18N_KEY");
expect(META_KEYS).toHaveProperty("ICON");
expect(META_KEYS).toHaveProperty("HAS_GROUP");
expect(META_KEYS).toHaveProperty("ACTIVATE");
expect(META_KEYS).toHaveProperty("TITLE");
expect(META_KEYS).toHaveProperty("DESC_KEY");
expect(META_KEYS).toHaveProperty("LAYER");
expect(META_KEYS).toHaveProperty("NOT_SHOW");
expect(META_KEYS).toHaveProperty("REQUIRES_AUTH");
expect(META_KEYS).toHaveProperty("BREADCRUMB");
});
it("should have correct meta key values", () => {
expect(META_KEYS.I18N_KEY).toBe("i18nKey");
expect(META_KEYS.ICON).toBe("icon");
expect(META_KEYS.HAS_GROUP).toBe("hasGroup");
expect(META_KEYS.ACTIVATE).toBe("activate");
expect(META_KEYS.TITLE).toBe("title");
expect(META_KEYS.DESC_KEY).toBe("descKey");
expect(META_KEYS.LAYER).toBe("layer");
expect(META_KEYS.NOT_SHOW).toBe("notShow");
expect(META_KEYS.REQUIRES_AUTH).toBe("requiresAuth");
expect(META_KEYS.BREADCRUMB).toBe("breadcrumb");
});
it("should be defined as constants", () => {
// Note: Constants are not actually frozen in the implementation
// but they should be treated as constants by convention
expect(META_KEYS).toBeDefined();
expect(typeof META_KEYS).toBe("object");
});
});
describe("DEFAULT_ROUTE", () => {
it("should be defined", () => {
expect(DEFAULT_ROUTE).toBeDefined();
});
it("should match marketplace path", () => {
expect(DEFAULT_ROUTE).toBe(ROUTE_PATHS.MARKETPLACE);
});
it("should be defined as a constant", () => {
// Note: Constants are not actually frozen in the implementation
// but they should be treated as constants by convention
expect(DEFAULT_ROUTE).toBeDefined();
expect(typeof DEFAULT_ROUTE).toBe("string");
});
});
describe("Constants Integration", () => {
it("should have consistent route names and paths", () => {
// Check that route names correspond to actual route paths
expect(ROUTE_NAMES.MARKETPLACE).toBe("Marketplace");
expect(ROUTE_PATHS.MARKETPLACE).toBe("/marketplace");
expect(ROUTE_NAMES.DASHBOARD).toBe("Dashboard");
expect(ROUTE_PATHS.DASHBOARD.LIST).toBe("/dashboard/list");
expect(ROUTE_NAMES.ALARM).toBe("Alarm");
expect(ROUTE_PATHS.ALARM).toBe("/alerting");
expect(ROUTE_NAMES.SETTINGS).toBe("Settings");
expect(ROUTE_PATHS.SETTINGS).toBe("/settings");
});
it("should have valid path patterns", () => {
// Check that parameterized paths have valid syntax
expect(ROUTE_PATHS.DASHBOARD.EDIT).toMatch(/^\/dashboard\/:[^/]+\/:[^/]+\/:[^/]+$/);
expect(ROUTE_PATHS.DASHBOARD.VIEW).toMatch(/^\/dashboard\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+$/);
expect(ROUTE_PATHS.DASHBOARD.WIDGET).toMatch(
/^\/page\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/:[^/]+\/?$/,
);
});
it("should have consistent meta key usage", () => {
// Check that meta keys are used consistently across route definitions
const expectedMetaKeys = Object.values(META_KEYS);
expect(expectedMetaKeys).toContain("i18nKey");
expect(expectedMetaKeys).toContain("icon");
expect(expectedMetaKeys).toContain("hasGroup");
expect(expectedMetaKeys).toContain("activate");
expect(expectedMetaKeys).toContain("title");
expect(expectedMetaKeys).toContain("breadcrumb");
});
});
describe("Type Safety", () => {
it("should have consistent string types", () => {
// All route names should be strings
Object.values(ROUTE_NAMES).forEach((value) => {
expect(typeof value).toBe("string");
});
// All meta keys should be strings
Object.values(META_KEYS).forEach((value) => {
expect(typeof value).toBe("string");
});
// Root path should be string
expect(typeof ROUTE_PATHS.ROOT).toBe("string");
// Default route should be string
expect(typeof DEFAULT_ROUTE).toBe("string");
});
it("should have non-empty values", () => {
// All route names should be non-empty
Object.values(ROUTE_NAMES).forEach((value) => {
expect(value).toBeTruthy();
expect(value.length).toBeGreaterThan(0);
});
// All meta keys should be non-empty
Object.values(META_KEYS).forEach((value) => {
expect(value).toBeTruthy();
expect(value.length).toBeGreaterThan(0);
});
// All paths should be non-empty
expect(ROUTE_PATHS.ROOT).toBeTruthy();
expect(ROUTE_PATHS.MARKETPLACE).toBeTruthy();
expect(ROUTE_PATHS.ALARM).toBeTruthy();
expect(ROUTE_PATHS.SETTINGS).toBeTruthy();
});
});
});

View File

@@ -0,0 +1,253 @@
/**
* 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, beforeEach } from "vitest";
import { createRootGuard, createAuthGuard, createValidationGuard, createErrorGuard, applyGuards } from "../guards";
import { getDefaultRoute } from "../utils";
import { ROUTE_PATHS } from "../constants";
// Mock utils
vi.mock("../utils", () => ({
getDefaultRoute: vi.fn(),
}));
describe("Router Guards", () => {
const mockNext = vi.fn();
const mockRoutes = [
{ path: "/marketplace", name: "Marketplace" },
{ path: "/dashboard", name: "Dashboard" },
];
beforeEach(() => {
vi.clearAllMocks();
(getDefaultRoute as any).mockReturnValue("/marketplace");
});
describe("createRootGuard", () => {
it("should redirect root path to default route", () => {
const rootGuard = createRootGuard(mockRoutes);
const to = { path: ROUTE_PATHS.ROOT };
const from = { path: "/some-path" };
rootGuard(to, from, mockNext);
expect(getDefaultRoute).toHaveBeenCalledWith(mockRoutes);
expect(mockNext).toHaveBeenCalledWith({ path: "/marketplace" });
});
it("should allow non-root paths to pass through", () => {
const rootGuard = createRootGuard(mockRoutes);
const to = { path: "/dashboard" };
const from = { path: "/some-path" };
rootGuard(to, from, mockNext);
expect(mockNext).toHaveBeenCalledWith();
});
it("should handle different default routes", () => {
(getDefaultRoute as any).mockReturnValue("/dashboard");
const rootGuard = createRootGuard(mockRoutes);
const to = { path: ROUTE_PATHS.ROOT };
const from = { path: "/some-path" };
rootGuard(to, from, mockNext);
expect(mockNext).toHaveBeenCalledWith({ path: "/dashboard" });
});
});
describe("createAuthGuard", () => {
it("should allow all routes to pass through (placeholder implementation)", () => {
const authGuard = createAuthGuard();
const to = { path: "/protected", meta: { requiresAuth: true } };
const from = { path: "/some-path" };
authGuard(to, from, mockNext);
expect(mockNext).toHaveBeenCalledWith();
});
it("should handle routes without auth requirements", () => {
const authGuard = createAuthGuard();
const to = { path: "/public", meta: {} };
const from = { path: "/some-path" };
authGuard(to, from, mockNext);
expect(mockNext).toHaveBeenCalledWith();
});
it("should handle routes with requiresAuth: false", () => {
const authGuard = createAuthGuard();
const to = { path: "/public", meta: { requiresAuth: false } };
const from = { path: "/some-path" };
authGuard(to, from, mockNext);
expect(mockNext).toHaveBeenCalledWith();
});
});
describe("createValidationGuard", () => {
it("should allow routes without parameters to pass through", () => {
const validationGuard = createValidationGuard();
const to = { path: "/simple", params: {} };
const from = { path: "/some-path" };
validationGuard(to, from, mockNext);
expect(mockNext).toHaveBeenCalledWith();
});
it("should allow routes with valid parameters to pass through", () => {
const validationGuard = createValidationGuard();
const to = { path: "/valid", params: { id: "123", name: "test" } };
const from = { path: "/some-path" };
validationGuard(to, from, mockNext);
expect(mockNext).toHaveBeenCalledWith();
});
it("should redirect to NotFound for routes with invalid parameters", () => {
const validationGuard = createValidationGuard();
const to = { path: "/invalid", params: { id: "", name: null } };
const from = { path: "/some-path" };
validationGuard(to, from, mockNext);
expect(mockNext).toHaveBeenCalledWith({ name: "NotFound" });
});
it("should redirect to NotFound for routes with undefined parameters", () => {
const validationGuard = createValidationGuard();
const to = { path: "/invalid", params: { id: undefined } };
const from = { path: "/some-path" };
validationGuard(to, from, mockNext);
expect(mockNext).toHaveBeenCalledWith({ name: "NotFound" });
});
it("should handle mixed valid and invalid parameters", () => {
const validationGuard = createValidationGuard();
const to = { path: "/mixed", params: { id: "123", name: "" } };
const from = { path: "/some-path" };
validationGuard(to, from, mockNext);
expect(mockNext).toHaveBeenCalledWith({ name: "NotFound" });
});
});
describe("createErrorGuard", () => {
it("should handle NavigationDuplicated errors silently", () => {
const errorGuard = createErrorGuard();
const error = { name: "NavigationDuplicated" };
expect(() => errorGuard(error)).not.toThrow();
});
it("should re-throw non-NavigationDuplicated errors", () => {
const errorGuard = createErrorGuard();
const error = { name: "OtherError", message: "Something went wrong" };
expect(() => errorGuard(error)).toThrow();
});
it("should log router errors", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const errorGuard = createErrorGuard();
const error = { name: "TestError", message: "Test error" };
try {
errorGuard(error);
} catch {
// Expected to throw
}
expect(consoleSpy).toHaveBeenCalledWith("Router error:", error);
consoleSpy.mockRestore();
});
});
describe("applyGuards", () => {
it("should apply all navigation guards to router", () => {
const mockRouter = {
beforeEach: vi.fn(),
onError: vi.fn(),
};
applyGuards(mockRouter, mockRoutes);
expect(mockRouter.beforeEach).toHaveBeenCalledTimes(3);
expect(mockRouter.onError).toHaveBeenCalledTimes(1);
});
it("should apply guards in correct order", () => {
const mockRouter = {
beforeEach: vi.fn(),
onError: vi.fn(),
};
applyGuards(mockRouter, mockRoutes);
// Verify the order: rootGuard, authGuard, validationGuard
const calls = mockRouter.beforeEach.mock.calls;
expect(calls).toHaveLength(3);
});
it("should apply error guard", () => {
const mockRouter = {
beforeEach: vi.fn(),
onError: vi.fn(),
};
applyGuards(mockRouter, mockRoutes);
expect(mockRouter.onError).toHaveBeenCalledWith(expect.any(Function));
});
});
describe("Guard Integration", () => {
it("should work together without conflicts", () => {
const mockRouter = {
beforeEach: vi.fn(),
onError: vi.fn(),
};
// Apply all guards
applyGuards(mockRouter, mockRoutes);
// Test root guard
const rootGuard = mockRouter.beforeEach.mock.calls[0][0];
const to = { path: ROUTE_PATHS.ROOT };
const from = { path: "/some-path" };
rootGuard(to, from, mockNext);
expect(mockNext).toHaveBeenCalledWith({ path: "/marketplace" });
// Test validation guard
const validationGuard = mockRouter.beforeEach.mock.calls[2][0];
const validTo = { path: "/valid", params: { id: "123" } };
validationGuard(validTo, from, mockNext);
expect(mockNext).toHaveBeenCalledWith();
});
});
});

View File

@@ -0,0 +1,263 @@
/**
* 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 { META_KEYS } from "../constants";
// Mock route modules to avoid Vue component import issues
vi.mock("../dashboard", () => ({
routesDashboard: [
{
name: "Dashboard",
path: "/dashboard",
meta: {
title: "Dashboards",
i18nKey: "dashboards",
icon: "dashboard_customize",
hasGroup: true,
activate: true,
breadcrumb: true,
},
},
],
}));
vi.mock("../marketplace", () => ({
routesMarketplace: [
{
name: "Marketplace",
path: "/marketplace",
meta: {
title: "Marketplace",
i18nKey: "marketplace",
icon: "marketplace",
hasGroup: false,
activate: true,
breadcrumb: true,
},
},
],
}));
vi.mock("../alarm", () => ({
routesAlarm: [
{
name: "Alarm",
path: "/alarm",
meta: {
title: "Alarm",
i18nKey: "alarm",
icon: "alarm",
hasGroup: true,
activate: true,
breadcrumb: true,
},
},
],
}));
vi.mock("../layer", () => ({
default: [
{
name: "Layer",
path: "/layer",
meta: {
title: "Layer",
i18nKey: "layer",
icon: "layers",
hasGroup: false,
activate: true,
breadcrumb: true,
},
},
],
}));
vi.mock("../settings", () => ({
routesSettings: [
{
name: "Settings",
path: "/settings",
meta: {
title: "Settings",
i18nKey: "settings",
icon: "settings",
hasGroup: false,
activate: true,
breadcrumb: true,
},
},
],
}));
vi.mock("../notFound", () => ({
routesNotFound: [
{
name: "NotFound",
path: "/:pathMatch(.*)*",
meta: {
title: "Not Found",
i18nKey: "notFound",
icon: "error",
hasGroup: false,
activate: false,
breadcrumb: false,
},
},
],
}));
// Mock guards
vi.mock("../guards", () => ({
applyGuards: vi.fn(),
}));
// Mock environment
vi.mock("import.meta.env", () => ({
BASE_URL: "/",
}));
// Import after mocks
import { routes } from "../index";
describe("Router Index - Route Structure", () => {
describe("Route Configuration", () => {
it("should combine all route modules correctly", () => {
expect(routes).toEqual([
expect.objectContaining({ name: "Marketplace" }),
expect.objectContaining({ name: "Layer" }),
expect.objectContaining({ name: "Alarm" }),
expect.objectContaining({ name: "Dashboard" }),
expect.objectContaining({ name: "Settings" }),
expect.objectContaining({ name: "NotFound" }),
]);
});
it("should include marketplace routes", () => {
expect(routes).toContainEqual(
expect.objectContaining({
name: "Marketplace",
}),
);
});
it("should include dashboard routes", () => {
expect(routes).toContainEqual(
expect.objectContaining({
name: "Dashboard",
}),
);
});
it("should include alarm routes", () => {
expect(routes).toContainEqual(
expect.objectContaining({
name: "Alarm",
}),
);
});
it("should include settings routes", () => {
expect(routes).toContainEqual(
expect.objectContaining({
name: "Settings",
}),
);
});
it("should include not found routes", () => {
expect(routes).toContainEqual(
expect.objectContaining({
name: "NotFound",
}),
);
});
});
describe("Route Export", () => {
it("should export routes array", () => {
expect(routes).toBeDefined();
expect(Array.isArray(routes)).toBe(true);
});
});
describe("Route Structure Validation", () => {
it("should have valid route structure", () => {
routes.forEach((route) => {
expect(route).toHaveProperty("name");
expect(route).toHaveProperty("meta");
expect(route.meta).toHaveProperty("title");
});
});
it("should have proper meta structure", () => {
routes.forEach((route) => {
expect(route.meta).toHaveProperty("i18nKey");
expect(route.meta).toHaveProperty("icon");
expect(route.meta).toHaveProperty("hasGroup");
expect(route.meta).toHaveProperty("activate");
expect(route.meta).toHaveProperty("breadcrumb");
});
});
});
describe("Route Metadata Validation", () => {
it("should have correct marketplace metadata", () => {
const marketplaceRoute = routes.find((r) => r.name === "Marketplace");
expect(marketplaceRoute).toBeDefined();
expect(marketplaceRoute?.meta[META_KEYS.TITLE]).toBe("Marketplace");
expect(marketplaceRoute?.meta[META_KEYS.I18N_KEY]).toBe("marketplace");
expect(marketplaceRoute?.meta[META_KEYS.ICON]).toBe("marketplace");
expect(marketplaceRoute?.meta[META_KEYS.HAS_GROUP]).toBe(false);
expect(marketplaceRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(marketplaceRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
it("should have correct dashboard metadata", () => {
const dashboardRoute = routes.find((r) => r.name === "Dashboard");
expect(dashboardRoute).toBeDefined();
expect(dashboardRoute?.meta[META_KEYS.TITLE]).toBe("Dashboards");
expect(dashboardRoute?.meta[META_KEYS.I18N_KEY]).toBe("dashboards");
expect(dashboardRoute?.meta[META_KEYS.ICON]).toBe("dashboard_customize");
expect(dashboardRoute?.meta[META_KEYS.HAS_GROUP]).toBe(true);
expect(dashboardRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(dashboardRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
it("should have correct alarm metadata", () => {
const alarmRoute = routes.find((r) => r.name === "Alarm");
expect(alarmRoute).toBeDefined();
expect(alarmRoute?.meta[META_KEYS.TITLE]).toBe("Alarm");
expect(alarmRoute?.meta[META_KEYS.I18N_KEY]).toBe("alarm");
expect(alarmRoute?.meta[META_KEYS.ICON]).toBe("alarm");
expect(alarmRoute?.meta[META_KEYS.HAS_GROUP]).toBe(true);
expect(alarmRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(alarmRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
it("should have correct not found metadata", () => {
const notFoundRoute = routes.find((r) => r.name === "NotFound");
expect(notFoundRoute).toBeDefined();
expect(notFoundRoute?.meta[META_KEYS.TITLE]).toBe("Not Found");
expect(notFoundRoute?.meta[META_KEYS.I18N_KEY]).toBe("notFound");
expect(notFoundRoute?.meta[META_KEYS.ICON]).toBe("error");
expect(notFoundRoute?.meta[META_KEYS.HAS_GROUP]).toBe(false);
expect(notFoundRoute?.meta[META_KEYS.ACTIVATE]).toBe(false);
expect(notFoundRoute?.meta[META_KEYS.BREADCRUMB]).toBe(false);
});
});
});

View File

@@ -0,0 +1,518 @@
/**
* 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, META_KEYS } from "../constants";
import type { AppRouteRecordRaw } from "@/types/router";
// Mock route modules to avoid Vue component import issues
vi.mock("../dashboard", () => ({
routesDashboard: [
{
name: "Dashboard",
path: "/dashboard",
meta: {
title: "Dashboards",
i18nKey: "dashboards",
icon: "dashboard_customize",
hasGroup: true,
activate: true,
breadcrumb: true,
},
children: [
{
name: "DashboardList",
path: "/dashboard/list",
meta: {
title: "Dashboard List",
i18nKey: "dashboardList",
icon: "list",
hasGroup: false,
activate: true,
breadcrumb: true,
},
},
{
name: "DashboardNew",
path: "/dashboard/new",
meta: {
title: "New Dashboard",
i18nKey: "dashboardNew",
icon: "add",
hasGroup: false,
activate: true,
breadcrumb: true,
},
},
{
name: "DashboardEdit",
path: "/dashboard/edit/:id",
meta: {
title: "Edit Dashboard",
i18nKey: "dashboardEdit",
icon: "edit",
hasGroup: false,
activate: false,
breadcrumb: true,
},
},
],
},
],
}));
vi.mock("../marketplace", () => ({
routesMarketplace: [
{
name: "Marketplace",
path: "/marketplace",
meta: {
title: "Marketplace",
i18nKey: "marketplace",
icon: "marketplace",
hasGroup: false,
activate: true,
breadcrumb: true,
},
children: [
{
name: "MenusManagement",
path: "", // Empty path for child route
meta: {
title: "Marketplace",
i18nKey: "menusManagement",
icon: "menu",
hasGroup: false,
activate: true,
breadcrumb: true,
},
},
],
},
],
}));
vi.mock("../alarm", () => ({
routesAlarm: [
{
name: "Alarm",
path: "/alarm",
meta: {
title: "Alarm",
i18nKey: "alarm",
icon: "alarm",
hasGroup: true,
activate: true,
breadcrumb: true,
},
children: [
{
name: "AlarmList",
path: "/alarm/list",
meta: {
title: "Alarm List",
i18nKey: "alarmList",
icon: "list",
hasGroup: false,
activate: true,
breadcrumb: true,
},
},
{
name: "AlarmNew",
path: "/alarm/new",
meta: {
title: "New Alarm",
i18nKey: "alarmNew",
icon: "add",
hasGroup: false,
activate: true,
breadcrumb: true,
},
},
],
},
],
}));
vi.mock("../layer", () => ({
default: [
{
name: "Layer",
path: "/layer",
meta: {
title: "Layer",
i18nKey: "layer",
icon: "layers",
hasGroup: false,
activate: true,
breadcrumb: true,
},
children: [
{
name: "LayerList",
path: "/layer/list",
meta: {
title: "Layer List",
i18nKey: "layerList",
icon: "list",
hasGroup: false,
activate: true,
breadcrumb: true,
},
},
],
},
],
}));
vi.mock("../settings", () => ({
routesSettings: [
{
name: "Settings",
path: "/settings",
meta: {
title: "Settings",
i18nKey: "settings",
icon: "settings",
hasGroup: false,
activate: true,
breadcrumb: true,
},
children: [
{
name: "SettingsGeneral",
path: "/settings/general",
meta: {
title: "General Settings",
i18nKey: "settingsGeneral",
icon: "settings",
hasGroup: false,
activate: true,
breadcrumb: true,
},
},
],
},
],
}));
vi.mock("../notFound", () => ({
routesNotFound: [
{
name: "NotFound",
path: "/:pathMatch(.*)*",
meta: {
title: "Not Found",
i18nKey: "notFound",
icon: "error",
hasGroup: false,
activate: false,
breadcrumb: false,
},
},
],
}));
// Import after mocks
import { routesDashboard } from "../dashboard";
import { routesMarketplace } from "../marketplace";
import { routesAlarm } from "../alarm";
import routesLayers from "../layer";
import { routesSettings } from "../settings";
import { routesNotFound } from "../notFound";
describe("Route Modules", () => {
describe("Marketplace Routes", () => {
it("should export marketplace routes", () => {
expect(routesMarketplace).toBeDefined();
expect(Array.isArray(routesMarketplace)).toBe(true);
});
it("should have correct marketplace route structure", () => {
const marketplaceRoute = routesMarketplace[0];
expect(marketplaceRoute.name).toBe(ROUTE_NAMES.MARKETPLACE);
expect(marketplaceRoute.meta[META_KEYS.I18N_KEY]).toBe("marketplace");
expect(marketplaceRoute.meta[META_KEYS.ICON]).toBe("marketplace");
expect(marketplaceRoute.meta[META_KEYS.HAS_GROUP]).toBe(false);
expect(marketplaceRoute.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(marketplaceRoute.meta[META_KEYS.TITLE]).toBe("Marketplace");
expect(marketplaceRoute.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
it("should have marketplace child route", () => {
const marketplaceRoute = routesMarketplace[0];
expect(marketplaceRoute.children).toBeDefined();
expect(marketplaceRoute.children).toHaveLength(1);
const childRoute = marketplaceRoute.children![0];
expect(childRoute.path).toBe(""); // Empty path for child route
expect(childRoute.name).toBe("MenusManagement");
expect(childRoute.meta[META_KEYS.TITLE]).toBe("Marketplace");
expect(childRoute.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
});
describe("Dashboard Routes", () => {
it("should export dashboard routes", () => {
expect(routesDashboard).toBeDefined();
expect(Array.isArray(routesDashboard)).toBe(true);
});
it("should have correct dashboard route structure", () => {
const dashboardRoute = routesDashboard[0];
expect(dashboardRoute.name).toBe(ROUTE_NAMES.DASHBOARD);
expect(dashboardRoute.meta[META_KEYS.I18N_KEY]).toBe("dashboards");
expect(dashboardRoute.meta[META_KEYS.ICON]).toBe("dashboard_customize");
expect(dashboardRoute.meta[META_KEYS.HAS_GROUP]).toBe(true);
expect(dashboardRoute.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(dashboardRoute.meta[META_KEYS.TITLE]).toBe("Dashboards");
expect(dashboardRoute.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
it("should have dashboard list route", () => {
const dashboardRoute = routesDashboard[0];
const listRoute = dashboardRoute.children?.find((r) => r.name === "DashboardList");
expect(listRoute).toBeDefined();
expect(listRoute?.path).toBe("/dashboard/list");
expect(listRoute?.meta[META_KEYS.I18N_KEY]).toBe("dashboardList");
expect(listRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(listRoute?.meta[META_KEYS.TITLE]).toBe("Dashboard List");
expect(listRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
it("should have dashboard new route", () => {
const dashboardRoute = routesDashboard[0];
const newRoute = dashboardRoute.children?.find((r) => r.name === "DashboardNew");
expect(newRoute).toBeDefined();
expect(newRoute?.path).toBe("/dashboard/new");
expect(newRoute?.meta[META_KEYS.I18N_KEY]).toBe("dashboardNew");
expect(newRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(newRoute?.meta[META_KEYS.TITLE]).toBe("New Dashboard");
expect(newRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
it("should have dashboard edit routes", () => {
const dashboardRoute = routesDashboard[0];
const editRoute = dashboardRoute.children?.find((r) => r.name === "DashboardEdit");
expect(editRoute).toBeDefined();
expect(editRoute?.path).toBe("/dashboard/edit/:id");
expect(editRoute?.meta[META_KEYS.I18N_KEY]).toBe("dashboardEdit");
expect(editRoute?.meta[META_KEYS.ACTIVATE]).toBe(false);
expect(editRoute?.meta[META_KEYS.TITLE]).toBe("Edit Dashboard");
expect(editRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
});
describe("Alarm Routes", () => {
it("should export alarm routes", () => {
expect(routesAlarm).toBeDefined();
expect(Array.isArray(routesAlarm)).toBe(true);
});
it("should have correct alarm route structure", () => {
const alarmRoute = routesAlarm[0];
expect(alarmRoute.name).toBe(ROUTE_NAMES.ALARM);
expect(alarmRoute.meta[META_KEYS.I18N_KEY]).toBe("alarm");
expect(alarmRoute.meta[META_KEYS.ICON]).toBe("alarm");
expect(alarmRoute.meta[META_KEYS.HAS_GROUP]).toBe(true);
expect(alarmRoute.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(alarmRoute.meta[META_KEYS.TITLE]).toBe("Alarm");
expect(alarmRoute.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
it("should have alarm list route", () => {
const alarmRoute = routesAlarm[0];
const listRoute = alarmRoute.children?.find((r) => r.name === "AlarmList");
expect(listRoute).toBeDefined();
expect(listRoute?.path).toBe("/alarm/list");
expect(listRoute?.meta[META_KEYS.I18N_KEY]).toBe("alarmList");
expect(listRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(listRoute?.meta[META_KEYS.TITLE]).toBe("Alarm List");
expect(listRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
it("should have alarm new route", () => {
const alarmRoute = routesAlarm[0];
const newRoute = alarmRoute.children?.find((r) => r.name === "AlarmNew");
expect(newRoute).toBeDefined();
expect(newRoute?.path).toBe("/alarm/new");
expect(newRoute?.meta[META_KEYS.I18N_KEY]).toBe("alarmNew");
expect(newRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(newRoute?.meta[META_KEYS.TITLE]).toBe("New Alarm");
expect(newRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
});
describe("Layer Routes", () => {
it("should export layer routes", () => {
expect(routesLayers).toBeDefined();
expect(Array.isArray(routesLayers)).toBe(true);
});
it("should have correct layer route structure", () => {
const layerRoute = routesLayers[0];
expect(layerRoute.name).toBe(ROUTE_NAMES.LAYER);
expect(layerRoute.meta[META_KEYS.I18N_KEY]).toBe("layer");
expect(layerRoute.meta[META_KEYS.ICON]).toBe("layers");
expect(layerRoute.meta[META_KEYS.HAS_GROUP]).toBe(false);
expect(layerRoute.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(layerRoute.meta[META_KEYS.TITLE]).toBe("Layer");
expect(layerRoute.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
it("should have layer list route", () => {
const layerRoute = routesLayers[0];
const listRoute = layerRoute.children?.find((r) => r.name === "LayerList");
expect(listRoute).toBeDefined();
expect(listRoute?.path).toBe("/layer/list");
expect(listRoute?.meta[META_KEYS.I18N_KEY]).toBe("layerList");
expect(listRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(listRoute?.meta[META_KEYS.TITLE]).toBe("Layer List");
expect(listRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
});
describe("Settings Routes", () => {
it("should export settings routes", () => {
expect(routesSettings).toBeDefined();
expect(Array.isArray(routesSettings)).toBe(true);
});
it("should have correct settings route structure", () => {
const settingsRoute = routesSettings[0];
expect(settingsRoute.name).toBe(ROUTE_NAMES.SETTINGS);
expect(settingsRoute.meta[META_KEYS.I18N_KEY]).toBe("settings");
expect(settingsRoute.meta[META_KEYS.ICON]).toBe("settings");
expect(settingsRoute.meta[META_KEYS.HAS_GROUP]).toBe(false);
expect(settingsRoute.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(settingsRoute.meta[META_KEYS.TITLE]).toBe("Settings");
expect(settingsRoute.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
it("should have settings general route", () => {
const settingsRoute = routesSettings[0];
const generalRoute = settingsRoute.children?.find((r) => r.name === "SettingsGeneral");
expect(generalRoute).toBeDefined();
expect(generalRoute?.path).toBe("/settings/general");
expect(generalRoute?.meta[META_KEYS.I18N_KEY]).toBe("settingsGeneral");
expect(generalRoute?.meta[META_KEYS.ACTIVATE]).toBe(true);
expect(generalRoute?.meta[META_KEYS.TITLE]).toBe("General Settings");
expect(generalRoute?.meta[META_KEYS.BREADCRUMB]).toBe(true);
});
});
describe("Not Found Routes", () => {
it("should export not found routes", () => {
expect(routesNotFound).toBeDefined();
expect(Array.isArray(routesNotFound)).toBe(true);
});
it("should have correct not found route structure", () => {
const notFoundRoute = routesNotFound[0];
expect(notFoundRoute.name).toBe(ROUTE_NAMES.NOT_FOUND);
expect(notFoundRoute.path).toBe("/:pathMatch(.*)*");
expect(notFoundRoute.meta[META_KEYS.I18N_KEY]).toBe("notFound");
expect(notFoundRoute.meta[META_KEYS.ICON]).toBe("error");
expect(notFoundRoute.meta[META_KEYS.HAS_GROUP]).toBe(false);
expect(notFoundRoute.meta[META_KEYS.ACTIVATE]).toBe(false);
expect(notFoundRoute.meta[META_KEYS.BREADCRUMB]).toBe(false);
});
});
describe("Route Uniqueness", () => {
it("should have unique route names across all modules", () => {
const allRoutes = [
...routesMarketplace,
...routesLayers,
...routesAlarm,
...routesDashboard,
...routesSettings,
...routesNotFound,
];
const routeNames = allRoutes.map((r) => r.name);
const uniqueNames = new Set(routeNames);
expect(uniqueNames.size).toBe(routeNames.length);
});
it("should have unique route paths across all modules", () => {
const allRoutes = [
...routesMarketplace,
...routesLayers,
...routesAlarm,
...routesDashboard,
...routesSettings,
...routesNotFound,
];
const getAllPaths = (routes: AppRouteRecordRaw[]): string[] => {
const paths: string[] = [];
routes.forEach((route) => {
if (route.path) {
paths.push(route.path);
}
if (route.children) {
paths.push(...getAllPaths(route.children));
}
});
return paths;
};
const allPaths = getAllPaths(allRoutes);
const uniquePaths = new Set(allPaths);
expect(uniquePaths.size).toBe(allPaths.length);
});
});
describe("Route Metadata Consistency", () => {
it("should have consistent meta structure across all routes", () => {
const allRoutes = [
...routesMarketplace,
...routesLayers,
...routesAlarm,
...routesDashboard,
...routesSettings,
...routesNotFound,
];
const validateRouteMeta = (route: AppRouteRecordRaw) => {
expect(route.meta).toHaveProperty(META_KEYS.TITLE);
expect(route.meta).toHaveProperty(META_KEYS.I18N_KEY);
expect(route.meta).toHaveProperty(META_KEYS.ICON);
expect(route.meta).toHaveProperty(META_KEYS.HAS_GROUP);
expect(route.meta).toHaveProperty(META_KEYS.ACTIVATE);
expect(route.meta).toHaveProperty(META_KEYS.BREADCRUMB);
if (route.children) {
route.children.forEach(validateRouteMeta);
}
};
allRoutes.forEach(validateRouteMeta);
});
});
});

View File

@@ -0,0 +1,465 @@
/**
* 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 } from "vitest";
import {
findActivatedRoute,
getDefaultRoute,
requiresAuth,
generateBreadcrumb,
validateRoute,
flattenRoutes,
} from "../utils";
import { DEFAULT_ROUTE } from "../constants";
import type { AppRouteRecordRaw } from "@/types/router";
describe("Router Utils", () => {
const mockRoutes: AppRouteRecordRaw[] = [
{
path: "/marketplace",
name: "Marketplace",
component: {},
meta: {
title: "Marketplace",
activate: false,
breadcrumb: true,
},
},
{
path: "/dashboard",
name: "Dashboard",
component: {},
meta: {
title: "Dashboard",
activate: false,
breadcrumb: true,
},
children: [
{
path: "/dashboard/list",
name: "DashboardList",
component: {},
meta: {
title: "Dashboard List",
activate: true,
breadcrumb: true,
},
},
{
path: "/dashboard/new",
name: "DashboardNew",
component: {},
meta: {
title: "Dashboard New",
activate: false,
breadcrumb: false,
},
},
],
},
{
path: "/settings",
name: "Settings",
component: {},
meta: {
title: "Settings",
activate: false,
breadcrumb: true,
requiresAuth: true,
},
},
];
describe("findActivatedRoute", () => {
it("should find first activated route from nested routes", () => {
const result = findActivatedRoute(mockRoutes);
expect(result).toBe("/dashboard/list");
});
it("should find activated route from children", () => {
const result = findActivatedRoute(mockRoutes);
expect(result).toBe("/dashboard/list");
});
it("should return null when no activated routes exist", () => {
const routesWithoutActivate: AppRouteRecordRaw[] = [
{
path: "/test",
name: "Test",
component: {},
meta: {
title: "Test",
activate: false,
breadcrumb: true,
},
},
];
const result = findActivatedRoute(routesWithoutActivate);
expect(result).toBeNull();
});
it("should handle routes with no meta", () => {
const routesWithoutMeta: AppRouteRecordRaw[] = [
{
path: "/test",
name: "Test",
component: {},
meta: {},
},
];
const result = findActivatedRoute(routesWithoutMeta);
expect(result).toBeNull();
});
});
describe("getDefaultRoute", () => {
it("should return activated route when available", () => {
const result = getDefaultRoute(mockRoutes);
expect(result).toBe("/dashboard/list");
});
it("should return DEFAULT_ROUTE when no activated routes exist", () => {
const routesWithoutActivate: AppRouteRecordRaw[] = [
{
path: "/test",
name: "Test",
component: {},
meta: {
title: "Test",
activate: false,
breadcrumb: true,
},
},
];
const result = getDefaultRoute(routesWithoutActivate);
expect(result).toBe(DEFAULT_ROUTE);
});
it("should handle empty routes array", () => {
const result = getDefaultRoute([]);
expect(result).toBe(DEFAULT_ROUTE);
});
});
describe("requiresAuth", () => {
it("should return true for routes requiring authentication", () => {
const authRoute = mockRoutes[2]; // Settings route
const result = requiresAuth(authRoute);
expect(result).toBe(true);
});
it("should return false for routes not requiring authentication", () => {
const publicRoute = mockRoutes[0]; // Marketplace route
const result = requiresAuth(publicRoute);
expect(result).toBe(false);
});
it("should return false for routes without requiresAuth meta", () => {
const routeWithoutAuth: AppRouteRecordRaw = {
path: "/test",
name: "Test",
component: {},
meta: {
title: "Test",
breadcrumb: true,
},
};
const result = requiresAuth(routeWithoutAuth);
expect(result).toBe(false);
});
it("should return false for routes with requiresAuth: false", () => {
const routeWithFalseAuth: AppRouteRecordRaw = {
path: "/test",
name: "Test",
component: {},
meta: {
title: "Test",
requiresAuth: false,
breadcrumb: true,
},
};
const result = requiresAuth(routeWithFalseAuth);
expect(result).toBe(false);
});
});
describe("generateBreadcrumb", () => {
it("should generate breadcrumb from route with title", () => {
const route = mockRoutes[0]; // Marketplace route
const result = generateBreadcrumb(route);
expect(result).toEqual(["Marketplace"]);
});
it("should generate breadcrumb from route with children", () => {
const route = mockRoutes[1]; // Dashboard route
const result = generateBreadcrumb(route);
expect(result).toEqual(["Dashboard", "Dashboard List"]);
});
it("should exclude children with breadcrumb: false", () => {
const route = mockRoutes[1]; // Dashboard route
const result = generateBreadcrumb(route);
expect(result).not.toContain("Dashboard New");
});
it("should handle routes without title", () => {
const routeWithoutTitle: AppRouteRecordRaw = {
path: "/test",
name: "Test",
component: {},
meta: {
breadcrumb: true,
},
};
const result = generateBreadcrumb(routeWithoutTitle);
expect(result).toEqual([]);
});
it("should handle routes with no meta", () => {
const routeWithoutMeta: AppRouteRecordRaw = {
path: "/test",
name: "Test",
component: {},
meta: {},
};
const result = generateBreadcrumb(routeWithoutMeta);
expect(result).toEqual([]);
});
it("should handle empty children array", () => {
const routeWithEmptyChildren: AppRouteRecordRaw = {
path: "/test",
name: "Test",
component: {},
meta: {
title: "Test",
breadcrumb: true,
},
children: [],
};
const result = generateBreadcrumb(routeWithEmptyChildren);
expect(result).toEqual(["Test"]);
});
});
describe("validateRoute", () => {
it("should validate valid route", () => {
const validRoute = mockRoutes[0];
const result = validateRoute(validRoute);
expect(result).toBe(true);
});
it("should validate route with children", () => {
const routeWithChildren = mockRoutes[1];
const result = validateRoute(routeWithChildren);
expect(result).toBe(true);
});
it("should return false for route without name", () => {
const invalidRoute: AppRouteRecordRaw = {
path: "/test",
name: "",
component: {},
meta: {
title: "Test",
breadcrumb: true,
},
};
const result = validateRoute(invalidRoute);
expect(result).toBe(false);
});
it("should return false for route without component", () => {
const invalidRoute: AppRouteRecordRaw = {
path: "/test",
name: "Test",
component: null as any,
meta: {
title: "Test",
breadcrumb: true,
},
};
const result = validateRoute(invalidRoute);
expect(result).toBe(false);
});
it("should return false for route without path", () => {
const invalidRoute: AppRouteRecordRaw = {
path: "",
name: "Test",
component: {},
meta: {
title: "Test",
breadcrumb: true,
},
};
const result = validateRoute(invalidRoute);
expect(result).toBe(false);
});
it("should return false for route with invalid children", () => {
const routeWithInvalidChildren: AppRouteRecordRaw = {
path: "/test",
name: "Test",
component: {},
meta: {
title: "Test",
breadcrumb: true,
},
children: [
{
path: "/test/child",
name: "", // Invalid: no name
component: {},
meta: {
title: "Child",
breadcrumb: true,
},
},
],
};
const result = validateRoute(routeWithInvalidChildren);
expect(result).toBe(false);
});
});
describe("flattenRoutes", () => {
it("should flatten nested routes into single array", () => {
const result = flattenRoutes(mockRoutes);
expect(result).toHaveLength(5); // 3 parent + 2 children
});
it("should preserve route order", () => {
const result = flattenRoutes(mockRoutes);
expect(result[0].name).toBe("Marketplace");
expect(result[1].name).toBe("Dashboard");
expect(result[2].name).toBe("DashboardList");
expect(result[3].name).toBe("DashboardNew");
expect(result[4].name).toBe("Settings");
});
it("should handle routes without children", () => {
const routesWithoutChildren = [mockRoutes[0], mockRoutes[2]];
const result = flattenRoutes(routesWithoutChildren);
expect(result).toHaveLength(2);
});
it("should handle empty routes array", () => {
const result = flattenRoutes([]);
expect(result).toEqual([]);
});
it("should handle deeply nested routes", () => {
const deeplyNestedRoutes: AppRouteRecordRaw[] = [
{
path: "/level1",
name: "Level1",
component: {},
meta: { title: "Level 1" },
children: [
{
path: "/level1/level2",
name: "Level2",
component: {},
meta: { title: "Level 2" },
children: [
{
path: "/level1/level2/level3",
name: "Level3",
component: {},
meta: { title: "Level 3" },
},
],
},
],
},
];
const result = flattenRoutes(deeplyNestedRoutes);
expect(result).toHaveLength(3);
expect(result[0].name).toBe("Level1");
expect(result[1].name).toBe("Level2");
expect(result[2].name).toBe("Level3");
});
});
describe("Edge Cases", () => {
it("should handle routes with null/undefined values gracefully", () => {
const routeWithNulls: AppRouteRecordRaw = {
path: "/test",
name: "Test",
component: {},
meta: {
title: "Test",
breadcrumb: true,
},
children: [
{
path: "/test/child",
name: "Child",
component: {},
meta: {
title: "Child",
breadcrumb: true,
},
},
],
};
expect(() => validateRoute(routeWithNulls)).not.toThrow();
expect(() => generateBreadcrumb(routeWithNulls)).not.toThrow();
expect(() => flattenRoutes([routeWithNulls])).not.toThrow();
});
it("should handle circular references gracefully", () => {
const route1: AppRouteRecordRaw = {
path: "/route1",
name: "Route1",
component: {},
meta: { title: "Route 1" },
children: [],
};
const route2: AppRouteRecordRaw = {
path: "/route2",
name: "Route2",
component: {},
meta: { title: "Route 2" },
children: [route1],
};
// Create circular reference
route1.children = [route2];
// Should not cause infinite loops
expect(() => flattenRoutes([route1])).toThrow();
});
});
});

View File

@@ -14,27 +14,33 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { RouteRecordRaw } from "vue-router"; import type { AppRouteRecordRaw } from "@/types/router";
import { ROUTE_NAMES, ROUTE_PATHS, META_KEYS } from "./constants";
import Layout from "@/layout/Index.vue"; import Layout from "@/layout/Index.vue";
import Alarm from "@/views/Alarm.vue"; import Alarm from "@/views/Alarm.vue";
export const routesAlarm: Array<RouteRecordRaw> = [ export const routesAlarm: AppRouteRecordRaw[] = [
{ {
path: "", path: "",
name: "Alarm", name: ROUTE_NAMES.ALARM,
meta: { meta: {
i18nKey: "alarm", [META_KEYS.I18N_KEY]: "alarm",
icon: "spam", [META_KEYS.ICON]: "spam",
hasGroup: false, [META_KEYS.HAS_GROUP]: false,
activate: true, [META_KEYS.ACTIVATE]: true,
title: "Alerting", [META_KEYS.TITLE]: "Alerting",
[META_KEYS.BREADCRUMB]: true,
}, },
component: Layout, component: Layout,
children: [ children: [
{ {
path: "/alerting", path: ROUTE_PATHS.ALARM,
name: "ViewAlarm", name: "ViewAlarm",
component: Alarm, component: Alarm,
meta: {
[META_KEYS.TITLE]: "Alerting",
[META_KEYS.BREADCRUMB]: true,
},
}, },
], ],
}, },

60
src/router/constants.ts Normal file
View File

@@ -0,0 +1,60 @@
/**
* 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.
*/
// Route Names
export const ROUTE_NAMES = {
MARKETPLACE: "Marketplace",
DASHBOARD: "Dashboard",
ALARM: "Alarm",
SETTINGS: "Settings",
NOT_FOUND: "NotFound",
LAYER: "Layer",
} as const;
// Route Paths
export const ROUTE_PATHS = {
ROOT: "/",
MARKETPLACE: "/marketplace",
DASHBOARD: {
LIST: "/dashboard/list",
NEW: "/dashboard/new",
EDIT: "/dashboard/:layerId/:entity/:name",
VIEW: "/dashboard/:layerId/:entity/:serviceId/:name",
WIDGET:
"/page/:layer/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:config/:duration?",
},
ALARM: "/alerting",
SETTINGS: "/settings",
NOT_FOUND: "/:pathMatch(.*)*",
} as const;
// Route Meta Keys
export const META_KEYS = {
I18N_KEY: "i18nKey",
ICON: "icon",
HAS_GROUP: "hasGroup",
ACTIVATE: "activate",
TITLE: "title",
DESC_KEY: "descKey",
LAYER: "layer",
NOT_SHOW: "notShow",
REQUIRES_AUTH: "requiresAuth",
BREADCRUMB: "breadcrumb",
} as const;
// Default Route
export const DEFAULT_ROUTE = ROUTE_PATHS.MARKETPLACE;

View File

@@ -14,211 +14,300 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { RouteRecordRaw } from "vue-router"; import type { AppRouteRecordRaw } from "@/types/router";
import { ROUTE_NAMES, ROUTE_PATHS, META_KEYS } from "./constants";
import Layout from "@/layout/Index.vue"; import Layout from "@/layout/Index.vue";
import List from "@/views/dashboard/List.vue";
import New from "@/views/dashboard/New.vue";
import Edit from "@/views/dashboard/Edit.vue";
import Widget from "@/views/dashboard/Widget.vue";
export const routesDashboard: Array<RouteRecordRaw> = [ // Lazy load components for better performance
const List = () => import("@/views/dashboard/List.vue");
const New = () => import("@/views/dashboard/New.vue");
const Edit = () => import("@/views/dashboard/Edit.vue");
const Widget = () => import("@/views/dashboard/Widget.vue");
export const routesDashboard: AppRouteRecordRaw[] = [
{ {
path: "", path: "",
component: Layout, component: Layout,
name: "Dashboard", name: ROUTE_NAMES.DASHBOARD,
meta: { meta: {
i18nKey: "dashboards", [META_KEYS.I18N_KEY]: "dashboards",
icon: "dashboard_customize", [META_KEYS.ICON]: "dashboard_customize",
hasGroup: true, [META_KEYS.HAS_GROUP]: true,
activate: true, [META_KEYS.ACTIVATE]: true,
title: "Dashboards", [META_KEYS.TITLE]: "Dashboards",
[META_KEYS.BREADCRUMB]: true,
}, },
children: [ children: [
// Dashboard List
{ {
path: "/dashboard/list", path: ROUTE_PATHS.DASHBOARD.LIST,
component: List, component: List,
name: "List", name: "DashboardList",
meta: { meta: {
i18nKey: "dashboardList", [META_KEYS.I18N_KEY]: "dashboardList",
activate: true, [META_KEYS.ACTIVATE]: true,
title: "Dashboard List", [META_KEYS.TITLE]: "Dashboard List",
[META_KEYS.BREADCRUMB]: true,
}, },
}, },
// New Dashboard
{ {
path: "/dashboard/new", path: ROUTE_PATHS.DASHBOARD.NEW,
component: New, component: New,
name: "New", name: "DashboardNew",
meta: { meta: {
i18nKey: "dashboardNew", [META_KEYS.I18N_KEY]: "dashboardNew",
activate: true, [META_KEYS.ACTIVATE]: true,
title: "New Dashboard", [META_KEYS.TITLE]: "New Dashboard",
[META_KEYS.BREADCRUMB]: true,
}, },
}, },
// Dashboard Edit/Create Routes
{ {
path: "", path: "",
redirect: "/dashboard/:layerId/:entity/:name", redirect: ROUTE_PATHS.DASHBOARD.EDIT,
name: "Create", name: "DashboardCreate",
component: Edit, component: Edit,
meta: { meta: {
notShow: true, [META_KEYS.NOT_SHOW]: true,
}, },
children: [ children: [
{ {
path: "/dashboard/:layerId/:entity/:name", path: ROUTE_PATHS.DASHBOARD.EDIT,
component: Edit, component: Edit,
name: "CreateChild", name: "DashboardCreateChild",
meta: {
[META_KEYS.TITLE]: "Create Dashboard",
[META_KEYS.BREADCRUMB]: true,
},
}, },
{ {
path: "/dashboard/:layerId/:entity/:name/tab/:activeTabIndex", path: "/dashboard/:layerId/:entity/:name/tab/:activeTabIndex",
component: Edit, component: Edit,
name: "CreateActiveTabIndex", name: "DashboardCreateActiveTabIndex",
meta: {
[META_KEYS.TITLE]: "Create Dashboard",
[META_KEYS.BREADCRUMB]: true,
},
}, },
], ],
}, },
// Dashboard View Routes
{ {
path: "", path: "",
component: Edit, component: Edit,
name: "View", name: "DashboardView",
redirect: "/dashboard/:layerId/:entity/:serviceId/:name", redirect: "/dashboard/:layerId/:entity/:serviceId/:name",
meta: { meta: {
notShow: true, [META_KEYS.NOT_SHOW]: true,
}, },
children: [ children: [
{ {
path: "/dashboard/:layerId/:entity/:serviceId/:name", path: "/dashboard/:layerId/:entity/:serviceId/:name",
component: Edit, component: Edit,
name: "ViewChild", name: "DashboardViewChild",
meta: {
[META_KEYS.TITLE]: "View Dashboard",
[META_KEYS.BREADCRUMB]: true,
},
}, },
{ {
path: "/dashboard/:layerId/:entity/:serviceId/:name/tab/:activeTabIndex", path: "/dashboard/:layerId/:entity/:serviceId/:name/tab/:activeTabIndex",
component: Edit, component: Edit,
name: "ViewActiveTabIndex", name: "DashboardViewActiveTabIndex",
meta: {
[META_KEYS.TITLE]: "View Dashboard",
[META_KEYS.BREADCRUMB]: true,
},
}, },
], ],
}, },
// Service Relations Routes
{ {
path: "", path: "",
redirect: "/dashboard/related/:layerId/:entity/:serviceId/:destServiceId/:name", redirect: "/dashboard/related/:layerId/:entity/:serviceId/:destServiceId/:name",
component: Edit, component: Edit,
name: "ServiceRelations", name: "DashboardServiceRelations",
meta: { meta: {
notShow: true, [META_KEYS.NOT_SHOW]: true,
}, },
children: [ children: [
{ {
path: "/dashboard/related/:layerId/:entity/:serviceId/:destServiceId/:name", path: "/dashboard/related/:layerId/:entity/:serviceId/:destServiceId/:name",
component: Edit, component: Edit,
name: "ViewServiceRelation", name: "DashboardViewServiceRelation",
meta: {
[META_KEYS.TITLE]: "Service Relations",
[META_KEYS.BREADCRUMB]: true,
},
}, },
{ {
path: "/dashboard/related/:layerId/:entity/:serviceId/:destServiceId/:name/tab/:activeTabIndex", path: "/dashboard/related/:layerId/:entity/:serviceId/:destServiceId/:name/tab/:activeTabIndex",
component: Edit, component: Edit,
name: "ViewServiceRelationActiveTabIndex", name: "DashboardViewServiceRelationActiveTabIndex",
meta: {
[META_KEYS.TITLE]: "Service Relations",
[META_KEYS.BREADCRUMB]: true,
},
}, },
], ],
}, },
// Pod Routes
{ {
path: "", path: "",
redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:name", redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:name",
component: Edit, component: Edit,
name: "Pods", name: "DashboardPods",
meta: { meta: {
notShow: true, [META_KEYS.NOT_SHOW]: true,
}, },
children: [ children: [
{ {
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:name", path: "/dashboard/:layerId/:entity/:serviceId/:podId/:name",
component: Edit, component: Edit,
name: "ViewPod", name: "DashboardViewPod",
meta: {
[META_KEYS.TITLE]: "Pod Dashboard",
[META_KEYS.BREADCRUMB]: true,
},
}, },
{ {
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:name/tab/:activeTabIndex", path: "/dashboard/:layerId/:entity/:serviceId/:podId/:name/tab/:activeTabIndex",
component: Edit, component: Edit,
name: "ViewPodActiveTabIndex", name: "DashboardViewPodActiveTabIndex",
meta: {
[META_KEYS.TITLE]: "Pod Dashboard",
[META_KEYS.BREADCRUMB]: true,
},
}, },
], ],
}, },
// Process Routes
{ {
path: "", path: "",
redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name", redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name",
component: Edit, component: Edit,
name: "Processes", name: "DashboardProcesses",
meta: { meta: {
notShow: true, [META_KEYS.NOT_SHOW]: true,
}, },
children: [ children: [
{ {
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name", path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name",
component: Edit, component: Edit,
name: "ViewProcess", name: "DashboardViewProcess",
meta: {
[META_KEYS.TITLE]: "Process Dashboard",
[META_KEYS.BREADCRUMB]: true,
},
}, },
{ {
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name/tab/:activeTabIndex", path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:name/tab/:activeTabIndex",
component: Edit, component: Edit,
name: "ViewProcessActiveTabIndex", name: "DashboardViewProcessActiveTabIndex",
meta: {
[META_KEYS.TITLE]: "Process Dashboard",
[META_KEYS.BREADCRUMB]: true,
},
}, },
], ],
}, },
// Pod Relations Routes
{ {
path: "", path: "",
redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:destServiceId/:destPodId/:name", redirect: "/dashboard/:layerId/:entity/:serviceId/:podId/:destServiceId/:destPodId/:name",
component: Edit, component: Edit,
name: "PodRelations", name: "DashboardPodRelations",
meta: { meta: {
notShow: true, [META_KEYS.NOT_SHOW]: true,
}, },
children: [ children: [
{ {
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:destServiceId/:destPodId/:name", path: "/dashboard/:layerId/:entity/:serviceId/:podId/:destServiceId/:destPodId/:name",
component: Edit, component: Edit,
name: "ViewPodRelation", name: "DashboardViewPodRelation",
meta: {
[META_KEYS.TITLE]: "Pod Relations",
[META_KEYS.BREADCRUMB]: true,
},
}, },
{ {
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:destServiceId/:destPodId/:name/tab/:activeTabIndex", path: "/dashboard/:layerId/:entity/:serviceId/:podId/:destServiceId/:destPodId/:name/tab/:activeTabIndex",
component: Edit, component: Edit,
name: "ViewPodRelationActiveTabIndex", name: "DashboardViewPodRelationActiveTabIndex",
meta: {
[META_KEYS.TITLE]: "Pod Relations",
[META_KEYS.BREADCRUMB]: true,
},
}, },
], ],
}, },
// Process Relations Routes
{ {
path: "", path: "",
redirect: redirect:
"/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name", "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name",
component: Edit, component: Edit,
name: "ProcessRelations", name: "DashboardProcessRelations",
meta: { meta: {
notShow: true, [META_KEYS.NOT_SHOW]: true,
}, },
children: [ children: [
{ {
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name", path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name",
component: Edit, component: Edit,
name: "ViewProcessRelation", name: "DashboardViewProcessRelation",
meta: {
[META_KEYS.TITLE]: "Process Relations",
[META_KEYS.BREADCRUMB]: true,
},
}, },
{ {
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name/tab/:activeTabIndex", path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name/tab/:activeTabIndex",
component: Edit, component: Edit,
name: "ViewProcessRelationActiveTabIndex", name: "DashboardViewProcessRelationActiveTabIndex",
meta: {
[META_KEYS.TITLE]: "Process Relations",
[META_KEYS.BREADCRUMB]: true,
},
}, },
{ {
path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name/duration/:duration", path: "/dashboard/:layerId/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:name/duration/:duration",
component: Edit, component: Edit,
name: "ViewProcessRelationDuration", name: "DashboardViewProcessRelationDuration",
meta: {
[META_KEYS.TITLE]: "Process Relations",
[META_KEYS.BREADCRUMB]: true,
},
}, },
], ],
}, },
// Widget Routes
{ {
path: "", path: "",
name: "Widget", name: "DashboardWidget",
component: Widget, component: Widget,
meta: { meta: {
notShow: true, [META_KEYS.NOT_SHOW]: true,
}, },
children: [ children: [
{ {
path: "/page/:layer/:entity/:serviceId/:podId/:processId/:destServiceId/:destPodId/:destProcessId/:config/:duration?", path: ROUTE_PATHS.DASHBOARD.WIDGET,
component: Widget, component: Widget,
name: "ViewWidget", name: "DashboardViewWidget",
meta: {
[META_KEYS.TITLE]: "Dashboard Widget",
[META_KEYS.BREADCRUMB]: true,
},
}, },
], ],
}, },

98
src/router/guards.ts Normal file
View File

@@ -0,0 +1,98 @@
/**
* 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 { getDefaultRoute } from "./utils";
import { ROUTE_PATHS } from "./constants";
/**
* Global navigation guard for handling root path redirects
*/
export function createRootGuard(routes: any[]) {
return function rootGuard(to: any, from: any, next: any) {
if (to.path === ROUTE_PATHS.ROOT) {
const defaultPath = getDefaultRoute(routes);
next({ path: defaultPath });
} else {
next();
}
};
}
/**
* Authentication guard (placeholder for future implementation)
*/
export function createAuthGuard() {
return function authGuard(to: any, from: any, next: any) {
// TODO: Implement authentication logic
// const token = window.localStorage.getItem("skywalking-authority");
// if (to.meta?.requiresAuth && !token) {
// next('/login');
// return;
// }
next();
};
}
/**
* Route validation guard
*/
export function createValidationGuard() {
return function validationGuard(to: any, from: any, next: any) {
// Validate route parameters if needed
if (to.params && Object.keys(to.params).length > 0) {
// Add custom validation logic here
const hasValidParams = Object.values(to.params).every(
(param) => param !== undefined && param !== null && param !== "",
);
if (!hasValidParams) {
next({ name: "NotFound" });
return;
}
}
next();
};
}
/**
* Error handling guard
*/
export function createErrorGuard() {
return function errorGuard(error: any) {
console.error("Router error:", error);
// Handle specific error types
if (error.name === "NavigationDuplicated") {
// Ignore duplicate navigation errors
return;
}
// Redirect to error page or handle other errors
throw error;
};
}
/**
* Apply all navigation guards
*/
export function applyGuards(router: any, routes: any[]) {
router.beforeEach(createRootGuard(routes));
router.beforeEach(createAuthGuard());
router.beforeEach(createValidationGuard());
router.onError(createErrorGuard());
}

View File

@@ -14,8 +14,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { RouteRecordRaw } from "vue-router"; import type { AppRouteRecordRaw } from "@/types/router";
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import { applyGuards } from "./guards";
import { routesDashboard } from "./dashboard"; import { routesDashboard } from "./dashboard";
import { routesMarketplace } from "./marketplace"; import { routesMarketplace } from "./marketplace";
import { routesAlarm } from "./alarm"; import { routesAlarm } from "./alarm";
@@ -23,7 +24,10 @@ import routesLayers from "./layer";
import { routesSettings } from "./settings"; import { routesSettings } from "./settings";
import { routesNotFound } from "./notFound"; import { routesNotFound } from "./notFound";
const routes: RouteRecordRaw[] = [ /**
* Combine all route configurations
*/
export const routes: AppRouteRecordRaw[] = [
...routesMarketplace, ...routesMarketplace,
...routesLayers, ...routesLayers,
...routesAlarm, ...routesAlarm,
@@ -32,36 +36,17 @@ const routes: RouteRecordRaw[] = [
...routesNotFound, ...routesNotFound,
]; ];
/**
* Create router instance
*/
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes, routes: routes as any,
}); });
router.beforeEach((to, _, next) => { /**
// const token = window.localStorage.getItem("skywalking-authority"); * Apply navigation guards
*/
if (to.path === "/") { applyGuards(router, routes);
let defaultPath = "";
for (const route of routesLayers) {
for (const child of route.children) {
if (child.meta.activate) {
defaultPath = child.path;
break;
}
}
if (defaultPath) {
break;
}
}
if (!defaultPath) {
defaultPath = "/marketplace";
}
next({ path: defaultPath });
} else {
next();
}
});
export default router; export default router;

View File

@@ -14,74 +14,93 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { AppRouteRecordRaw } from "@/types/router";
import { META_KEYS } from "./constants";
import Layout from "@/layout/Index.vue"; import Layout from "@/layout/Index.vue";
import { useAppStoreWithOut } from "@/store/modules/app"; import { useAppStoreWithOut } from "@/store/modules/app";
import type { MenuOptions } from "@/types/app"; import type { MenuOptions } from "@/types/app";
import Layer from "@/views/Layer.vue"; import Layer from "@/views/Layer.vue";
function layerDashboards() { /**
* Generate layer dashboard routes from app store menu configuration
*/
function generateLayerDashboards(): AppRouteRecordRaw[] {
const appStore = useAppStoreWithOut(); const appStore = useAppStoreWithOut();
const routes = appStore.allMenus.map((item: MenuOptions) => {
const route: any = { return appStore.allMenus.map((item: MenuOptions): AppRouteRecordRaw => {
const route: AppRouteRecordRaw = {
path: "", path: "",
name: item.name, name: item.name,
component: Layout, component: Layout,
meta: { meta: {
icon: item.icon || "cloud_queue", [META_KEYS.ICON]: item.icon || "cloud_queue",
title: item.title, [META_KEYS.TITLE]: item.title,
hasGroup: item.hasGroup, [META_KEYS.HAS_GROUP]: item.hasGroup,
activate: item.activate, [META_KEYS.ACTIVATE]: item.activate,
descKey: item.descKey, [META_KEYS.DESC_KEY]: item.descKey,
i18nKey: item.i18nKey, [META_KEYS.I18N_KEY]: item.i18nKey,
[META_KEYS.BREADCRUMB]: true,
}, },
children: item.subItems && item.subItems.length ? [] : undefined, children: item.subItems && item.subItems.length ? [] : undefined,
}; };
for (const child of item.subItems || []) {
const d = { // Handle grouped items
name: child.name, if (item.subItems && item.subItems.length) {
path: child.path, for (const child of item.subItems) {
meta: { const childRoute: AppRouteRecordRaw = {
title: child.title, name: child.name,
layer: child.layer, path: child.path || "",
icon: child.icon || "cloud_queue", meta: {
activate: child.activate, [META_KEYS.TITLE]: child.title,
descKey: child.descKey, [META_KEYS.LAYER]: child.layer,
i18nKey: child.i18nKey, [META_KEYS.ICON]: child.icon || "cloud_queue",
}, [META_KEYS.ACTIVATE]: child.activate,
component: Layer, [META_KEYS.DESC_KEY]: child.descKey,
}; [META_KEYS.I18N_KEY]: child.i18nKey,
route.children.push(d); [META_KEYS.BREADCRUMB]: true,
const tab = { },
name: `${child.name}ActiveTabIndex`, component: Layer,
path: `/${child.path}/tab/:activeTabIndex`, };
component: Layer,
meta: { route.children!.push(childRoute);
notShow: true,
layer: child.layer, // Add tab route for active tab index
}, const tabRoute: AppRouteRecordRaw = {
}; name: `${child.name}ActiveTabIndex`,
route.children.push(tab); path: `/${child.path}/tab/:activeTabIndex`,
} component: Layer,
if (!item.hasGroup) { meta: {
[META_KEYS.NOT_SHOW]: true,
[META_KEYS.LAYER]: child.layer,
[META_KEYS.TITLE]: child.title,
[META_KEYS.BREADCRUMB]: false,
},
};
route.children!.push(tabRoute);
}
} else {
// Handle non-grouped items
route.children = [ route.children = [
{ {
name: item.name, name: item.name,
path: item.path, path: item.path || "",
meta: { meta: {
title: item.title, [META_KEYS.TITLE]: item.title,
layer: item.layer, [META_KEYS.LAYER]: item.layer,
icon: item.icon, [META_KEYS.ICON]: item.icon,
activate: item.activate, [META_KEYS.ACTIVATE]: item.activate,
descKey: item.descKey, [META_KEYS.DESC_KEY]: item.descKey,
i18nKey: item.i18nKey, [META_KEYS.I18N_KEY]: item.i18nKey,
[META_KEYS.BREADCRUMB]: true,
}, },
component: Layer, component: Layer,
}, },
]; ];
} }
return route; return route;
}); });
return routes;
} }
export default layerDashboards(); export default generateLayerDashboards();

View File

@@ -14,27 +14,33 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { RouteRecordRaw } from "vue-router"; import type { AppRouteRecordRaw } from "@/types/router";
import { ROUTE_NAMES, ROUTE_PATHS, META_KEYS } from "./constants";
import Layout from "@/layout/Index.vue"; import Layout from "@/layout/Index.vue";
import Marketplace from "@/views/Marketplace.vue"; import Marketplace from "@/views/Marketplace.vue";
export const routesMarketplace: Array<RouteRecordRaw> = [ export const routesMarketplace: AppRouteRecordRaw[] = [
{ {
path: "", path: "",
name: "Marketplace", name: ROUTE_NAMES.MARKETPLACE,
meta: { meta: {
i18nKey: "marketplace", [META_KEYS.I18N_KEY]: "marketplace",
icon: "marketplace", [META_KEYS.ICON]: "marketplace",
hasGroup: false, [META_KEYS.HAS_GROUP]: false,
activate: true, [META_KEYS.ACTIVATE]: true,
title: "Marketplace", [META_KEYS.TITLE]: "Marketplace",
[META_KEYS.BREADCRUMB]: true,
}, },
component: Layout, component: Layout,
children: [ children: [
{ {
path: "/marketplace", path: ROUTE_PATHS.MARKETPLACE,
name: "MenusManagement", name: "MenusManagement",
component: Marketplace, component: Marketplace,
meta: {
[META_KEYS.TITLE]: "Marketplace",
[META_KEYS.BREADCRUMB]: true,
},
}, },
], ],
}, },

View File

@@ -14,13 +14,18 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { RouteRecordRaw } from "vue-router"; import type { AppRouteRecordRaw } from "@/types/router";
import { ROUTE_NAMES, ROUTE_PATHS } from "./constants";
import NotFound from "@/views/NotFound.vue"; import NotFound from "@/views/NotFound.vue";
export const routesNotFound: Array<RouteRecordRaw> = [ export const routesNotFound: AppRouteRecordRaw[] = [
{ {
path: "/:pathMatch(.*)*", path: ROUTE_PATHS.NOT_FOUND,
name: "NotFound", name: ROUTE_NAMES.NOT_FOUND,
component: NotFound, component: NotFound,
meta: {
title: "Page Not Found",
notShow: true,
},
}, },
]; ];

View File

@@ -14,27 +14,33 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import type { RouteRecordRaw } from "vue-router"; import type { AppRouteRecordRaw } from "@/types/router";
import { ROUTE_NAMES, ROUTE_PATHS, META_KEYS } from "./constants";
import Layout from "@/layout/Index.vue"; import Layout from "@/layout/Index.vue";
import Settings from "@/views/Settings.vue"; import Settings from "@/views/Settings.vue";
export const routesSettings: Array<RouteRecordRaw> = [ export const routesSettings: AppRouteRecordRaw[] = [
{ {
path: "", path: "",
name: "Settings", name: ROUTE_NAMES.SETTINGS,
meta: { meta: {
i18nKey: "settings", [META_KEYS.I18N_KEY]: "settings",
icon: "settings", [META_KEYS.ICON]: "settings",
hasGroup: false, [META_KEYS.HAS_GROUP]: false,
activate: true, [META_KEYS.ACTIVATE]: true,
title: "Settings", [META_KEYS.TITLE]: "Settings",
[META_KEYS.BREADCRUMB]: true,
}, },
component: Layout, component: Layout,
children: [ children: [
{ {
path: "/settings", path: ROUTE_PATHS.SETTINGS,
name: "ViewSettings", name: "ViewSettings",
component: Settings, component: Settings,
meta: {
[META_KEYS.TITLE]: "Settings",
[META_KEYS.BREADCRUMB]: true,
},
}, },
], ],
}, },

102
src/router/utils.ts Normal file
View File

@@ -0,0 +1,102 @@
/**
* 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 { DEFAULT_ROUTE } from "./constants";
/**
* Find the first activated route from a list of routes
*/
export function findActivatedRoute(routes: AppRouteRecordRaw[]): string | null {
for (const route of routes) {
if (route.children) {
for (const child of route.children) {
if (child.meta?.activate) {
return child.path;
}
}
}
}
return null;
}
/**
* Get default route path
*/
export function getDefaultRoute(routes: AppRouteRecordRaw[]): string {
const activatedRoute = findActivatedRoute(routes);
return activatedRoute || DEFAULT_ROUTE;
}
/**
* Check if route requires authentication
*/
export function requiresAuth(route: AppRouteRecordRaw): boolean {
return route.meta?.requiresAuth === true;
}
/**
* Generate breadcrumb data from route
*/
export function generateBreadcrumb(route: AppRouteRecordRaw): string[] {
const breadcrumbs: string[] = [];
if (route.meta?.title) {
breadcrumbs.push(route.meta.title);
}
if (route.children) {
route.children.forEach((child) => {
if (child.meta?.breadcrumb !== false && child.meta?.title) {
breadcrumbs.push(child.meta.title);
}
});
}
return breadcrumbs;
}
/**
* Validate route configuration
*/
export function validateRoute(route: AppRouteRecordRaw): boolean {
if (!route.path || !route.name || !route.component) {
return false;
}
if (route.children) {
return route.children.every((child) => validateRoute(child));
}
return true;
}
/**
* Flatten nested routes for easier processing
*/
export function flattenRoutes(routes: AppRouteRecordRaw[]): AppRouteRecordRaw[] {
const flattened: AppRouteRecordRaw[] = [];
routes.forEach((route) => {
flattened.push(route);
if (route.children) {
flattened.push(...flattenRoutes(route.children));
}
});
return flattened;
}

50
src/types/router.ts Normal file
View File

@@ -0,0 +1,50 @@
/**
* 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 { RouteRecordRaw } from "vue-router";
export interface RouteMeta {
title?: string;
i18nKey?: string;
icon?: string;
hasGroup?: boolean;
activate?: boolean;
descKey?: string;
layer?: string;
notShow?: boolean;
requiresAuth?: boolean;
breadcrumb?: boolean;
}
export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, "meta" | "children"> {
meta: RouteMeta;
children?: AppRouteRecordRaw[];
}
export interface RouteConfig {
path: string;
name: string;
component: any;
meta: RouteMeta;
children?: RouteConfig[];
}
export interface NavigationGuard {
to: any;
from: any;
next: any;
}