diff --git a/webui/package.json b/webui/package.json index 3721a8ae1..d2972a97c 100644 --- a/webui/package.json +++ b/webui/package.json @@ -43,6 +43,8 @@ "type": "module", "dependencies": { "@eslint/js": "^9.32.0", + "@noble/ed25519": "^3.0.0", + "@noble/hashes": "^2.0.1", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", @@ -56,6 +58,7 @@ "@typescript-eslint/parser": "^8.38.0", "@vitejs/plugin-react": "^4.7.0", "@vitest/coverage-v8": "^3.2.4", + "@vitest/web-worker": "^4.0.2", "chart.js": "^4.4.1", "eslint": "^9.32.0", "eslint-config-prettier": "^10.1.8", @@ -85,7 +88,7 @@ "usehooks-ts": "^2.14.0", "vite": "^5.4.19", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.2.4", + "vitest": "^4.0.3", "vitest-canvas-mock": "^0.3.3" }, "devDependencies": { diff --git a/webui/public/img/gopher-something-went-wrong.png b/webui/public/img/gopher-something-went-wrong.png new file mode 100644 index 000000000..fb0d6d4c3 Binary files /dev/null and b/webui/public/img/gopher-something-went-wrong.png differ diff --git a/webui/src/App.tsx b/webui/src/App.tsx index b3f536ef2..8e256437f 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -1,5 +1,5 @@ import { globalCss, Box, darkTheme, FaencyProvider, lightTheme } from '@traefiklabs/faency' -import { Suspense, useEffect } from 'react' +import { Suspense, useContext, useEffect } from 'react' import { HelmetProvider } from 'react-helmet-async' import { HashRouter, Navigate, Route, Routes as RouterRoutes, useLocation } from 'react-router-dom' import { SWRConfig } from 'swr' @@ -12,6 +12,7 @@ import { useIsDarkMode } from 'hooks/use-theme' import ErrorSuspenseWrapper from 'layout/ErrorSuspenseWrapper' import { Dashboard, HTTPPages, NotFound, TCPPages, UDPPages } from 'pages' import { DashboardSkeleton } from 'pages/dashboard/Dashboard' +import { HubDemoContext, HubDemoProvider } from 'pages/hub-demo/demoNavContext' export const LIGHT_THEME = lightTheme('blue') export const DARK_THEME = darkTheme('blue') @@ -33,47 +34,56 @@ const ScrollToTop = () => { } export const Routes = () => { + const { routes: hubDemoRoutes } = useContext(HubDemoContext) + return ( - }> - - }> - - - } - /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + + }> + + }> + + + } + /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Hub Dashboard demo content */} + {hubDemoRoutes?.map((route, idx) => )} + + } /> + + + ) } const isDev = import.meta.env.NODE_ENV === 'development' const customGlobalStyle = globalCss({ - 'span[role=cell]': { // target the AriaTd component - p: '$2 $3' + // target the AriaTd component, but exclude anything inside hub-ui-demo-app + 'body:not(:has(hub-ui-demo-app)) span[role=cell]': { + p: '$2 $3', }, }) @@ -101,9 +111,11 @@ const App = () => { > - {customGlobalStyle()} - - + + {customGlobalStyle()} + + + diff --git a/webui/src/components/SpinnerLoader.tsx b/webui/src/components/SpinnerLoader.tsx index 663130307..e336fc436 100644 --- a/webui/src/components/SpinnerLoader.tsx +++ b/webui/src/components/SpinnerLoader.tsx @@ -2,17 +2,17 @@ import { Flex } from '@traefiklabs/faency' import { motion } from 'framer-motion' import { FiLoader } from 'react-icons/fi' -export const SpinnerLoader = () => ( +export const SpinnerLoader = ({ size = 24 }: { size?: number }) => ( - + ) diff --git a/webui/src/components/icons/providers/Knative.tsx b/webui/src/components/icons/providers/Knative.tsx index e6adf46e2..8eaf21d50 100644 --- a/webui/src/components/icons/providers/Knative.tsx +++ b/webui/src/components/icons/providers/Knative.tsx @@ -2,10 +2,12 @@ import { ProviderIconProps } from 'components/icons/providers' export default function Knative(props: ProviderIconProps) { return ( - - + + ) } diff --git a/webui/src/components/icons/providers/index.tsx b/webui/src/components/icons/providers/index.tsx index cd953d1b0..8745a7644 100644 --- a/webui/src/components/icons/providers/index.tsx +++ b/webui/src/components/icons/providers/index.tsx @@ -8,7 +8,7 @@ import File from 'components/icons/providers/File' import Http from 'components/icons/providers/Http' import Hub from 'components/icons/providers/Hub' import Internal from 'components/icons/providers/Internal' -import Knative from "components/icons/providers/Knative"; +import Knative from 'components/icons/providers/Knative' import Kubernetes from 'components/icons/providers/Kubernetes' import Nomad from 'components/icons/providers/Nomad' import Plugin from 'components/icons/providers/Plugin' diff --git a/webui/src/layout/Navigation.tsx b/webui/src/layout/Navigation.tsx index 62b29eedf..658ef2257 100644 --- a/webui/src/layout/Navigation.tsx +++ b/webui/src/layout/Navigation.tsx @@ -2,6 +2,7 @@ import { Badge, Box, Button, + CSS, DialogTitle, DropdownMenu, DropdownMenuContent, @@ -37,6 +38,7 @@ import TooltipText from 'components/TooltipText' import { VersionContext } from 'contexts/version' import useTotals from 'hooks/use-overview-totals' import { useIsDarkMode } from 'hooks/use-theme' +import ApimDemoNavMenu from 'pages/hub-demo/HubDemoNav' import { Route, ROUTES } from 'routes' export const LAPTOP_BP = 1025 @@ -54,7 +56,7 @@ const NavigationDrawer = styled(Flex, { }, }) -const BasicNavigationItem = ({ +export const BasicNavigationItem = ({ route, count, isSmallScreen, @@ -270,7 +272,7 @@ export const SideNav = ({ ))} ))} - + } css={{ @@ -283,12 +285,14 @@ export const SideNav = ({ {!isSmallScreen || isExpanded ? 'Plugins' : ''} + + ) } -export const TopNav = () => { +export const TopNav = ({ css, noHubButton = false }: { css?: CSS; noHubButton?: boolean }) => { const [hasHubButtonComponent, setHasHubButtonComponent] = useState(false) const { showHubButton, version } = useContext(VersionContext) const isDarkMode = useIsDarkMode() @@ -341,8 +345,8 @@ export const TopNav = () => { }, [showHubButton]) return ( - - {hasHubButtonComponent && ( + + {!noHubButton && hasHubButtonComponent && ( ', () => { it('should render an empty page', () => { - const { getByTestId } = renderWithProviders() - expect(getByTestId('Test page')).toBeInTheDocument() + const { getByTestId } = renderWithProviders(, { route: '/test' }) + expect(getByTestId('/test page')).toBeInTheDocument() }) }) diff --git a/webui/src/layout/Page.tsx b/webui/src/layout/Page.tsx index f11afa091..ec8b6ea81 100644 --- a/webui/src/layout/Page.tsx +++ b/webui/src/layout/Page.tsx @@ -1,6 +1,7 @@ import { Flex, globalCss, styled } from '@traefiklabs/faency' -import { ReactNode, useState } from 'react' +import { ReactNode, useMemo, useState } from 'react' import { Helmet } from 'react-helmet-async' +import { useLocation } from 'react-router-dom' import Container from './Container' import { LAPTOP_BP, SideBarPanel, SideNav, TopNav } from './Navigation' @@ -40,14 +41,31 @@ export interface Props { children?: ReactNode } -const Page = ({ children, title }: Props) => { +const Page = ({ children }: Props) => { + const { pathname } = useLocation() const [isSideBarPanelOpen, setIsSideBarPanelOpen] = useState(false) + const location = useLocation() + + const isDemoPage = useMemo(() => pathname.includes('hub-dashboard'), [pathname]) + + const renderedContent = useMemo(() => { + if (isDemoPage) { + return children + } + + return ( + + + {children} + + ) + }, [children, isDemoPage, location.pathname]) return ( {globalStyles()} - {title ? `${title} - ` : ''}Traefik Proxy + Traefik Proxy @@ -56,10 +74,7 @@ const Page = ({ children, title }: Props) => { justify="center" css={{ flex: 1, margin: 'auto', ml: 264, [`@media (max-width:${LAPTOP_BP}px)`]: { ml: 60 } }} > - - - {children} - + {renderedContent} diff --git a/webui/src/pages/NotFound.tsx b/webui/src/pages/NotFound.tsx index 51a130c4c..486b0b217 100644 --- a/webui/src/pages/NotFound.tsx +++ b/webui/src/pages/NotFound.tsx @@ -1,24 +1,24 @@ import { Box, Button, Flex, H1, Text } from '@traefiklabs/faency' +import { Helmet } from 'react-helmet-async' import { useNavigate } from 'react-router-dom' -import Page from 'layout/Page' - export const NotFound = () => { const navigate = useNavigate() return ( - - - -

404

- - - I'm sorry, nothing around here... - - - - + + + Not found - Traefik Proxy + + +

404

+
+ + I'm sorry, nothing around here... + + +
) } diff --git a/webui/src/pages/dashboard/Dashboard.tsx b/webui/src/pages/dashboard/Dashboard.tsx index 9100823eb..b26cf0a9c 100644 --- a/webui/src/pages/dashboard/Dashboard.tsx +++ b/webui/src/pages/dashboard/Dashboard.tsx @@ -1,12 +1,12 @@ import { Card, CSS, Flex, Grid, H2, Text } from '@traefiklabs/faency' import { ReactNode, useMemo } from 'react' +import { Helmet } from 'react-helmet-async' import useSWR from 'swr' import ProviderIcon from 'components/icons/providers' import FeatureCard, { FeatureCardSkeleton } from 'components/resources/FeatureCard' import ResourceCard from 'components/resources/ResourceCard' import TraefikResourceStatsCard, { StatsCardSkeleton } from 'components/resources/TraefikResourceStatsCard' -import Page from 'layout/Page' import { capitalizeFirstLetter } from 'utils/string' const RESOURCES = ['routers', 'services', 'middlewares'] @@ -76,159 +76,158 @@ export const Dashboard = () => { } return ( - - - - {entrypoints?.map((i, idx) => ( - - {i.address} - - ))} - + + + Dashboard - Traefik Proxy + + + {entrypoints?.map((i, idx) => ( + + {i.address} + + ))} + - - {overview?.http && hasResources.http ? ( - RESOURCES.map((i) => ( - - )) - ) : ( - No related objects to show. - )} - + + {overview?.http && hasResources.http ? ( + RESOURCES.map((i) => ( + + )) + ) : ( + No related objects to show. + )} + - - {overview?.tcp && hasResources.tcp ? ( - RESOURCES.map((i) => ( - - )) - ) : ( - No related objects to show. - )} - + + {overview?.tcp && hasResources.tcp ? ( + RESOURCES.map((i) => ( + + )) + ) : ( + No related objects to show. + )} + - - {overview?.udp && hasResources.udp ? ( - RESOURCES.map((i) => ( - - )) - ) : ( - No related objects to show. - )} - + + {overview?.udp && hasResources.udp ? ( + RESOURCES.map((i) => ( + + )) + ) : ( + No related objects to show. + )} + - - {features.length - ? features.map((i, idx) => { - return - }) - : null} - + + {features.length + ? features.map((i, idx) => { + return + }) + : null} + - - {overview?.providers?.length ? ( - overview.providers.map((p, idx) => ( - - - - {p} - - - )) - ) : ( - No related objects to show. - )} - - - + + {overview?.providers?.length ? ( + overview.providers.map((p, idx) => ( + + + + {p} + + + )) + ) : ( + No related objects to show. + )} + + ) } export const DashboardSkeleton = () => { return ( - - - - {[...Array(5)].map((_, i) => ( - - ))} - + + + {[...Array(5)].map((_, i) => ( + + ))} + - - {[...Array(3)].map((_, i) => ( - - ))} - + + {[...Array(3)].map((_, i) => ( + + ))} + - - {[...Array(3)].map((_, i) => ( - - ))} - + + {[...Array(3)].map((_, i) => ( + + ))} + - - {[...Array(3)].map((_, i) => ( - - ))} - + + {[...Array(3)].map((_, i) => ( + + ))} + - - {[...Array(3)].map((_, i) => ( - - ))} - + + {[...Array(3)].map((_, i) => ( + + ))} + - - {[...Array(3)].map((_, i) => ( - - ))} - - - + + {[...Array(3)].map((_, i) => ( + + ))} + + ) } diff --git a/webui/src/pages/http/HttpMiddleware.spec.tsx b/webui/src/pages/http/HttpMiddleware.spec.tsx index d7a7c39bb..8bf003735 100644 --- a/webui/src/pages/http/HttpMiddleware.spec.tsx +++ b/webui/src/pages/http/HttpMiddleware.spec.tsx @@ -7,6 +7,7 @@ describe('', () => { it('should render the error message', () => { const { getByTestId } = renderWithProviders( , + { route: '/http/middlewares/mock-middleware', withPage: true }, ) expect(getByTestId('error-text')).toBeInTheDocument() }) @@ -14,6 +15,7 @@ describe('', () => { it('should render the skeleton', () => { const { getByTestId } = renderWithProviders( , + { route: '/http/middlewares/mock-middleware', withPage: true }, ) expect(getByTestId('skeleton')).toBeInTheDocument() }) @@ -21,6 +23,7 @@ describe('', () => { it('should render the not found page', () => { const { getByTestId } = renderWithProviders( , + { route: '/http/middlewares/mock-middleware', withPage: true }, ) expect(getByTestId('Not found page')).toBeInTheDocument() }) @@ -53,6 +56,7 @@ describe('', () => { const { container, getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/http/middlewares/middleware-simple', withPage: true }, ) const headings = Array.from(container.getElementsByTagName('h1')) @@ -99,6 +103,7 @@ describe('', () => { const { container, getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/http/middlewares/middleware-plugin', withPage: true }, ) const headings = Array.from(container.getElementsByTagName('h1')) @@ -338,6 +343,7 @@ describe('', () => { const { container, getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/http/middlewares/middleware-complex', withPage: true }, ) const headings = Array.from(container.getElementsByTagName('h1')) @@ -459,6 +465,7 @@ describe('', () => { const { container, getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/http/middlewares/middleware-plugin-no-type', withPage: true }, ) const headings = Array.from(container.getElementsByTagName('h1')) diff --git a/webui/src/pages/http/HttpMiddleware.tsx b/webui/src/pages/http/HttpMiddleware.tsx index 4b2fe7e2a..d762e6975 100644 --- a/webui/src/pages/http/HttpMiddleware.tsx +++ b/webui/src/pages/http/HttpMiddleware.tsx @@ -1,11 +1,11 @@ import { Box, Card, H1, Skeleton, styled, Text } from '@traefiklabs/faency' +import { Helmet } from 'react-helmet-async' import { useParams } from 'react-router-dom' import { DetailSectionSkeleton } from 'components/resources/DetailSections' import { RenderMiddleware } from 'components/resources/MiddlewarePanel' import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection' import { ResourceDetailDataType, useResourceDetail } from 'hooks/use-resource-detail' -import Page from 'layout/Page' import { NotFound } from 'pages/NotFound' import breakpoints from 'utils/breakpoints' @@ -27,23 +27,29 @@ type HttpMiddlewareRenderProps = { export const HttpMiddlewareRender = ({ data, error, name }: HttpMiddlewareRenderProps) => { if (error) { return ( - + <> + + {name} - Traefik Proxy + Sorry, we could not fetch detail information for this Middleware right now. Please, try again later. - + ) } if (!data) { return ( - + <> + + {name} - Traefik Proxy + - + ) } @@ -52,7 +58,10 @@ export const HttpMiddlewareRender = ({ data, error, name }: HttpMiddlewareRender } return ( - + <> + + {data.name} - Traefik Proxy +

{data.name}

@@ -60,7 +69,7 @@ export const HttpMiddlewareRender = ({ data, error, name }: HttpMiddlewareRender -
+ ) } diff --git a/webui/src/pages/http/HttpMiddlewares.spec.tsx b/webui/src/pages/http/HttpMiddlewares.spec.tsx index 1f4ce2607..b49b42062 100644 --- a/webui/src/pages/http/HttpMiddlewares.spec.tsx +++ b/webui/src/pages/http/HttpMiddlewares.spec.tsx @@ -76,10 +76,13 @@ describe('', () => { .spyOn(useFetchWithPagination, 'default') .mockImplementation(() => useFetchWithPaginationMock({ pages })) - const { container, getByTestId } = renderWithProviders() + const { container, getByTestId } = renderWithProviders(, { + route: '/http/middlewares', + withPage: true, + }) expect(mock).toHaveBeenCalled() - expect(getByTestId('HTTP Middlewares page')).toBeInTheDocument() + expect(getByTestId('/http/middlewares page')).toBeInTheDocument() const tbody = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[1] expect(tbody.querySelectorAll('a[role="row"]')).toHaveLength(5) @@ -120,6 +123,7 @@ describe('', () => { pageCount={1} pages={[]} />, + { route: '/http/middlewares', withPage: true }, ) expect(() => getByTestId('loading')).toThrow('Unable to find an element by: [data-testid="loading"]') const tfoot = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[2] diff --git a/webui/src/pages/http/HttpMiddlewares.tsx b/webui/src/pages/http/HttpMiddlewares.tsx index f61a4ed6b..e3274009e 100644 --- a/webui/src/pages/http/HttpMiddlewares.tsx +++ b/webui/src/pages/http/HttpMiddlewares.tsx @@ -1,5 +1,6 @@ import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex } from '@traefiklabs/faency' import { useMemo } from 'react' +import { Helmet } from 'react-helmet-async' import useInfiniteScroll from 'react-infinite-scroll-hook' import { useSearchParams } from 'react-router-dom' @@ -14,7 +15,6 @@ import Tooltip from 'components/Tooltip' import TooltipText from 'components/TooltipText' import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination' import { EmptyPlaceholder } from 'layout/EmptyPlaceholder' -import Page from 'layout/Page' import { parseMiddlewareType } from 'libs/parsers' export const makeRowRender = (): RenderRowType => { @@ -109,7 +109,10 @@ export const HttpMiddlewares = () => { ) return ( - + <> + + HTTP Middlewares - Traefik Proxy + { pageCount={pageCount} pages={pages} /> - + ) } diff --git a/webui/src/pages/http/HttpRouter.spec.tsx b/webui/src/pages/http/HttpRouter.spec.tsx index a7f1e3ad7..ca90455d9 100644 --- a/webui/src/pages/http/HttpRouter.spec.tsx +++ b/webui/src/pages/http/HttpRouter.spec.tsx @@ -10,6 +10,7 @@ describe('', () => { it('should render the error message', () => { const { getByTestId } = renderWithProviders( , + { route: '/http/routers/mock-router', withPage: true }, ) expect(getByTestId('error-text')).toBeInTheDocument() }) @@ -17,6 +18,7 @@ describe('', () => { it('should render the skeleton', () => { const { getByTestId } = renderWithProviders( , + { route: '/http/routers/mock-router', withPage: true }, ) expect(getByTestId('skeleton')).toBeInTheDocument() }) @@ -24,6 +26,7 @@ describe('', () => { it('should render the not found page', () => { const { getByTestId } = renderWithProviders( , + { route: '/http/routers/mock-router', withPage: true }, ) expect(getByTestId('Not found page')).toBeInTheDocument() }) @@ -40,6 +43,7 @@ describe('', () => { const { getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/http/routers/orphan-router@file', withPage: true }, ) const routerStructure = getByTestId('router-structure') diff --git a/webui/src/pages/http/HttpRouter.tsx b/webui/src/pages/http/HttpRouter.tsx index dbb493e4d..62801083a 100644 --- a/webui/src/pages/http/HttpRouter.tsx +++ b/webui/src/pages/http/HttpRouter.tsx @@ -1,5 +1,6 @@ import { Flex, styled, Text } from '@traefiklabs/faency' import { useContext, useEffect, useMemo } from 'react' +import { Helmet } from 'react-helmet-async' import { FiGlobe, FiLayers, FiLogIn, FiZap } from 'react-icons/fi' import { useParams } from 'react-router-dom' @@ -9,7 +10,6 @@ import RouterPanel from 'components/resources/RouterPanel' import TlsPanel from 'components/resources/TlsPanel' import { ToastContext } from 'contexts/toasts' import { EntryPoint, ResourceDetailDataType, useResourceDetail } from 'hooks/use-resource-detail' -import Page from 'layout/Page' import { getErrorData, getValidData } from 'libs/objectHandlers' import { parseMiddlewareType } from 'libs/parsers' import { NotFound } from 'pages/NotFound' @@ -105,17 +105,23 @@ type HttpRouterRenderProps = { export const HttpRouterRender = ({ data, error, name }: HttpRouterRenderProps) => { if (error) { return ( - + <> + + {name} - Traefik Proxy + Sorry, we could not fetch detail information for this Router right now. Please, try again later. - + ) } if (!data) { return ( - + <> + + {name} - Traefik Proxy + @@ -127,7 +133,7 @@ export const HttpRouterRender = ({ data, error, name }: HttpRouterRenderProps) = - + ) } @@ -136,10 +142,13 @@ export const HttpRouterRender = ({ data, error, name }: HttpRouterRenderProps) = } return ( - + <> + + {data.name} - Traefik Proxy + - + ) } diff --git a/webui/src/pages/http/HttpRouters.spec.tsx b/webui/src/pages/http/HttpRouters.spec.tsx index bdddd21b4..a4df44257 100644 --- a/webui/src/pages/http/HttpRouters.spec.tsx +++ b/webui/src/pages/http/HttpRouters.spec.tsx @@ -49,10 +49,13 @@ describe('', () => { .spyOn(useFetchWithPagination, 'default') .mockImplementation(() => useFetchWithPaginationMock({ pages })) - const { container, getByTestId } = renderWithProviders() + const { container, getByTestId } = renderWithProviders(, { + route: '/http/routers', + withPage: true, + }) expect(mock).toHaveBeenCalled() - expect(getByTestId('HTTP Routers page')).toBeInTheDocument() + expect(getByTestId('/http/routers page')).toBeInTheDocument() const tbody = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[1] expect(tbody.querySelectorAll('a[role="row"]')).toHaveLength(4) @@ -100,6 +103,7 @@ describe('', () => { pageCount={1} pages={[]} />, + { route: '/http/routers', withPage: true }, ) expect(() => getByTestId('loading')).toThrow('Unable to find an element by: [data-testid="loading"]') const tfoot = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[2] diff --git a/webui/src/pages/http/HttpRouters.tsx b/webui/src/pages/http/HttpRouters.tsx index d646b8f42..06c2c255f 100644 --- a/webui/src/pages/http/HttpRouters.tsx +++ b/webui/src/pages/http/HttpRouters.tsx @@ -1,5 +1,6 @@ import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex } from '@traefiklabs/faency' import { useMemo } from 'react' +import { Helmet } from 'react-helmet-async' import { FiShield } from 'react-icons/fi' import useInfiniteScroll from 'react-infinite-scroll-hook' import { useSearchParams } from 'react-router-dom' @@ -16,7 +17,6 @@ import Tooltip from 'components/Tooltip' import TooltipText from 'components/TooltipText' import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination' import { EmptyPlaceholder } from 'layout/EmptyPlaceholder' -import Page from 'layout/Page' export const makeRowRender = (protocol = 'http'): RenderRowType => { const HttpRoutersRenderRow = (row) => ( @@ -130,7 +130,10 @@ export const HttpRouters = () => { ) return ( - + <> + + HTTP Routers - Traefik Proxy + { pageCount={pageCount} pages={pages} /> - + ) } diff --git a/webui/src/pages/http/HttpService.spec.tsx b/webui/src/pages/http/HttpService.spec.tsx index 781d43099..76059e21d 100644 --- a/webui/src/pages/http/HttpService.spec.tsx +++ b/webui/src/pages/http/HttpService.spec.tsx @@ -7,6 +7,7 @@ describe('', () => { it('should render the error message', () => { const { getByTestId } = renderWithProviders( , + { route: '/http/services/mock-service', withPage: true }, ) expect(getByTestId('error-text')).toBeInTheDocument() }) @@ -14,6 +15,7 @@ describe('', () => { it('should render the skeleton', () => { const { getByTestId } = renderWithProviders( , + { route: '/http/services/mock-service', withPage: true }, ) expect(getByTestId('skeleton')).toBeInTheDocument() }) @@ -21,6 +23,7 @@ describe('', () => { it('should render the not found page', () => { const { getByTestId } = renderWithProviders( , + { route: '/http/services/mock-service', withPage: true }, ) expect(getByTestId('Not found page')).toBeInTheDocument() }) @@ -71,6 +74,7 @@ describe('', () => { const { container, getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/http/services/mock-service', withPage: true }, ) const headings = Array.from(container.getElementsByTagName('h1')) @@ -142,6 +146,7 @@ describe('', () => { const { getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/http/services/mock-service', withPage: true }, ) const healthCheck = getByTestId('health-check') @@ -196,6 +201,7 @@ describe('', () => { const { getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/http/services/mock-service', withPage: true }, ) const mirrorServices = getByTestId('mirror-services') diff --git a/webui/src/pages/http/HttpService.tsx b/webui/src/pages/http/HttpService.tsx index 4e74c552f..1761cbf05 100644 --- a/webui/src/pages/http/HttpService.tsx +++ b/webui/src/pages/http/HttpService.tsx @@ -1,5 +1,6 @@ import { Badge, Box, Flex, H1, Skeleton, styled, Text } from '@traefiklabs/faency' import { useMemo } from 'react' +import { Helmet } from 'react-helmet-async' import { FiGlobe, FiInfo, FiShield } from 'react-icons/fi' import { useParams } from 'react-router-dom' @@ -18,7 +19,6 @@ import { ResourceStatus } from 'components/resources/ResourceStatus' import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection' import Tooltip from 'components/Tooltip' import { ResourceDetailDataType, ServiceDetailType, useResourceDetail } from 'hooks/use-resource-detail' -import Page from 'layout/Page' import { NotFound } from 'pages/NotFound' type DetailProps = { @@ -270,17 +270,23 @@ type HttpServiceRenderProps = { export const HttpServiceRender = ({ data, error, name }: HttpServiceRenderProps) => { if (error) { return ( - + <> + + {name} - Traefik Proxy + Sorry, we could not fetch detail information for this Service right now. Please, try again later. - + ) } if (!data) { return ( - + <> + + {name} - Traefik Proxy + @@ -288,7 +294,7 @@ export const HttpServiceRender = ({ data, error, name }: HttpServiceRenderProps) - + ) } @@ -297,11 +303,14 @@ export const HttpServiceRender = ({ data, error, name }: HttpServiceRenderProps) } return ( - + <> + + {data.name} - Traefik Proxy +

{data.name}

-
+ ) } diff --git a/webui/src/pages/http/HttpServices.spec.tsx b/webui/src/pages/http/HttpServices.spec.tsx index 720fc2549..31a749c8c 100644 --- a/webui/src/pages/http/HttpServices.spec.tsx +++ b/webui/src/pages/http/HttpServices.spec.tsx @@ -49,10 +49,13 @@ describe('', () => { .spyOn(useFetchWithPagination, 'default') .mockImplementation(() => useFetchWithPaginationMock({ pages })) - const { container, getByTestId } = renderWithProviders() + const { container, getByTestId } = renderWithProviders(, { + route: '/http/services', + withPage: true, + }) expect(mock).toHaveBeenCalled() - expect(getByTestId('HTTP Services page')).toBeInTheDocument() + expect(getByTestId('/http/services page')).toBeInTheDocument() const tbody = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[1] expect(tbody.querySelectorAll('a[role="row"]')).toHaveLength(4) @@ -92,6 +95,7 @@ describe('', () => { pageCount={1} pages={[]} />, + { route: '/http/services', withPage: true }, ) expect(() => getByTestId('loading')).toThrow('Unable to find an element by: [data-testid="loading"]') const tfoot = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[2] diff --git a/webui/src/pages/http/HttpServices.tsx b/webui/src/pages/http/HttpServices.tsx index 6febd6b1f..8c41badcd 100644 --- a/webui/src/pages/http/HttpServices.tsx +++ b/webui/src/pages/http/HttpServices.tsx @@ -1,5 +1,6 @@ import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex, Text } from '@traefiklabs/faency' import { useMemo } from 'react' +import { Helmet } from 'react-helmet-async' import useInfiniteScroll from 'react-infinite-scroll-hook' import { useSearchParams } from 'react-router-dom' @@ -14,7 +15,6 @@ import Tooltip from 'components/Tooltip' import TooltipText from 'components/TooltipText' import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination' import { EmptyPlaceholder } from 'layout/EmptyPlaceholder' -import Page from 'layout/Page' export const makeRowRender = (): RenderRowType => { const HttpServicesRenderRow = (row) => ( @@ -108,7 +108,10 @@ export const HttpServices = () => { ) return ( - + <> + + HTTP Services - Traefik Proxy + { pageCount={pageCount} pages={pages} /> - + ) } diff --git a/webui/src/pages/hub-demo/HubDashboard.spec.tsx b/webui/src/pages/hub-demo/HubDashboard.spec.tsx new file mode 100644 index 000000000..7f338ab69 --- /dev/null +++ b/webui/src/pages/hub-demo/HubDashboard.spec.tsx @@ -0,0 +1,204 @@ +import { waitFor } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +import HubDashboard, { resetCache } from './HubDashboard' +import verifySignature from './workers/scriptVerification' + +import { renderWithProviders } from 'utils/test' + +vi.mock('./workers/scriptVerification', () => ({ + default: vi.fn(), +})) + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom') + return { + ...actual, + useParams: vi.fn(() => ({ id: 'test-id' })), + } +}) + +vi.mock('hooks/use-theme', () => ({ + useIsDarkMode: vi.fn(() => false), + useTheme: vi.fn(() => ({ + selectedTheme: 'light', + appliedTheme: 'light', + setTheme: vi.fn(), + })), +})) + +describe('HubDashboard demo', () => { + const mockVerifyScriptSignature = vi.mocked(verifySignature) + let mockCreateObjectURL: ReturnType + + beforeEach(() => { + vi.clearAllMocks() + + // Mock URL.createObjectURL + mockCreateObjectURL = vi.fn(() => 'blob:mock-url') + globalThis.URL.createObjectURL = mockCreateObjectURL + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('without cache', () => { + beforeEach(() => { + // Reset cache before each test suites + resetCache() + }) + + it('should render loading state during script verification', async () => { + const mockScriptContent = new ArrayBuffer(100) + mockVerifyScriptSignature.mockImplementation( + () => + new Promise((resolve) => + setTimeout(() => resolve({ verified: true, scriptContent: mockScriptContent }), 100), + ), + ) + + const { getByTestId } = renderWithProviders(, { + route: '/hub-dashboard', + }) + + expect(getByTestId('loading')).toBeInTheDocument() + + await waitFor(() => { + expect(mockVerifyScriptSignature).toHaveBeenCalledTimes(1) + }) + }) + + it('should render the custom web component when signature is verified', async () => { + const mockScriptContent = new ArrayBuffer(100) + mockVerifyScriptSignature.mockResolvedValue({ verified: true, scriptContent: mockScriptContent }) + + const { container } = renderWithProviders(, { + route: '/hub-dashboard', + }) + + await waitFor(() => { + expect(mockVerifyScriptSignature).toHaveBeenCalledTimes(1) + }) + + const hubComponent = container.querySelector('hub-ui-demo-app') + expect(hubComponent).toBeInTheDocument() + expect(hubComponent?.getAttribute('path')).toBe('dashboard') + expect(hubComponent?.getAttribute('baseurl')).toBe('#/hub-dashboard') + expect(hubComponent?.getAttribute('theme')).toBe('light') + }) + + it('should render error state when signature verification fails', async () => { + mockVerifyScriptSignature.mockResolvedValue({ verified: false }) + + const { container } = renderWithProviders() + + await waitFor(() => { + expect(mockVerifyScriptSignature).toHaveBeenCalledTimes(1) + }) + + expect(container.textContent).toContain("Oops! We couldn't load the demo content") + + const errorImage = container.querySelector('img[src="/img/gopher-something-went-wrong.png"]') + expect(errorImage).toBeInTheDocument() + + const links = container.querySelectorAll('a') + const websiteLink = Array.from(links).find((link) => link.href.includes('traefik.io/traefik-hub')) + const docLink = Array.from(links).find((link) => link.href.includes('doc.traefik.io/traefik-hub')) + + expect(websiteLink).toBeInTheDocument() + expect(docLink).toBeInTheDocument() + }) + + it('should render error state when verification throws an error', async () => { + mockVerifyScriptSignature.mockRejectedValue(new Error('Network error')) + + const { container } = renderWithProviders() + + await waitFor(() => { + expect(container.textContent).toContain("Oops! We couldn't load the demo content") + }) + }) + + it('should call verifyScriptSignature with correct parameters', async () => { + const mockScriptContent = new ArrayBuffer(100) + mockVerifyScriptSignature.mockResolvedValue({ verified: true, scriptContent: mockScriptContent }) + + renderWithProviders() + + await waitFor(() => { + expect(mockVerifyScriptSignature).toHaveBeenCalledWith( + 'https://assets.traefik.io/hub-ui-demo.js', + 'https://assets.traefik.io/hub-ui-demo.js.sig', + ) + }) + }) + + it('should set theme attribute based on dark mode', async () => { + const mockScriptContent = new ArrayBuffer(100) + mockVerifyScriptSignature.mockResolvedValue({ verified: true, scriptContent: mockScriptContent }) + + const { container } = renderWithProviders() + + await waitFor(() => { + expect(mockVerifyScriptSignature).toHaveBeenCalledTimes(1) + }) + + const hubComponent = container.querySelector('hub-ui-demo-app') + + expect(hubComponent?.getAttribute('theme')).toMatch(/light|dark/) + }) + + it('should handle path with :id parameter correctly', async () => { + const mockScriptContent = new ArrayBuffer(100) + mockVerifyScriptSignature.mockResolvedValue({ verified: true, scriptContent: mockScriptContent }) + + const { container } = renderWithProviders() + + await waitFor(() => { + expect(mockVerifyScriptSignature).toHaveBeenCalledTimes(1) + }) + + const hubComponent = container.querySelector('hub-ui-demo-app') + expect(hubComponent).toBeInTheDocument() + expect(hubComponent?.getAttribute('path')).toBe('gateways/test-id') + }) + }) + + describe('with cache', () => { + beforeEach(() => { + resetCache() + }) + + it('should use cached blob URL without calling verifySignature again', async () => { + const mockScriptContent = new ArrayBuffer(100) + mockVerifyScriptSignature.mockResolvedValue({ verified: true, scriptContent: mockScriptContent }) + + // First render + const { container: firstContainer, unmount: firstUnmount } = renderWithProviders( + , + ) + + await waitFor(() => { + expect(mockVerifyScriptSignature).toHaveBeenCalledTimes(1) + }) + + const firstHubComponent = firstContainer.querySelector('hub-ui-demo-app') + expect(firstHubComponent).toBeInTheDocument() + + firstUnmount() + + mockVerifyScriptSignature.mockClear() + + // Second render - should use cache + const { container: secondContainer } = renderWithProviders() + + await waitFor(() => { + const secondHubComponent = secondContainer.querySelector('hub-ui-demo-app') + expect(secondHubComponent).toBeInTheDocument() + }) + + expect(mockVerifyScriptSignature).toHaveBeenCalledTimes(0) + }) + }) +}) diff --git a/webui/src/pages/hub-demo/HubDashboard.tsx b/webui/src/pages/hub-demo/HubDashboard.tsx new file mode 100644 index 000000000..fdbb4d37f --- /dev/null +++ b/webui/src/pages/hub-demo/HubDashboard.tsx @@ -0,0 +1,147 @@ +import { Box, Flex, Image, Link, Text } from '@traefiklabs/faency' +import { useMemo, useEffect, useState } from 'react' +import { Helmet } from 'react-helmet-async' +import { useParams } from 'react-router-dom' + +import verifySignature from './workers/scriptVerification' + +import { SpinnerLoader } from 'components/SpinnerLoader' +import { useIsDarkMode } from 'hooks/use-theme' +import { TopNav } from 'layout/Navigation' + +const SCRIPT_URL = 'https://assets.traefik.io/hub-ui-demo.js' + +// Module-level cache to persist across component mount/unmount +let cachedBlobUrl: string | null = null + +// Export a function to reset the cache (for testing) +export const resetCache = () => { + cachedBlobUrl = null +} + +const HubDashboard = ({ path }: { path: string }) => { + const isDarkMode = useIsDarkMode() + const [scriptError, setScriptError] = useState(undefined) + const [signatureVerified, setSignatureVerified] = useState(false) + const [verificationInProgress, setVerificationInProgress] = useState(false) + const [scriptBlobUrl, setScriptBlobUrl] = useState(null) + + const { id } = useParams() + + const usedPath = useMemo(() => { + if (path?.includes(':id')) { + const splitted = path.split(':') + return `${splitted[0]}/${id}` + } + + return path + }, [id, path]) + + useEffect(() => { + const verifyAndLoadScript = async () => { + setVerificationInProgress(true) + + try { + const { verified, scriptContent: content } = await verifySignature(SCRIPT_URL, `${SCRIPT_URL}.sig`) + + if (!verified || !content) { + setScriptError(true) + setVerificationInProgress(false) + } else { + setScriptError(false) + + const blob = new Blob([content], { type: 'application/javascript' }) + cachedBlobUrl = URL.createObjectURL(blob) + + setScriptBlobUrl(cachedBlobUrl) + setSignatureVerified(true) + setVerificationInProgress(false) + } + } catch { + setScriptError(true) + setVerificationInProgress(false) + } + } + + if (!cachedBlobUrl) { + verifyAndLoadScript() + } else { + setScriptBlobUrl(cachedBlobUrl) + setSignatureVerified(true) + } + }, []) + + if (scriptError && !verificationInProgress) { + return ( + + + Oops! We couldn't load the demo content. + + Don't worry — you can still learn more about{' '} + + Traefik Hub API Management + {' '} + on our{' '} + + website + {' '} + or in our{' '} + + documentation + + . + + + ) + } + + return ( + + + Hub Demo - Traefik Proxy + + {signatureVerified && scriptBlobUrl && } + + + + + {verificationInProgress ? ( + + + + ) : ( + + )} + + ) +} + +export default HubDashboard diff --git a/webui/src/pages/hub-demo/HubDemoNav.tsx b/webui/src/pages/hub-demo/HubDemoNav.tsx new file mode 100644 index 000000000..eba41dc07 --- /dev/null +++ b/webui/src/pages/hub-demo/HubDemoNav.tsx @@ -0,0 +1,84 @@ +import { Badge, Box, Flex, Text } from '@traefiklabs/faency' +import { useContext, useState } from 'react' +import { BsChevronRight } from 'react-icons/bs' + +import { HubDemoContext } from './demoNavContext' +import { HubIcon } from './icons' + +import Tooltip from 'components/Tooltip' +import { BasicNavigationItem, LAPTOP_BP } from 'layout/Navigation' + +const ApimDemoNavMenu = ({ + isResponsive, + isSmallScreen, + isExpanded, +}: { + isResponsive: boolean + isSmallScreen: boolean + isExpanded: boolean +}) => { + const [isCollapsed, setIsCollapsed] = useState(false) + const { navigationItems: hubDemoNavItems } = useContext(HubDemoContext) + + if (!hubDemoNavItems) { + return null + } + + return ( + + setIsCollapsed(!isCollapsed)} + > + + {isSmallScreen ? ( + + + + + + ) : ( + <> + + API management + + + Demo + + + )} + + + + {hubDemoNavItems.map((route, idx) => ( + + ))} + + + ) +} + +export default ApimDemoNavMenu diff --git a/webui/src/pages/hub-demo/demoNavContext.tsx b/webui/src/pages/hub-demo/demoNavContext.tsx new file mode 100644 index 000000000..3f28afc78 --- /dev/null +++ b/webui/src/pages/hub-demo/demoNavContext.tsx @@ -0,0 +1,15 @@ +import { createContext } from 'react' +import { RouteObject } from 'react-router-dom' + +import { useHubDemo } from './use-hub-demo' + +export const HubDemoContext = createContext<{ + routes: RouteObject[] | null + navigationItems: HubDemo.NavItem[] | null +}>({ routes: null, navigationItems: null }) + +export const HubDemoProvider = ({ basePath, children }) => { + const { routes, navigationItems } = useHubDemo(basePath) + + return {children} +} diff --git a/webui/src/pages/hub-demo/hub-demo.d.ts b/webui/src/pages/hub-demo/hub-demo.d.ts new file mode 100644 index 000000000..31c6cfde7 --- /dev/null +++ b/webui/src/pages/hub-demo/hub-demo.d.ts @@ -0,0 +1,21 @@ +namespace HubDemo { + interface Route { + path: string + label: string + icon: string + contentPath: string + dynamicSegments?: string[] + activeMatches?: string[] + } + + interface Manifest { + routes: Route[] + } + + interface NavItem { + path: string + label: string + icon: ReactNode + activeMatches?: string[] + } +} diff --git a/webui/src/pages/hub-demo/icons/api.tsx b/webui/src/pages/hub-demo/icons/api.tsx new file mode 100644 index 000000000..04b51da6f --- /dev/null +++ b/webui/src/pages/hub-demo/icons/api.tsx @@ -0,0 +1,68 @@ +import { Flex } from '@traefiklabs/faency' +import { useId } from 'react' + +import { CustomIconProps } from 'components/icons' + +const ApiIcon = ({ color = 'currentColor', css = {}, ...props }: CustomIconProps) => { + const linearGradient1Id = useId() + const linearGradient2Id = useId() + const linearGradient3Id = useId() + const titleId = useId() + + return ( + + + apis + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default ApiIcon diff --git a/webui/src/pages/hub-demo/icons/dashboard.tsx b/webui/src/pages/hub-demo/icons/dashboard.tsx new file mode 100644 index 000000000..44ab47aa6 --- /dev/null +++ b/webui/src/pages/hub-demo/icons/dashboard.tsx @@ -0,0 +1,28 @@ +import { Flex } from '@traefiklabs/faency' + +import { CustomIconProps } from 'components/icons' + +const DashboardIcon = ({ color = 'currentColor', css = {}, ...props }: CustomIconProps) => { + return ( + + + dashboard + + + + + + + + ) +} + +export default DashboardIcon diff --git a/webui/src/pages/hub-demo/icons/gateway.tsx b/webui/src/pages/hub-demo/icons/gateway.tsx new file mode 100644 index 000000000..d1f682bf4 --- /dev/null +++ b/webui/src/pages/hub-demo/icons/gateway.tsx @@ -0,0 +1,69 @@ +import { Flex } from '@traefiklabs/faency' +import { useId } from 'react' + +import { CustomIconProps } from 'components/icons' + +const GatewayIcon = ({ color = 'currentColor', css = {}, ...props }: CustomIconProps) => { + const titleId = useId() + + return ( + + + gateways_icon + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default GatewayIcon diff --git a/webui/src/pages/hub-demo/icons/hub.tsx b/webui/src/pages/hub-demo/icons/hub.tsx new file mode 100644 index 000000000..64050576f --- /dev/null +++ b/webui/src/pages/hub-demo/icons/hub.tsx @@ -0,0 +1,18 @@ +import { CustomIconProps } from 'components/icons' + +const Hub = (props: CustomIconProps) => { + const { color = 'currentColor', ...restProps } = props + + return ( + + + + + + ) +} + +export default Hub diff --git a/webui/src/pages/hub-demo/icons/index.ts b/webui/src/pages/hub-demo/icons/index.ts new file mode 100644 index 000000000..5118c0fe4 --- /dev/null +++ b/webui/src/pages/hub-demo/icons/index.ts @@ -0,0 +1,5 @@ +export { default as ApiIcon } from './api' +export { default as DashboardIcon } from './dashboard' +export { default as GatewayIcon } from './gateway' +export { default as HubIcon } from './hub' +export { default as PortalIcon } from './portal' diff --git a/webui/src/pages/hub-demo/icons/portal.tsx b/webui/src/pages/hub-demo/icons/portal.tsx new file mode 100644 index 000000000..a21413ce8 --- /dev/null +++ b/webui/src/pages/hub-demo/icons/portal.tsx @@ -0,0 +1,48 @@ +import { Flex } from '@traefiklabs/faency' +import { useId } from 'react' + +import { CustomIconProps } from 'components/icons' + +const PortalIcon = ({ color = 'currentColor', css = {}, ...props }: CustomIconProps) => { + const linearGradientId = useId() + const titleId = useId() + + return ( + + + portals + + + + + + + + + + + + + + ) +} + +export default PortalIcon diff --git a/webui/src/pages/hub-demo/use-hub-demo.spec.tsx b/webui/src/pages/hub-demo/use-hub-demo.spec.tsx new file mode 100644 index 000000000..dba13cc97 --- /dev/null +++ b/webui/src/pages/hub-demo/use-hub-demo.spec.tsx @@ -0,0 +1,301 @@ +import { renderHook, waitFor } from '@testing-library/react' +import { ReactNode } from 'react' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +import { useHubDemo } from './use-hub-demo' +import verifySignature from './workers/scriptVerification' + +vi.mock('./workers/scriptVerification', () => ({ + default: vi.fn(), +})) + +const MOCK_ROUTES_MANIFEST = { + routes: [ + { + path: '/dashboard', + label: 'Dashboard', + icon: 'dashboard', + contentPath: 'dashboard', + }, + { + path: '/gateway', + label: 'Gateway', + icon: 'gateway', + contentPath: 'gateway', + dynamicSegments: [':id'], + activeMatches: ['/gateway/:id'], + }, + ], +} + +describe('useHubDemo', () => { + const mockVerifySignature = vi.mocked(verifySignature) + + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + const setupMockVerification = (manifest: HubDemo.Manifest) => { + const encoder = new TextEncoder() + const mockScriptContent = encoder.encode(JSON.stringify(manifest)) + + mockVerifySignature.mockResolvedValue({ + verified: true, + scriptContent: mockScriptContent.buffer, + }) + } + + describe('basic functions', () => { + const mockVerifySignature = vi.mocked(verifySignature) + + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should return null when signature verification fails', async () => { + mockVerifySignature.mockResolvedValue({ + verified: false, + }) + + const { result } = renderHook(() => useHubDemo('/hub')) + + await waitFor(() => { + expect(mockVerifySignature).toHaveBeenCalled() + }) + + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(result.current.routes).toBeNull() + expect(result.current.navigationItems).toBeNull() + }) + + it('should return null when scriptContent is missing', async () => { + mockVerifySignature.mockResolvedValue({ + verified: true, + scriptContent: undefined, + }) + + const { result } = renderHook(() => useHubDemo('/hub')) + + await waitFor(() => { + expect(mockVerifySignature).toHaveBeenCalled() + }) + + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(result.current.routes).toBeNull() + expect(result.current.navigationItems).toBeNull() + }) + + it('should handle errors during manifest fetch', async () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + mockVerifySignature.mockRejectedValue(new Error('Network error')) + + const { result } = renderHook(() => useHubDemo('/hub')) + + await waitFor(() => { + expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to load hub demo manifest:', expect.any(Error)) + }) + + expect(result.current.routes).toBeNull() + expect(result.current.navigationItems).toBeNull() + + consoleErrorSpy.mockRestore() + }) + + it('should handle invalid JSON in manifest', async () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const encoder = new TextEncoder() + const invalidJson = encoder.encode('{ invalid json }') + + mockVerifySignature.mockResolvedValue({ + verified: true, + scriptContent: invalidJson.buffer, + }) + + const { result } = renderHook(() => useHubDemo('/hub')) + + await waitFor(() => { + expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to load hub demo manifest:', expect.any(Error)) + }) + + expect(result.current.routes).toBeNull() + expect(result.current.navigationItems).toBeNull() + + consoleErrorSpy.mockRestore() + }) + }) + + describe('routes generation', () => { + it('should generate routes with correct base path', async () => { + setupMockVerification(MOCK_ROUTES_MANIFEST) + + const { result } = renderHook(() => useHubDemo('/hub')) + + await waitFor(() => { + expect(result.current.routes).not.toBeNull() + }) + + expect(result.current.routes).toHaveLength(3) + expect(result.current.routes![0].path).toBe('/hub/dashboard') + expect(result.current.routes![1].path).toBe('/hub/gateway') + expect(result.current.routes![2].path).toBe('/hub/gateway/:id') + }) + + it('should generate routes for dynamic segments', async () => { + setupMockVerification(MOCK_ROUTES_MANIFEST) + + const { result } = renderHook(() => useHubDemo('/hub')) + + await waitFor(() => { + expect(result.current.routes).not.toBeNull() + }) + + expect(result.current.routes).toHaveLength(3) + expect(result.current.routes![0].path).toBe('/hub/dashboard') + expect(result.current.routes![1].path).toBe('/hub/gateway') + expect(result.current.routes![2].path).toBe('/hub/gateway/:id') + }) + + it('should render HubDashboard with correct contentPath for dynamic segments', async () => { + setupMockVerification(MOCK_ROUTES_MANIFEST) + + const { result } = renderHook(() => useHubDemo('/hub')) + + await waitFor(() => { + expect(result.current.routes).not.toBeNull() + }) + + const baseRoute = result.current.routes![1] + const dynamicRoute = result.current.routes![2] + + expect(baseRoute.element).toBeDefined() + expect(dynamicRoute.element).toBeDefined() + + const baseElement = baseRoute.element as ReactNode & { props?: { path: string } } + const dynamicElement = dynamicRoute.element as ReactNode & { props?: { path: string } } + + expect((baseElement as { props: { path: string } }).props.path).toBe('gateway') + expect((dynamicElement as { props: { path: string } }).props.path).toBe('gateway:id') + }) + + it('should update routes when basePath changes', async () => { + setupMockVerification(MOCK_ROUTES_MANIFEST) + + const { result, rerender } = renderHook(({ basePath }) => useHubDemo(basePath), { + initialProps: { basePath: '/hub' }, + }) + + await waitFor(() => { + expect(result.current.routes).not.toBeNull() + }) + + expect(result.current.routes![0].path).toBe('/hub/dashboard') + + rerender({ basePath: '/demo' }) + + expect(result.current.routes![0].path).toBe('/demo/dashboard') + }) + }) + + describe('navigation items generation', () => { + it('should generate navigation items with correct icons', async () => { + setupMockVerification(MOCK_ROUTES_MANIFEST) + + const { result } = renderHook(() => useHubDemo('/hub')) + + await waitFor(() => { + expect(result.current.navigationItems).not.toBeNull() + }) + + expect(result.current.navigationItems).toHaveLength(2) + expect(result.current.navigationItems![0].label).toBe('Dashboard') + expect(result.current.navigationItems![0].path).toBe('/hub/dashboard') + expect(result.current.navigationItems![0].icon).toBeDefined() + expect(result.current.navigationItems![1].label).toBe('Gateway') + }) + + it('should include activeMatches in navigation items', async () => { + setupMockVerification(MOCK_ROUTES_MANIFEST) + + const { result } = renderHook(() => useHubDemo('/hub')) + + await waitFor(() => { + expect(result.current.navigationItems).not.toBeNull() + }) + + expect(result.current.navigationItems![1].activeMatches).toEqual(['/hub/gateway/:id']) + }) + + it('should update navigation items when basePath changes', async () => { + setupMockVerification(MOCK_ROUTES_MANIFEST) + + const { result, rerender } = renderHook(({ basePath }) => useHubDemo(basePath), { + initialProps: { basePath: '/hub' }, + }) + + await waitFor(() => { + expect(result.current.navigationItems).not.toBeNull() + }) + + expect(result.current.navigationItems![0].path).toBe('/hub/dashboard') + + rerender({ basePath: '/demo' }) + + expect(result.current.navigationItems![0].path).toBe('/demo/dashboard') + }) + + it('should handle unknown icon types gracefully', async () => { + const manifestWithUnknownIcon: HubDemo.Manifest = { + routes: [ + { + path: '/unknown', + label: 'Unknown', + icon: 'unknown-icon-type', + contentPath: 'unknown', + }, + ], + } + + setupMockVerification(manifestWithUnknownIcon) + + const { result } = renderHook(() => useHubDemo('/hub')) + + await waitFor(() => { + expect(result.current.navigationItems).not.toBeNull() + }) + + expect(result.current.navigationItems![0].icon).toBeUndefined() + }) + }) + + describe('memoization', () => { + it('should not regenerate routes when manifest and basePath are unchanged', async () => { + setupMockVerification(MOCK_ROUTES_MANIFEST) + + const { result, rerender } = renderHook(() => useHubDemo('/hub')) + + await waitFor(() => { + expect(result.current.routes).not.toBeNull() + }) + + const firstRoutes = result.current.routes + const firstNavItems = result.current.navigationItems + + rerender() + + expect(result.current.routes).toBe(firstRoutes) + expect(result.current.navigationItems).toBe(firstNavItems) + }) + }) +}) diff --git a/webui/src/pages/hub-demo/use-hub-demo.tsx b/webui/src/pages/hub-demo/use-hub-demo.tsx new file mode 100644 index 000000000..4eee4f455 --- /dev/null +++ b/webui/src/pages/hub-demo/use-hub-demo.tsx @@ -0,0 +1,89 @@ +import { ReactNode, useEffect, useMemo, useState } from 'react' +import { RouteObject } from 'react-router-dom' + +import HubDashboard from 'pages/hub-demo/HubDashboard' +import { ApiIcon, DashboardIcon, GatewayIcon, PortalIcon } from 'pages/hub-demo/icons' +import verifySignature from 'pages/hub-demo/workers/scriptVerification' + +const ROUTES_MANIFEST_URL = 'https://traefik.github.io/hub-ui-demo-app/config/routes.json' + +const HUB_DEMO_NAV_ICONS: Record = { + dashboard: , + gateway: , + api: , + portal: , +} + +const useHubDemoRoutesManifest = (): HubDemo.Manifest | null => { + const [manifest, setManifest] = useState(null) + + useEffect(() => { + const fetchManifest = async () => { + try { + const { verified, scriptContent } = await verifySignature(ROUTES_MANIFEST_URL, `${ROUTES_MANIFEST_URL}.sig`) + + if (!verified || !scriptContent) { + setManifest(null) + return + } + + const textDecoder = new TextDecoder() + const jsonString = textDecoder.decode(scriptContent) + const data: HubDemo.Manifest = JSON.parse(jsonString) + setManifest(data) + } catch (error) { + console.error('Failed to load hub demo manifest:', error) + setManifest(null) + } + } + + fetchManifest() + }, []) + + return manifest +} + +export const useHubDemo = (basePath: string) => { + const manifest = useHubDemoRoutesManifest() + + const routes = useMemo(() => { + if (!manifest) { + return null + } + + const routeObjects: RouteObject[] = [] + + manifest.routes.forEach((route: HubDemo.Route) => { + routeObjects.push({ + path: `${basePath}${route.path}`, + element: , + }) + + if (route.dynamicSegments) { + route.dynamicSegments.forEach((segment) => { + routeObjects.push({ + path: `${basePath}${route.path}/${segment}`, + element: , + }) + }) + } + }) + + return routeObjects + }, [basePath, manifest]) + + const navigationItems = useMemo(() => { + if (!manifest) { + return null + } + + return manifest.routes.map((route) => ({ + path: `${basePath}${route.path}`, + label: route.label, + icon: HUB_DEMO_NAV_ICONS[route.icon], + activeMatches: route.activeMatches?.map((r) => `${basePath}${r}`), + })) + }, [basePath, manifest]) + + return { routes, navigationItems } +} diff --git a/webui/src/pages/hub-demo/workers/scriptVerification.integration.spec.ts b/webui/src/pages/hub-demo/workers/scriptVerification.integration.spec.ts new file mode 100644 index 000000000..189632c64 --- /dev/null +++ b/webui/src/pages/hub-demo/workers/scriptVerification.integration.spec.ts @@ -0,0 +1,156 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + +import verifySignature from './scriptVerification' + +describe('Script Signature Verification - Integration Tests', () => { + let fetchMock: ReturnType + + const SCRIPT_URL = 'https://example.com/script.js' + const SIGNATURE_URL = 'https://example.com/script.js.sig' + const TEST_PUBLIC_KEY = 'MCowBQYDK2VwAyEAWH71OHphISjNK3mizCR/BawiDxc6IXT1vFHpBcxSIA0=' + const VALID_SCRIPT = "console.log('Hello from verified script!');" + const VALID_SIGNATURE_HEX = + '04c90fcd35caaf3cf4582a2767345f8cd9f6519e1ce79ebaeedbe0d5f671d762d1aa8ec258831557e2de0e47f224883f84eb5a0f22ec18eb7b8c48de3096d000' + const CORRUPTED_SCRIPT = "console.log('Malicious code injected!');" + + beforeEach(() => { + vi.clearAllMocks() + fetchMock = vi.fn() + globalThis.fetch = fetchMock + }) + + it('should verify a valid script with correct signature through real worker', async () => { + fetchMock.mockImplementation((url: string) => { + if (url === SCRIPT_URL) { + return Promise.resolve( + new Response(VALID_SCRIPT, { + status: 200, + headers: { 'Content-Type': 'application/javascript' }, + }), + ) + } + if (url === SIGNATURE_URL) { + return Promise.resolve( + new Response(VALID_SIGNATURE_HEX, { + status: 200, + headers: { 'Content-Type': 'text/plain' }, + }), + ) + } + return Promise.reject(new Error('Unexpected URL')) + }) + + const result = await verifySignature(SCRIPT_URL, SIGNATURE_URL, TEST_PUBLIC_KEY) + + expect(fetchMock).toHaveBeenCalledWith(SCRIPT_URL) + expect(fetchMock).toHaveBeenCalledWith(SIGNATURE_URL) + expect(result.verified).toBe(true) + expect(result.scriptContent).toBeDefined() + }, 15000) + + it('should reject a corrupted script with mismatched signature', async () => { + fetchMock.mockImplementation((url: string) => { + if (url === SCRIPT_URL) { + return Promise.resolve( + new Response(CORRUPTED_SCRIPT, { + status: 200, + headers: { 'Content-Type': 'application/javascript' }, + }), + ) + } + if (url === SIGNATURE_URL) { + return Promise.resolve( + new Response(VALID_SIGNATURE_HEX, { + status: 200, + headers: { 'Content-Type': 'text/plain' }, + }), + ) + } + return Promise.reject(new Error('Unexpected URL')) + }) + + const result = await verifySignature(SCRIPT_URL, SIGNATURE_URL, TEST_PUBLIC_KEY) + + expect(fetchMock).toHaveBeenCalledWith(SCRIPT_URL) + expect(fetchMock).toHaveBeenCalledWith(SIGNATURE_URL) + expect(result.verified).toBe(false) + expect(result.scriptContent).toBeUndefined() + }, 15000) + + it('should reject script with invalid signature format', async () => { + fetchMock.mockImplementation((url: string) => { + if (url === SCRIPT_URL) { + return Promise.resolve( + new Response(VALID_SCRIPT, { + status: 200, + headers: { 'Content-Type': 'application/javascript' }, + }), + ) + } + if (url === SIGNATURE_URL) { + return Promise.resolve( + new Response('not-a-valid-signature', { + status: 200, + headers: { 'Content-Type': 'text/plain' }, + }), + ) + } + return Promise.reject(new Error('Unexpected URL')) + }) + + const result = await verifySignature(SCRIPT_URL, SIGNATURE_URL, TEST_PUBLIC_KEY) + + expect(result.verified).toBe(false) + expect(result.scriptContent).toBeUndefined() + }, 15000) + + it('should reject script with wrong public key', async () => { + const WRONG_PUBLIC_KEY = 'MCowBQYDK2VwAyEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==' + + fetchMock.mockImplementation((url: string) => { + if (url === SCRIPT_URL) { + return Promise.resolve( + new Response(VALID_SCRIPT, { + status: 200, + headers: { 'Content-Type': 'application/javascript' }, + }), + ) + } + if (url === SIGNATURE_URL) { + return Promise.resolve( + new Response(VALID_SIGNATURE_HEX, { + status: 200, + headers: { 'Content-Type': 'text/plain' }, + }), + ) + } + return Promise.reject(new Error('Unexpected URL')) + }) + + const result = await verifySignature(SCRIPT_URL, SIGNATURE_URL, WRONG_PUBLIC_KEY) + + expect(result.verified).toBe(false) + expect(result.scriptContent).toBeUndefined() + }, 15000) + + it('should handle network failures when fetching script', async () => { + fetchMock.mockImplementation(() => + Promise.resolve( + new Response(null, { + status: 404, + statusText: 'Not Found', + }), + ), + ) + + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const result = await verifySignature(SCRIPT_URL, SIGNATURE_URL, TEST_PUBLIC_KEY) + + expect(result.verified).toBe(false) + expect(result.scriptContent).toBeUndefined() + expect(consoleErrorSpy).toHaveBeenCalled() + + consoleErrorSpy.mockRestore() + }, 15000) +}) diff --git a/webui/src/pages/hub-demo/workers/scriptVerification.spec.ts b/webui/src/pages/hub-demo/workers/scriptVerification.spec.ts new file mode 100644 index 000000000..1f438aa71 --- /dev/null +++ b/webui/src/pages/hub-demo/workers/scriptVerification.spec.ts @@ -0,0 +1,125 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +import verifySignature from './scriptVerification' + +class MockWorker { + onmessage: ((event: MessageEvent) => void) | null = null + onerror: ((error: ErrorEvent) => void) | null = null + postMessage = vi.fn() + terminate = vi.fn() + + simulateMessage(data: unknown) { + if (this.onmessage) { + this.onmessage(new MessageEvent('message', { data })) + } + } + + simulateError(error: Error) { + if (this.onerror) { + this.onerror(new ErrorEvent('error', { error, message: error.message })) + } + } +} + +describe('verifySignature', () => { + let mockWorkerInstance: MockWorker + let originalWorker: typeof Worker + + beforeEach(() => { + vi.clearAllMocks() + + originalWorker = globalThis.Worker + + mockWorkerInstance = new MockWorker() + + globalThis.Worker = class extends EventTarget { + constructor() { + super() + return mockWorkerInstance as any + } + } as any + }) + + afterEach(() => { + globalThis.Worker = originalWorker + vi.restoreAllMocks() + }) + + it('should return true when verification succeeds', async () => { + const scriptPath = 'https://example.com/script.js' + const signaturePath = 'https://example.com/script.js.sig' + + const promise = verifySignature(scriptPath, signaturePath) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(mockWorkerInstance.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ + scriptUrl: scriptPath, + signatureUrl: signaturePath, + requestId: expect.any(String), + }), + ) + + const mockScriptContent = new ArrayBuffer(100) + mockWorkerInstance.simulateMessage({ + success: true, + verified: true, + error: null, + scriptContent: mockScriptContent, + }) + + const result = await promise + + expect(result).toEqual({ verified: true, scriptContent: mockScriptContent }) + expect(mockWorkerInstance.terminate).toHaveBeenCalled() + }) + + it('should return false when verification fails', async () => { + const scriptPath = 'https://example.com/script.js' + const signaturePath = 'https://example.com/script.js.sig' + + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const promise = verifySignature(scriptPath, signaturePath) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + mockWorkerInstance.simulateMessage({ + success: false, + verified: false, + error: 'Signature verification failed', + }) + + const result = await promise + + expect(result).toEqual({ verified: false }) + expect(mockWorkerInstance.terminate).toHaveBeenCalled() + expect(consoleErrorSpy).toHaveBeenCalledWith('Worker verification failed:', 'Signature verification failed') + + consoleErrorSpy.mockRestore() + }) + + it('should return false when worker throws an error', async () => { + const scriptPath = 'https://example.com/script.js' + const signaturePath = 'https://example.com/script.js.sig' + + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const promise = verifySignature(scriptPath, signaturePath) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + // Simulate worker onerror event + const error = new Error('Worker crashed') + mockWorkerInstance.simulateError(error) + + const result = await promise + + expect(result).toEqual({ verified: false }) + expect(mockWorkerInstance.terminate).toHaveBeenCalled() + expect(consoleErrorSpy).toHaveBeenCalledWith('Worker error:', expect.any(ErrorEvent)) + + consoleErrorSpy.mockRestore() + }) +}) diff --git a/webui/src/pages/hub-demo/workers/scriptVerification.ts b/webui/src/pages/hub-demo/workers/scriptVerification.ts new file mode 100644 index 000000000..e03a438da --- /dev/null +++ b/webui/src/pages/hub-demo/workers/scriptVerification.ts @@ -0,0 +1,57 @@ +export interface VerificationResult { + verified: boolean + scriptContent?: ArrayBuffer +} + +const PUBLIC_KEY = 'MCowBQYDK2VwAyEAWMBZ0pMBaL/s8gNXxpAPCIQ8bxjnuz6bQFwGYvjXDfg=' + +async function verifySignature( + contentPath: string, + signaturePath: string, + publicKey: string = PUBLIC_KEY, +): Promise { + return new Promise((resolve) => { + const requestId = Math.random().toString(36).substring(2) + const worker = new Worker(new URL('./scriptVerificationWorker.ts', import.meta.url), { type: 'module' }) + + const timeout = setTimeout(() => { + worker.terminate() + console.error('Script verification timeout') + resolve({ verified: false }) + }, 30000) + + worker.onmessage = (event) => { + clearTimeout(timeout) + worker.terminate() + + const { success, verified, error, scriptContent } = event.data + + if (!success) { + console.error('Worker verification failed:', error) + resolve({ verified: false }) + return + } + + resolve({ + verified: verified === true, + scriptContent: verified ? scriptContent : undefined, + }) + } + + worker.onerror = (error) => { + clearTimeout(timeout) + worker.terminate() + console.error('Worker error:', error) + resolve({ verified: false }) + } + + worker.postMessage({ + requestId, + scriptUrl: contentPath, + signatureUrl: signaturePath, + publicKey, + }) + }) +} + +export default verifySignature diff --git a/webui/src/pages/hub-demo/workers/scriptVerificationWorker.ts b/webui/src/pages/hub-demo/workers/scriptVerificationWorker.ts new file mode 100644 index 000000000..902dce094 --- /dev/null +++ b/webui/src/pages/hub-demo/workers/scriptVerificationWorker.ts @@ -0,0 +1,189 @@ +// Script verification worker +// Runs in isolated context for secure verification + +import { verify } from '@noble/ed25519' +import * as ed25519 from '@noble/ed25519' +import { sha512 } from '@noble/hashes/sha2.js' + +// Set up SHA-512 for @noble/ed25519 v3.x +ed25519.hashes.sha512 = sha512 +ed25519.hashes.sha512Async = (m) => Promise.resolve(sha512(m)) + +function base64ToArrayBuffer(base64: string): ArrayBuffer { + try { + // @ts-expect-error - fromBase64 is not yet in all TypeScript lib definitions + const bytes = Uint8Array.fromBase64(base64) + return bytes.buffer + } catch { + // Fallback for browsers without Uint8Array.fromBase64() + const binaryString = atob(base64) + const bytes = new Uint8Array(binaryString.length) + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + return bytes.buffer + } +} + +function extractEd25519PublicKey(spkiBytes: Uint8Array): Uint8Array { + if (spkiBytes.length !== 44) { + throw new Error('Invalid SPKI length for Ed25519') + } + return spkiBytes.slice(-32) +} + +async function importPublicKeyWebCrypto(publicKey: string): Promise { + const publicKeyBuffer = base64ToArrayBuffer(publicKey) + + return await crypto.subtle.importKey( + 'spki', + publicKeyBuffer, + { + name: 'Ed25519', + }, + false, + ['verify'], + ) +} + +async function verifyWithWebCrypto( + publicKey: string, + scriptBuffer: ArrayBuffer, + signatureBuffer: ArrayBuffer, +): Promise { + try { + const cryptoPublicKey = await importPublicKeyWebCrypto(publicKey) + + return await crypto.subtle.verify('Ed25519', cryptoPublicKey, signatureBuffer, scriptBuffer) + } catch (error) { + console.log('Web Crypto verification failed:', error instanceof Error ? error.message : 'Unknown error') + return false + } +} + +function parseSignature(signatureBuffer: ArrayBuffer): Uint8Array { + const signatureBytes = new Uint8Array(signatureBuffer) + + // If already 64 bytes, assume it's raw binary + if (signatureBytes.length === 64) { + return signatureBytes + } + + // Try to parse as text (base64 or hex) + const signatureText = new TextDecoder().decode(signatureBytes).trim() + + // base64 decoding + try { + const base64Decoded = new Uint8Array(base64ToArrayBuffer(signatureText)) + if (base64Decoded.length === 64) { + return base64Decoded + } + } catch (e) { + console.error(e) + } + + // hex decoding + if (signatureText.length === 128 && /^[0-9a-fA-F]+$/.test(signatureText)) { + try { + // @ts-expect-error - fromHex is not yet in all TypeScript lib definitions + return Uint8Array.fromHex(signatureText) + } catch { + // Fallback for browsers without Uint8Array.fromHex() + const hexDecoded = new Uint8Array(64) + for (let i = 0; i < 64; i++) { + hexDecoded[i] = parseInt(signatureText.slice(i * 2, i * 2 + 2), 16) + } + return hexDecoded + } + } + + throw new Error(`Unable to parse signature format.`) +} + +async function verifyWithNoble( + publicKey: string, + scriptBuffer: ArrayBuffer, + signatureBuffer: ArrayBuffer, +): Promise { + try { + const publicKeySpki = new Uint8Array(base64ToArrayBuffer(publicKey)) + const publicKeyRaw = extractEd25519PublicKey(publicKeySpki) + + const scriptBytes = new Uint8Array(scriptBuffer) + const signatureBytes = parseSignature(signatureBuffer) + + return verify(signatureBytes, scriptBytes, publicKeyRaw) + } catch (error) { + console.log('Noble verification failed:', error instanceof Error ? error.message : 'Unknown error') + return false + } +} + +self.onmessage = async function (event) { + const { requestId, scriptUrl, signatureUrl, publicKey } = event.data + + try { + const [scriptResponse, signatureResponse] = await Promise.all([fetch(scriptUrl), fetch(signatureUrl)]) + + if (!scriptResponse.ok || !signatureResponse.ok) { + self.postMessage({ + requestId, + success: false, + verified: false, + error: `Failed to fetch files. Script: ${scriptResponse.status} ${scriptResponse.statusText}, Signature: ${signatureResponse.status} ${signatureResponse.statusText}`, + }) + return + } + + const [scriptBuffer, signatureBuffer] = await Promise.all([ + scriptResponse.arrayBuffer(), + signatureResponse.arrayBuffer(), + ]) + + // Try Web Crypto API first, fallback to Noble if it fails + let verified = await verifyWithWebCrypto(publicKey, scriptBuffer, signatureBuffer) + + if (!verified) { + verified = await verifyWithNoble(publicKey, scriptBuffer, signatureBuffer) + } + + // If verified, include script content to avoid re-downloading + let scriptContent: ArrayBuffer | undefined + if (verified) { + scriptContent = scriptBuffer + } + + // Send message with transferable ArrayBuffer for efficiency + const message = { + requestId, + success: true, + verified, + scriptSize: scriptBuffer.byteLength, + signatureSize: signatureBuffer.byteLength, + scriptContent, + } + + if (scriptContent) { + self.postMessage(message, { transfer: [scriptContent] }) + } else { + self.postMessage(message) + } + } catch (error) { + console.error('[Worker] Verification error:', error) + self.postMessage({ + requestId, + success: false, + verified: false, + error: error instanceof Error ? error.message : 'Unknown error', + }) + } +} + +self.onerror = function (error) { + console.error('[Worker] Worker error:', error) + self.postMessage({ + success: false, + verified: false, + error, + }) +} diff --git a/webui/src/pages/tcp/TcpMiddleware.spec.tsx b/webui/src/pages/tcp/TcpMiddleware.spec.tsx index b783e0958..73e69e01c 100644 --- a/webui/src/pages/tcp/TcpMiddleware.spec.tsx +++ b/webui/src/pages/tcp/TcpMiddleware.spec.tsx @@ -7,6 +7,7 @@ describe('', () => { it('should render the error message', () => { const { getByTestId } = renderWithProviders( , + { route: '/tcp/middlewares/mock-middleware', withPage: true }, ) expect(getByTestId('error-text')).toBeInTheDocument() }) @@ -14,6 +15,7 @@ describe('', () => { it('should render the skeleton', () => { const { getByTestId } = renderWithProviders( , + { route: '/tcp/middlewares/mock-middleware', withPage: true }, ) expect(getByTestId('skeleton')).toBeInTheDocument() }) @@ -21,6 +23,7 @@ describe('', () => { it('should render the not found page', () => { const { getByTestId } = renderWithProviders( , + { route: '/tcp/middlewares/mock-middleware', withPage: true }, ) expect(getByTestId('Not found page')).toBeInTheDocument() }) @@ -53,6 +56,7 @@ describe('', () => { const { container, getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/tcp/middlewares/middleware-simple', withPage: true }, ) const headings = Array.from(container.getElementsByTagName('h1')) @@ -103,6 +107,7 @@ describe('', () => { const { container, getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/tcp/middlewares/middleware-complex', withPage: true }, ) const headings = Array.from(container.getElementsByTagName('h1')) diff --git a/webui/src/pages/tcp/TcpMiddleware.tsx b/webui/src/pages/tcp/TcpMiddleware.tsx index f3637a46d..d85cf22ec 100644 --- a/webui/src/pages/tcp/TcpMiddleware.tsx +++ b/webui/src/pages/tcp/TcpMiddleware.tsx @@ -1,11 +1,11 @@ import { Card, Box, H1, Skeleton, styled, Text } from '@traefiklabs/faency' +import { Helmet } from 'react-helmet-async' import { useParams } from 'react-router-dom' import { DetailSectionSkeleton } from 'components/resources/DetailSections' import { RenderMiddleware } from 'components/resources/MiddlewarePanel' import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection' import { ResourceDetailDataType, useResourceDetail } from 'hooks/use-resource-detail' -import Page from 'layout/Page' import { NotFound } from 'pages/NotFound' import breakpoints from 'utils/breakpoints' @@ -27,23 +27,29 @@ type TcpMiddlewareRenderProps = { export const TcpMiddlewareRender = ({ data, error, name }: TcpMiddlewareRenderProps) => { if (error) { return ( - + <> + + {name} - Traefik Proxy + Sorry, we could not fetch detail information for this Middleware right now. Please, try again later. - + ) } if (!data) { return ( - + <> + + {name} - Traefik Proxy + - + ) } @@ -52,7 +58,10 @@ export const TcpMiddlewareRender = ({ data, error, name }: TcpMiddlewareRenderPr } return ( - + <> + + {data.name} - Traefik Proxy +

{data.name}

@@ -60,7 +69,7 @@ export const TcpMiddlewareRender = ({ data, error, name }: TcpMiddlewareRenderPr -
+ ) } diff --git a/webui/src/pages/tcp/TcpMiddlewares.spec.tsx b/webui/src/pages/tcp/TcpMiddlewares.spec.tsx index 3da9cfc5b..12e95f92f 100644 --- a/webui/src/pages/tcp/TcpMiddlewares.spec.tsx +++ b/webui/src/pages/tcp/TcpMiddlewares.spec.tsx @@ -29,10 +29,13 @@ describe('', () => { .spyOn(useFetchWithPagination, 'default') .mockImplementation(() => useFetchWithPaginationMock({ pages })) - const { container, getByTestId } = renderWithProviders() + const { container, getByTestId } = renderWithProviders(, { + route: '/tcp/middlewares', + withPage: true, + }) expect(mock).toHaveBeenCalled() - expect(getByTestId('TCP Middlewares page')).toBeInTheDocument() + expect(getByTestId('/tcp/middlewares page')).toBeInTheDocument() const tbody = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[1] expect(tbody.querySelectorAll('a[role="row"]')).toHaveLength(2) @@ -58,6 +61,7 @@ describe('', () => { pageCount={1} pages={[]} />, + { route: '/tcp/middlewares', withPage: true }, ) expect(() => getByTestId('loading')).toThrow('Unable to find an element by: [data-testid="loading"]') const tfoot = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[2] diff --git a/webui/src/pages/tcp/TcpMiddlewares.tsx b/webui/src/pages/tcp/TcpMiddlewares.tsx index c736d4c47..b0189a2e5 100644 --- a/webui/src/pages/tcp/TcpMiddlewares.tsx +++ b/webui/src/pages/tcp/TcpMiddlewares.tsx @@ -1,5 +1,6 @@ import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex } from '@traefiklabs/faency' import { useMemo } from 'react' +import { Helmet } from 'react-helmet-async' import useInfiniteScroll from 'react-infinite-scroll-hook' import { useSearchParams } from 'react-router-dom' @@ -14,7 +15,6 @@ import Tooltip from 'components/Tooltip' import TooltipText from 'components/TooltipText' import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination' import { EmptyPlaceholder } from 'layout/EmptyPlaceholder' -import Page from 'layout/Page' import { parseMiddlewareType } from 'libs/parsers' export const makeRowRender = (): RenderRowType => { @@ -109,7 +109,10 @@ export const TcpMiddlewares = () => { ) return ( - + <> + + TCP Middlewares - Traefik Proxy + { pageCount={pageCount} pages={pages} /> - + ) } diff --git a/webui/src/pages/tcp/TcpRouter.spec.tsx b/webui/src/pages/tcp/TcpRouter.spec.tsx index bd5fe7059..c1f5bb6a4 100644 --- a/webui/src/pages/tcp/TcpRouter.spec.tsx +++ b/webui/src/pages/tcp/TcpRouter.spec.tsx @@ -7,6 +7,7 @@ describe('', () => { it('should render the error message', () => { const { getByTestId } = renderWithProviders( , + { route: '/tcp/routers/mock-router', withPage: true }, ) expect(getByTestId('error-text')).toBeInTheDocument() }) @@ -14,6 +15,7 @@ describe('', () => { it('should render the skeleton', () => { const { getByTestId } = renderWithProviders( , + { route: '/tcp/routers/mock-router', withPage: true }, ) expect(getByTestId('skeleton')).toBeInTheDocument() }) @@ -21,6 +23,7 @@ describe('', () => { it('should render the not found page', () => { const { getByTestId } = renderWithProviders( , + { route: '/tcp/routers/mock-router', withPage: true }, ) expect(getByTestId('Not found page')).toBeInTheDocument() }) @@ -66,6 +69,7 @@ describe('', () => { const { getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/tcp/routers/tcp-all@docker', withPage: true }, ) const routerStructure = getByTestId('router-structure') diff --git a/webui/src/pages/tcp/TcpRouter.tsx b/webui/src/pages/tcp/TcpRouter.tsx index 1bdac707c..92f9b47b2 100644 --- a/webui/src/pages/tcp/TcpRouter.tsx +++ b/webui/src/pages/tcp/TcpRouter.tsx @@ -1,4 +1,5 @@ import { Flex, styled, Text } from '@traefiklabs/faency' +import { Helmet } from 'react-helmet-async' import { useParams } from 'react-router-dom' import { CardListSection, DetailSectionSkeleton } from 'components/resources/DetailSections' @@ -6,7 +7,6 @@ import MiddlewarePanel from 'components/resources/MiddlewarePanel' import RouterPanel from 'components/resources/RouterPanel' import TlsPanel from 'components/resources/TlsPanel' import { ResourceDetailDataType, useResourceDetail } from 'hooks/use-resource-detail' -import Page from 'layout/Page' import { RouterStructure } from 'pages/http/HttpRouter' import { NotFound } from 'pages/NotFound' @@ -37,17 +37,23 @@ type TcpRouterRenderProps = { export const TcpRouterRender = ({ data, error, name }: TcpRouterRenderProps) => { if (error) { return ( - + <> + + {name} - Traefik Proxy + Sorry, we could not fetch detail information for this Router right now. Please, try again later. - + ) } if (!data) { return ( - + <> + + {name} - Traefik Proxy + @@ -57,7 +63,7 @@ export const TcpRouterRender = ({ data, error, name }: TcpRouterRenderProps) => - + ) } @@ -66,10 +72,13 @@ export const TcpRouterRender = ({ data, error, name }: TcpRouterRenderProps) => } return ( - + <> + + {data.name} - Traefik Proxy + - + ) } diff --git a/webui/src/pages/tcp/TcpRouters.spec.tsx b/webui/src/pages/tcp/TcpRouters.spec.tsx index 60acab68e..fac801fb6 100644 --- a/webui/src/pages/tcp/TcpRouters.spec.tsx +++ b/webui/src/pages/tcp/TcpRouters.spec.tsx @@ -39,10 +39,13 @@ describe('', () => { .spyOn(useFetchWithPagination, 'default') .mockImplementation(() => useFetchWithPaginationMock({ pages })) - const { container, getByTestId } = renderWithProviders() + const { container, getByTestId } = renderWithProviders(, { + route: '/tcp/routers', + withPage: true, + }) expect(mock).toHaveBeenCalled() - expect(getByTestId('TCP Routers page')).toBeInTheDocument() + expect(getByTestId('/tcp/routers page')).toBeInTheDocument() const tbody = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[1] expect(tbody.querySelectorAll('a[role="row"]')).toHaveLength(3) @@ -76,6 +79,7 @@ describe('', () => { pageCount={1} pages={[]} />, + { route: '/tcp/routers', withPage: true }, ) expect(() => getByTestId('loading')).toThrow('Unable to find an element by: [data-testid="loading"]') const tfoot = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[2] diff --git a/webui/src/pages/tcp/TcpRouters.tsx b/webui/src/pages/tcp/TcpRouters.tsx index de8319e79..f3cd3d497 100644 --- a/webui/src/pages/tcp/TcpRouters.tsx +++ b/webui/src/pages/tcp/TcpRouters.tsx @@ -1,5 +1,6 @@ import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex } from '@traefiklabs/faency' import { useMemo } from 'react' +import { Helmet } from 'react-helmet-async' import { FiShield } from 'react-icons/fi' import useInfiniteScroll from 'react-infinite-scroll-hook' import { useSearchParams } from 'react-router-dom' @@ -16,7 +17,6 @@ import Tooltip from 'components/Tooltip' import TooltipText from 'components/TooltipText' import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination' import { EmptyPlaceholder } from 'layout/EmptyPlaceholder' -import Page from 'layout/Page' export const makeRowRender = (): RenderRowType => { const TcpRoutersRenderRow = (row) => ( @@ -126,7 +126,10 @@ export const TcpRouters = () => { ) return ( - + <> + + TCP Routers - Traefik Proxy + { pageCount={pageCount} pages={pages} /> - + ) } diff --git a/webui/src/pages/tcp/TcpService.spec.tsx b/webui/src/pages/tcp/TcpService.spec.tsx index e6b835832..faa8ac0f5 100644 --- a/webui/src/pages/tcp/TcpService.spec.tsx +++ b/webui/src/pages/tcp/TcpService.spec.tsx @@ -7,6 +7,7 @@ describe('', () => { it('should render the error message', () => { const { getByTestId } = renderWithProviders( , + { route: '/tcp/services/mock-service', withPage: true }, ) expect(getByTestId('error-text')).toBeInTheDocument() }) @@ -14,6 +15,7 @@ describe('', () => { it('should render the skeleton', () => { const { getByTestId } = renderWithProviders( , + { route: '/tcp/services/mock-service', withPage: true }, ) expect(getByTestId('skeleton')).toBeInTheDocument() }) @@ -21,6 +23,7 @@ describe('', () => { it('should render the not found page', () => { const { getByTestId } = renderWithProviders( , + { route: '/tcp/services/mock-service', withPage: true }, ) expect(getByTestId('Not found page')).toBeInTheDocument() }) @@ -69,6 +72,7 @@ describe('', () => { const { container, getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/tcp/services/mock-service', withPage: true }, ) const headings = Array.from(container.getElementsByTagName('h1')) @@ -150,6 +154,7 @@ describe('', () => { const { getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/tcp/services/mock-service', withPage: true }, ) const serversList = getByTestId('tcp-servers-list') @@ -176,6 +181,7 @@ describe('', () => { const { getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/tcp/services/mock-service', withPage: true }, ) expect(() => { diff --git a/webui/src/pages/tcp/TcpService.tsx b/webui/src/pages/tcp/TcpService.tsx index f2f3ef1fe..c36106156 100644 --- a/webui/src/pages/tcp/TcpService.tsx +++ b/webui/src/pages/tcp/TcpService.tsx @@ -1,5 +1,6 @@ import { Box, Flex, H1, Skeleton, styled, Text } from '@traefiklabs/faency' import { useMemo } from 'react' +import { Helmet } from 'react-helmet-async' import { FiGlobe, FiInfo, FiShield } from 'react-icons/fi' import { useParams } from 'react-router-dom' @@ -16,7 +17,6 @@ import { ResourceStatus } from 'components/resources/ResourceStatus' import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection' import Tooltip from 'components/Tooltip' import { ResourceDetailDataType, ServiceDetailType, useResourceDetail } from 'hooks/use-resource-detail' -import Page from 'layout/Page' import { NotFound } from 'pages/NotFound' type TcpDetailProps = { @@ -238,17 +238,23 @@ type TcpServiceRenderProps = { export const TcpServiceRender = ({ data, error, name }: TcpServiceRenderProps) => { if (error) { return ( - + <> + + {name} - Traefik Proxy + Sorry, we could not fetch detail information for this Service right now. Please, try again later. - + ) } if (!data) { return ( - + <> + + {name} - Traefik Proxy + @@ -256,7 +262,7 @@ export const TcpServiceRender = ({ data, error, name }: TcpServiceRenderProps) = - + ) } @@ -265,11 +271,14 @@ export const TcpServiceRender = ({ data, error, name }: TcpServiceRenderProps) = } return ( - + <> + + {data.name} - Traefik Proxy +

{data.name}

-
+ ) } diff --git a/webui/src/pages/tcp/TcpServices.spec.tsx b/webui/src/pages/tcp/TcpServices.spec.tsx index cd838bee4..336a26a54 100644 --- a/webui/src/pages/tcp/TcpServices.spec.tsx +++ b/webui/src/pages/tcp/TcpServices.spec.tsx @@ -36,10 +36,13 @@ describe('', () => { .spyOn(useFetchWithPagination, 'default') .mockImplementation(() => useFetchWithPaginationMock({ pages })) - const { container, getByTestId } = renderWithProviders() + const { container, getByTestId } = renderWithProviders(, { + route: '/tcp/services', + withPage: true, + }) expect(mock).toHaveBeenCalled() - expect(getByTestId('TCP Services page')).toBeInTheDocument() + expect(getByTestId('/tcp/services page')).toBeInTheDocument() const tbody = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[1] expect(tbody.querySelectorAll('a[role="row"]')).toHaveLength(3) @@ -73,6 +76,7 @@ describe('', () => { pageCount={1} pages={[]} />, + { route: '/tcp/services', withPage: true }, ) expect(() => getByTestId('loading')).toThrow('Unable to find an element by: [data-testid="loading"]') const tfoot = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[2] diff --git a/webui/src/pages/tcp/TcpServices.tsx b/webui/src/pages/tcp/TcpServices.tsx index a56027b61..77480fd4e 100644 --- a/webui/src/pages/tcp/TcpServices.tsx +++ b/webui/src/pages/tcp/TcpServices.tsx @@ -1,5 +1,6 @@ import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex, Text } from '@traefiklabs/faency' import { useMemo } from 'react' +import { Helmet } from 'react-helmet-async' import useInfiniteScroll from 'react-infinite-scroll-hook' import { useSearchParams } from 'react-router-dom' @@ -14,7 +15,6 @@ import Tooltip from 'components/Tooltip' import TooltipText from 'components/TooltipText' import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination' import { EmptyPlaceholder } from 'layout/EmptyPlaceholder' -import Page from 'layout/Page' export const makeRowRender = (): RenderRowType => { const TcpServicesRenderRow = (row) => ( @@ -108,7 +108,10 @@ export const TcpServices = () => { ) return ( - + <> + + TCP Services - Traefik Proxy + { pageCount={pageCount} pages={pages} /> - + ) } diff --git a/webui/src/pages/udp/UdpRouter.spec.tsx b/webui/src/pages/udp/UdpRouter.spec.tsx index 65aa5567d..2404b70a3 100644 --- a/webui/src/pages/udp/UdpRouter.spec.tsx +++ b/webui/src/pages/udp/UdpRouter.spec.tsx @@ -7,6 +7,7 @@ describe('', () => { it('should render the error message', () => { const { getByTestId } = renderWithProviders( , + { route: '/udp/routers/mock-router', withPage: true }, ) expect(getByTestId('error-text')).toBeInTheDocument() }) @@ -14,6 +15,7 @@ describe('', () => { it('should render the skeleton', () => { const { getByTestId } = renderWithProviders( , + { route: '/udp/routers/mock-router', withPage: true }, ) expect(getByTestId('skeleton')).toBeInTheDocument() }) @@ -21,6 +23,7 @@ describe('', () => { it('should render the not found page', () => { const { getByTestId } = renderWithProviders( , + { route: '/udp/routers/mock-router', withPage: true }, ) expect(getByTestId('Not found page')).toBeInTheDocument() }) @@ -51,6 +54,7 @@ describe('', () => { const { getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/udp/routers/udp-all@docker', withPage: true }, ) const routerStructure = getByTestId('router-structure') diff --git a/webui/src/pages/udp/UdpRouter.tsx b/webui/src/pages/udp/UdpRouter.tsx index d1aebf1e8..a41bdb0df 100644 --- a/webui/src/pages/udp/UdpRouter.tsx +++ b/webui/src/pages/udp/UdpRouter.tsx @@ -1,10 +1,10 @@ import { Flex, styled, Text } from '@traefiklabs/faency' +import { Helmet } from 'react-helmet-async' import { useParams } from 'react-router-dom' import { CardListSection, DetailSectionSkeleton } from 'components/resources/DetailSections' import RouterPanel from 'components/resources/RouterPanel' import { ResourceDetailDataType, useResourceDetail } from 'hooks/use-resource-detail' -import Page from 'layout/Page' import { RouterStructure } from 'pages/http/HttpRouter' import { NotFound } from 'pages/NotFound' @@ -33,17 +33,23 @@ type UdpRouterRenderProps = { export const UdpRouterRender = ({ data, error, name }: UdpRouterRenderProps) => { if (error) { return ( - + <> + + {name} - Traefik Proxy + Sorry, we could not fetch detail information for this Router right now. Please, try again later. - + ) } if (!data) { return ( - + <> + + {name} - Traefik Proxy + @@ -53,7 +59,7 @@ export const UdpRouterRender = ({ data, error, name }: UdpRouterRenderProps) => - + ) } @@ -62,10 +68,13 @@ export const UdpRouterRender = ({ data, error, name }: UdpRouterRenderProps) => } return ( - + <> + + {data.name} - Traefik Proxy + - + ) } diff --git a/webui/src/pages/udp/UdpRouters.spec.tsx b/webui/src/pages/udp/UdpRouters.spec.tsx index 8045cf8b7..658b8843b 100644 --- a/webui/src/pages/udp/UdpRouters.spec.tsx +++ b/webui/src/pages/udp/UdpRouters.spec.tsx @@ -39,10 +39,13 @@ describe('', () => { .spyOn(useFetchWithPagination, 'default') .mockImplementation(() => useFetchWithPaginationMock({ pages })) - const { container, getByTestId } = renderWithProviders() + const { container, getByTestId } = renderWithProviders(, { + route: '/udp/routers', + withPage: true, + }) expect(mock).toHaveBeenCalled() - expect(getByTestId('UDP Routers page')).toBeInTheDocument() + expect(getByTestId('/udp/routers page')).toBeInTheDocument() const tbody = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[1] expect(tbody.querySelectorAll('a[role="row"]')).toHaveLength(3) @@ -76,6 +79,7 @@ describe('', () => { pageCount={1} pages={[]} />, + { route: '/udp/routers', withPage: true }, ) expect(() => getByTestId('loading')).toThrow('Unable to find an element by: [data-testid="loading"]') const tfoot = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[2] diff --git a/webui/src/pages/udp/UdpRouters.tsx b/webui/src/pages/udp/UdpRouters.tsx index 56c6d8414..ce6348b75 100644 --- a/webui/src/pages/udp/UdpRouters.tsx +++ b/webui/src/pages/udp/UdpRouters.tsx @@ -1,5 +1,6 @@ import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex } from '@traefiklabs/faency' import { useMemo } from 'react' +import { Helmet } from 'react-helmet-async' import useInfiniteScroll from 'react-infinite-scroll-hook' import { useSearchParams } from 'react-router-dom' @@ -15,7 +16,6 @@ import Tooltip from 'components/Tooltip' import TooltipText from 'components/TooltipText' import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination' import { EmptyPlaceholder } from 'layout/EmptyPlaceholder' -import Page from 'layout/Page' export const makeRowRender = (): RenderRowType => { const UdpRoutersRenderRow = (row) => ( @@ -111,7 +111,10 @@ export const UdpRouters = () => { ) return ( - + <> + + UDP Routers - Traefik Proxy + { pageCount={pageCount} pages={pages} /> - + ) } diff --git a/webui/src/pages/udp/UdpService.spec.tsx b/webui/src/pages/udp/UdpService.spec.tsx index 71114f560..b6150c8ee 100644 --- a/webui/src/pages/udp/UdpService.spec.tsx +++ b/webui/src/pages/udp/UdpService.spec.tsx @@ -7,6 +7,7 @@ describe('', () => { it('should render the error message', () => { const { getByTestId } = renderWithProviders( , + { route: '/udp/services/mock-service', withPage: true }, ) expect(getByTestId('error-text')).toBeInTheDocument() }) @@ -14,6 +15,7 @@ describe('', () => { it('should render the skeleton', () => { const { getByTestId } = renderWithProviders( , + { route: '/udp/services/mock-service', withPage: true }, ) expect(getByTestId('skeleton')).toBeInTheDocument() }) @@ -21,6 +23,7 @@ describe('', () => { it('should render the not found page', () => { const { getByTestId } = renderWithProviders( , + { route: '/udp/services/mock-service', withPage: true }, ) expect(getByTestId('Not found page')).toBeInTheDocument() }) @@ -59,6 +62,7 @@ describe('', () => { const { container, getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/udp/services/mock-service', withPage: true }, ) const headings = Array.from(container.getElementsByTagName('h1')) @@ -128,6 +132,7 @@ describe('', () => { const { getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/udp/services/mock-service', withPage: true }, ) const serversList = getByTestId('servers-list') @@ -154,6 +159,7 @@ describe('', () => { const { getByTestId } = renderWithProviders( // eslint-disable-next-line @typescript-eslint/no-explicit-any , + { route: '/udp/services/mock-service', withPage: true }, ) expect(() => { diff --git a/webui/src/pages/udp/UdpService.tsx b/webui/src/pages/udp/UdpService.tsx index 9727a1663..132e63197 100644 --- a/webui/src/pages/udp/UdpService.tsx +++ b/webui/src/pages/udp/UdpService.tsx @@ -1,10 +1,10 @@ import { Flex, H1, Skeleton, styled, Text } from '@traefiklabs/faency' +import { Helmet } from 'react-helmet-async' import { useParams } from 'react-router-dom' import { DetailSectionSkeleton } from 'components/resources/DetailSections' import { UsedByRoutersSection, UsedByRoutersSkeleton } from 'components/resources/UsedByRoutersSection' import { ResourceDetailDataType, useResourceDetail } from 'hooks/use-resource-detail' -import Page from 'layout/Page' import { ServicePanels } from 'pages/http/HttpService' import { NotFound } from 'pages/NotFound' @@ -23,24 +23,30 @@ type UdpServiceRenderProps = { export const UdpServiceRender = ({ data, error, name }: UdpServiceRenderProps) => { if (error) { return ( - + <> + + {name} - Traefik Proxy + Sorry, we could not fetch detail information for this Service right now. Please, try again later. - + ) } if (!data) { return ( - + <> + + {name} - Traefik Proxy + - + ) } @@ -49,11 +55,14 @@ export const UdpServiceRender = ({ data, error, name }: UdpServiceRenderProps) = } return ( - + <> + + {data.name} - Traefik Proxy +

{data.name}

-
+ ) } diff --git a/webui/src/pages/udp/UdpServices.spec.tsx b/webui/src/pages/udp/UdpServices.spec.tsx index 8bb51ef0d..e28d1dba3 100644 --- a/webui/src/pages/udp/UdpServices.spec.tsx +++ b/webui/src/pages/udp/UdpServices.spec.tsx @@ -36,10 +36,13 @@ describe('', () => { .spyOn(useFetchWithPagination, 'default') .mockImplementation(() => useFetchWithPaginationMock({ pages })) - const { container, getByTestId } = renderWithProviders() + const { container, getByTestId } = renderWithProviders(, { + route: '/udp/services', + withPage: true, + }) expect(mock).toHaveBeenCalled() - expect(getByTestId('UDP Services page')).toBeInTheDocument() + expect(getByTestId('/udp/services page')).toBeInTheDocument() const tbody = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[1] expect(tbody.querySelectorAll('a[role="row"]')).toHaveLength(3) @@ -73,6 +76,7 @@ describe('', () => { pageCount={1} pages={[]} />, + { route: '/udp/services', withPage: true }, ) expect(() => getByTestId('loading')).toThrow('Unable to find an element by: [data-testid="loading"]') const tfoot = container.querySelectorAll('div[role="table"] > div[role="rowgroup"]')[2] diff --git a/webui/src/pages/udp/UdpServices.tsx b/webui/src/pages/udp/UdpServices.tsx index b02c2fdcf..5d6b47c66 100644 --- a/webui/src/pages/udp/UdpServices.tsx +++ b/webui/src/pages/udp/UdpServices.tsx @@ -1,5 +1,6 @@ import { AriaTable, AriaTbody, AriaTd, AriaTfoot, AriaThead, AriaTr, Box, Flex, Text } from '@traefiklabs/faency' import { useMemo } from 'react' +import { Helmet } from 'react-helmet-async' import useInfiniteScroll from 'react-infinite-scroll-hook' import { useSearchParams } from 'react-router-dom' @@ -14,7 +15,6 @@ import Tooltip from 'components/Tooltip' import TooltipText from 'components/TooltipText' import useFetchWithPagination, { pagesResponseInterface, RenderRowType } from 'hooks/use-fetch-with-pagination' import { EmptyPlaceholder } from 'layout/EmptyPlaceholder' -import Page from 'layout/Page' export const makeRowRender = (): RenderRowType => { const UdpServicesRenderRow = (row) => ( @@ -108,7 +108,10 @@ export const UdpServices = () => { ) return ( - + <> + + UDP Services - Traefik Proxy + { pageCount={pageCount} pages={pages} /> - + ) } diff --git a/webui/src/types/global.d.ts b/webui/src/types/global.d.ts index e1503ed42..4b024488c 100644 --- a/webui/src/types/global.d.ts +++ b/webui/src/types/global.d.ts @@ -5,5 +5,6 @@ interface Window { declare namespace JSX { interface IntrinsicElements { 'hub-button-app': React.DetailedHTMLProps, HTMLElement> + 'hub-ui-demo-app': { key: string; path: string; theme: 'dark' | 'light'; baseurl: string; containercss: string } } } diff --git a/webui/src/utils/test.tsx b/webui/src/utils/test.tsx index 2c79760cf..995908e9f 100644 --- a/webui/src/utils/test.tsx +++ b/webui/src/utils/test.tsx @@ -1,10 +1,11 @@ import { cleanup, render } from '@testing-library/react' import { FaencyProvider } from '@traefiklabs/faency' import { HelmetProvider } from 'react-helmet-async' -import { BrowserRouter } from 'react-router-dom' +import { MemoryRouter } from 'react-router-dom' import { SWRConfig } from 'swr' import { afterEach } from 'vitest' +import Page from '../layout/Page' import fetch from '../libs/fetch' afterEach(() => { @@ -25,7 +26,7 @@ export { default as userEvent } from '@testing-library/user-event' // override render export export { customRender as render } // eslint-disable-line import/export -export function renderWithProviders(ui: React.ReactElement) { +export function renderWithProviders(ui: React.ReactElement, { route = '/', withPage = false } = {}) { return customRender(ui, { wrapper: ({ children }) => ( @@ -36,7 +37,7 @@ export function renderWithProviders(ui: React.ReactElement) { fetcher: fetch, }} > - {children} + {withPage ? {children} : children} diff --git a/webui/test/setup.ts b/webui/test/setup.ts index 7c53d16b8..fe734b9b7 100644 --- a/webui/test/setup.ts +++ b/webui/test/setup.ts @@ -1,5 +1,6 @@ import '@testing-library/jest-dom' import 'vitest-canvas-mock' +import '@vitest/web-worker' import * as matchers from 'jest-extended' import { expect } from 'vitest' @@ -12,6 +13,7 @@ export class IntersectionObserver { root = null rootMargin = '' thresholds = [] + scrollMargin = '' disconnect() { return null @@ -43,10 +45,10 @@ class ResizeObserver { } beforeAll(() => { - global.IntersectionObserver = IntersectionObserver + globalThis.IntersectionObserver = IntersectionObserver window.IntersectionObserver = IntersectionObserver - global.ResizeObserver = ResizeObserver + globalThis.ResizeObserver = ResizeObserver window.ResizeObserver = ResizeObserver Object.defineProperty(window, 'matchMedia', { diff --git a/webui/yarn.lock b/webui/yarn.lock index 3934fc003..741958b91 100644 --- a/webui/yarn.lock +++ b/webui/yarn.lock @@ -2485,6 +2485,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.5.5": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" + checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" @@ -2561,6 +2568,20 @@ __metadata: languageName: node linkType: hard +"@noble/ed25519@npm:^3.0.0": + version: 3.0.0 + resolution: "@noble/ed25519@npm:3.0.0" + checksum: 10c0/6355aed04523d063d3e9a9952926af4a0eaa722e08133f558d44963a3048bf6dae02cae9032d42238d1fcb2a93ef27658f2baf4cf07939457717a4337a89dc26 + languageName: node + linkType: hard + +"@noble/hashes@npm:^2.0.1": + version: 2.0.1 + resolution: "@noble/hashes@npm:2.0.1" + checksum: 10c0/e81769ce21c3b1c80141a3b99bd001f17edea09879aa936692ae39525477386d696101cd573928a304806efb2b9fa751e1dd83241c67d0c84d30091e85c79bdb + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -4113,9 +4134,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.46.1" +"@rollup/rollup-android-arm-eabi@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.52.5" conditions: os=android & cpu=arm languageName: node linkType: hard @@ -4127,9 +4148,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-android-arm64@npm:4.46.1" +"@rollup/rollup-android-arm64@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-android-arm64@npm:4.52.5" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -4141,9 +4162,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-darwin-arm64@npm:4.46.1" +"@rollup/rollup-darwin-arm64@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-darwin-arm64@npm:4.52.5" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -4155,9 +4176,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-darwin-x64@npm:4.46.1" +"@rollup/rollup-darwin-x64@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-darwin-x64@npm:4.52.5" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -4169,9 +4190,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.46.1" +"@rollup/rollup-freebsd-arm64@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.52.5" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard @@ -4183,9 +4204,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-freebsd-x64@npm:4.46.1" +"@rollup/rollup-freebsd-x64@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-freebsd-x64@npm:4.52.5" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -4197,9 +4218,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.46.1" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.52.5" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard @@ -4211,9 +4232,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.46.1" +"@rollup/rollup-linux-arm-musleabihf@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.52.5" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard @@ -4225,9 +4246,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.46.1" +"@rollup/rollup-linux-arm64-gnu@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.52.5" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard @@ -4239,13 +4260,20 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.46.1" +"@rollup/rollup-linux-arm64-musl@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.52.5" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard +"@rollup/rollup-linux-loong64-gnu@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.52.5" + conditions: os=linux & cpu=loong64 & libc=glibc + languageName: node + linkType: hard + "@rollup/rollup-linux-loongarch64-gnu@npm:4.39.0": version: 4.39.0 resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.39.0" @@ -4253,13 +4281,6 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.46.1" - conditions: os=linux & cpu=loong64 & libc=glibc - languageName: node - linkType: hard - "@rollup/rollup-linux-powerpc64le-gnu@npm:4.39.0": version: 4.39.0 resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.39.0" @@ -4267,9 +4288,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-gnu@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.46.1" +"@rollup/rollup-linux-ppc64-gnu@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.52.5" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard @@ -4281,9 +4302,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.46.1" +"@rollup/rollup-linux-riscv64-gnu@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.52.5" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard @@ -4295,9 +4316,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.46.1" +"@rollup/rollup-linux-riscv64-musl@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.52.5" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard @@ -4309,9 +4330,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.46.1" +"@rollup/rollup-linux-s390x-gnu@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.52.5" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard @@ -4323,9 +4344,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.46.1" +"@rollup/rollup-linux-x64-gnu@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.52.5" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard @@ -4337,13 +4358,20 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.46.1" +"@rollup/rollup-linux-x64-musl@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.52.5" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard +"@rollup/rollup-openharmony-arm64@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.52.5" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@rollup/rollup-win32-arm64-msvc@npm:4.39.0": version: 4.39.0 resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.39.0" @@ -4351,9 +4379,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.46.1" +"@rollup/rollup-win32-arm64-msvc@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.52.5" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -4365,13 +4393,20 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.46.1" +"@rollup/rollup-win32-ia32-msvc@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.52.5" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard +"@rollup/rollup-win32-x64-gnu@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.52.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rollup/rollup-win32-x64-msvc@npm:4.39.0": version: 4.39.0 resolution: "@rollup/rollup-win32-x64-msvc@npm:4.39.0" @@ -4379,9 +4414,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.46.1": - version: 4.46.1 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.46.1" +"@rollup/rollup-win32-x64-msvc@npm:4.52.5": + version: 4.52.5 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.52.5" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -4522,6 +4557,13 @@ __metadata: languageName: node linkType: hard +"@standard-schema/spec@npm:^1.0.0": + version: 1.0.0 + resolution: "@standard-schema/spec@npm:1.0.0" + checksum: 10c0/a1ab9a8bdc09b5b47aa8365d0e0ec40cc2df6437be02853696a0e377321653b0d3ac6f079a8c67d5ddbe9821025584b1fb71d9cc041a6666a96f1fadf2ece15f + languageName: node + linkType: hard + "@stitches/react@npm:1.2.7": version: 1.2.7 resolution: "@stitches/react@npm:1.2.7" @@ -5838,86 +5880,94 @@ __metadata: languageName: node linkType: hard -"@vitest/expect@npm:3.2.4": - version: 3.2.4 - resolution: "@vitest/expect@npm:3.2.4" +"@vitest/expect@npm:4.0.3": + version: 4.0.3 + resolution: "@vitest/expect@npm:4.0.3" dependencies: + "@standard-schema/spec": "npm:^1.0.0" "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:3.2.4" - "@vitest/utils": "npm:3.2.4" - chai: "npm:^5.2.0" - tinyrainbow: "npm:^2.0.0" - checksum: 10c0/7586104e3fd31dbe1e6ecaafb9a70131e4197dce2940f727b6a84131eee3decac7b10f9c7c72fa5edbdb68b6f854353bd4c0fa84779e274207fb7379563b10db + "@vitest/spy": "npm:4.0.3" + "@vitest/utils": "npm:4.0.3" + chai: "npm:^6.0.1" + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/36a9ff769387f4475fea273ec3ee39553ed7607bda620eea3a3632cd88a1e8c97173abcce7bc4440c84bf55b8e752edacb519a64f02d7ad5b47df7770cb56883 languageName: node linkType: hard -"@vitest/mocker@npm:3.2.4": - version: 3.2.4 - resolution: "@vitest/mocker@npm:3.2.4" +"@vitest/mocker@npm:4.0.3": + version: 4.0.3 + resolution: "@vitest/mocker@npm:4.0.3" dependencies: - "@vitest/spy": "npm:3.2.4" + "@vitest/spy": "npm:4.0.3" estree-walker: "npm:^3.0.3" - magic-string: "npm:^0.30.17" + magic-string: "npm:^0.30.19" peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0-0 peerDependenciesMeta: msw: optional: true vite: optional: true - checksum: 10c0/f7a4aea19bbbf8f15905847ee9143b6298b2c110f8b64789224cb0ffdc2e96f9802876aa2ca83f1ec1b6e1ff45e822abb34f0054c24d57b29ab18add06536ccd + checksum: 10c0/16c9ef064a55b0fd3e0d7adaddfeef5c41d569d8181c40d8fed75464b43445305b83c017b50c5ea88f49355bef64e32f75cfd35db0682e0a8f5bbcc3acbde264 languageName: node linkType: hard -"@vitest/pretty-format@npm:3.2.4, @vitest/pretty-format@npm:^3.2.4": - version: 3.2.4 - resolution: "@vitest/pretty-format@npm:3.2.4" +"@vitest/pretty-format@npm:4.0.3": + version: 4.0.3 + resolution: "@vitest/pretty-format@npm:4.0.3" dependencies: - tinyrainbow: "npm:^2.0.0" - checksum: 10c0/5ad7d4278e067390d7d633e307fee8103958806a419ca380aec0e33fae71b44a64415f7a9b4bc11635d3c13d4a9186111c581d3cef9c65cc317e68f077456887 + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/031080fbcb16b42c511ef7553a0cdabcb78d0bf9a2bb960a03403ff4553ce05d15e13af4abe7f22bd187f369d3bfa1c59714a0b4d9db33313d7583346511f236 languageName: node linkType: hard -"@vitest/runner@npm:3.2.4": - version: 3.2.4 - resolution: "@vitest/runner@npm:3.2.4" +"@vitest/runner@npm:4.0.3": + version: 4.0.3 + resolution: "@vitest/runner@npm:4.0.3" dependencies: - "@vitest/utils": "npm:3.2.4" + "@vitest/utils": "npm:4.0.3" pathe: "npm:^2.0.3" - strip-literal: "npm:^3.0.0" - checksum: 10c0/e8be51666c72b3668ae3ea348b0196656a4a5adb836cb5e270720885d9517421815b0d6c98bfdf1795ed02b994b7bfb2b21566ee356a40021f5bf4f6ed4e418a + checksum: 10c0/77a4c76890d652115baded6004beb13f5f84e98d0a5175c38b3908f03875d6c02df998430e43b21bf324de9e1499069a54cf773854447607602cab5d9fe8cd60 languageName: node linkType: hard -"@vitest/snapshot@npm:3.2.4": - version: 3.2.4 - resolution: "@vitest/snapshot@npm:3.2.4" +"@vitest/snapshot@npm:4.0.3": + version: 4.0.3 + resolution: "@vitest/snapshot@npm:4.0.3" dependencies: - "@vitest/pretty-format": "npm:3.2.4" - magic-string: "npm:^0.30.17" + "@vitest/pretty-format": "npm:4.0.3" + magic-string: "npm:^0.30.19" pathe: "npm:^2.0.3" - checksum: 10c0/f8301a3d7d1559fd3d59ed51176dd52e1ed5c2d23aa6d8d6aa18787ef46e295056bc726a021698d8454c16ed825ecba163362f42fa90258bb4a98cfd2c9424fc + checksum: 10c0/69e6a3ebdf3852cb949237989aa88c73ead9e6a1debd2227d130e0ec3c33a0bc07ab2d16538a247b8eea0c105f0c9f62419700bbc9230edc99d1df8083bf7b0a languageName: node linkType: hard -"@vitest/spy@npm:3.2.4": - version: 3.2.4 - resolution: "@vitest/spy@npm:3.2.4" - dependencies: - tinyspy: "npm:^4.0.3" - checksum: 10c0/6ebf0b4697dc238476d6b6a60c76ba9eb1dd8167a307e30f08f64149612fd50227682b876420e4c2e09a76334e73f72e3ebf0e350714dc22474258292e202024 +"@vitest/spy@npm:4.0.3": + version: 4.0.3 + resolution: "@vitest/spy@npm:4.0.3" + checksum: 10c0/185fc2621c44c974fe2d89523cc8e20db46b4213fb7f153b63c4091c744b14e1e2c79f6160a3db0aab3e91775c6974214499c643032a02c2261cc68692b4cad8 languageName: node linkType: hard -"@vitest/utils@npm:3.2.4": - version: 3.2.4 - resolution: "@vitest/utils@npm:3.2.4" +"@vitest/utils@npm:4.0.3": + version: 4.0.3 + resolution: "@vitest/utils@npm:4.0.3" dependencies: - "@vitest/pretty-format": "npm:3.2.4" - loupe: "npm:^3.1.4" - tinyrainbow: "npm:^2.0.0" - checksum: 10c0/024a9b8c8bcc12cf40183c246c244b52ecff861c6deb3477cbf487ac8781ad44c68a9c5fd69f8c1361878e55b97c10d99d511f2597f1f7244b5e5101d028ba64 + "@vitest/pretty-format": "npm:4.0.3" + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/b773bb0133307176762a932bab0480e77c9b378406eb92c82f37a16d214b1d873a0378e4c567c4b05bf4628073bc7fc18b519a7c0bdaf75e18311545ca00c5ba + languageName: node + linkType: hard + +"@vitest/web-worker@npm:^4.0.2": + version: 4.0.2 + resolution: "@vitest/web-worker@npm:4.0.2" + dependencies: + debug: "npm:^4.4.3" + peerDependencies: + vitest: 4.0.2 + checksum: 10c0/d30b32b8c7e4df28fbe099125e62f3fc3d0e48a852b3749cbb47963e33070a4c04b3f55bfaf1f90109eacd6bc4d13b4b8028be984033edda532a8372a5abed86 languageName: node linkType: hard @@ -6421,13 +6471,6 @@ __metadata: languageName: node linkType: hard -"assertion-error@npm:^2.0.1": - version: 2.0.1 - resolution: "assertion-error@npm:2.0.1" - checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 - languageName: node - linkType: hard - "ast-types-flow@npm:^0.0.8": version: 0.0.8 resolution: "ast-types-flow@npm:0.0.8" @@ -6756,13 +6799,6 @@ __metadata: languageName: node linkType: hard -"cac@npm:^6.7.14": - version: 6.7.14 - resolution: "cac@npm:6.7.14" - checksum: 10c0/4ee06aaa7bab8981f0d54e5f5f9d4adcd64058e9697563ce336d8a3878ed018ee18ebe5359b2430eceae87e0758e62ea2019c3f52ae6e211b1bd2e133856cd10 - languageName: node - linkType: hard - "cacache@npm:^16.0.0, cacache@npm:^16.1.0, cacache@npm:^16.1.3": version: 16.1.3 resolution: "cacache@npm:16.1.3" @@ -6950,9 +6986,9 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001688": - version: 1.0.30001712 - resolution: "caniuse-lite@npm:1.0.30001712" - checksum: 10c0/b3df8bdcc3335969380c2e47acb36c89bfc7f8fb4ef7ee2a5380e30ba46aa69e9d411654bc29894a06c201a1d60d490ab9b92787f3b66d7a7a38d71360e68215 + version: 1.0.30001751 + resolution: "caniuse-lite@npm:1.0.30001751" + checksum: 10c0/c3f2d448f3569004ace160fd9379ea0def8e7a7bc6e65611baadb57d24e1f418258647a6210e46732419f5663e2356c22aa841f92449dd3849eb6471bb7ad592 languageName: node linkType: hard @@ -6968,16 +7004,10 @@ __metadata: languageName: node linkType: hard -"chai@npm:^5.2.0": - version: 5.2.1 - resolution: "chai@npm:5.2.1" - dependencies: - assertion-error: "npm:^2.0.1" - check-error: "npm:^2.1.1" - deep-eql: "npm:^5.0.1" - loupe: "npm:^3.1.0" - pathval: "npm:^2.0.0" - checksum: 10c0/58209c03ae9b2fd97cfa1cb0fbe372b1906e6091311b9ba1b0468cc4923b0766a50a1050a164df3ccefb9464944c9216b632f1477c9e429068013bdbb57220f6 +"chai@npm:^6.0.1": + version: 6.2.0 + resolution: "chai@npm:6.2.0" + checksum: 10c0/a4b7d7f5907187e09f1847afa838d6d1608adc7d822031b7900813c4ed5d9702911ac2468bf290676f22fddb3d727b1be90b57c1d0a69b902534ee29cdc6ff8a languageName: node linkType: hard @@ -7048,13 +7078,6 @@ __metadata: languageName: node linkType: hard -"check-error@npm:^2.1.1": - version: 2.1.1 - resolution: "check-error@npm:2.1.1" - checksum: 10c0/979f13eccab306cf1785fa10941a590b4e7ea9916ea2a4f8c87f0316fc3eab07eabefb6e587424ef0f88cbcd3805791f172ea739863ca3d7ce2afc54641c7f0e - languageName: node - linkType: hard - "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" @@ -7761,6 +7784,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.4.3": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 + languageName: node + linkType: hard + "debuglog@npm:^1.0.1": version: 1.0.1 resolution: "debuglog@npm:1.0.1" @@ -7815,13 +7850,6 @@ __metadata: languageName: node linkType: hard -"deep-eql@npm:^5.0.1": - version: 5.0.2 - resolution: "deep-eql@npm:5.0.2" - checksum: 10c0/7102cf3b7bb719c6b9c0db2e19bf0aa9318d141581befe8c7ce8ccd39af9eaa4346e5e05adef7f9bd7015da0f13a3a25dcfe306ef79dc8668aedbecb658dd247 - languageName: node - linkType: hard - "deep-equal@npm:^2.0.5": version: 2.2.3 resolution: "deep-equal@npm:2.2.3" @@ -9193,7 +9221,7 @@ __metadata: languageName: node linkType: hard -"expect-type@npm:^1.2.1": +"expect-type@npm:^1.2.2": version: 1.2.2 resolution: "expect-type@npm:1.2.2" checksum: 10c0/6019019566063bbc7a690d9281d920b1a91284a4a093c2d55d71ffade5ac890cf37a51e1da4602546c4b56569d2ad2fc175a2ccee77d1ae06cb3af91ef84f44b @@ -9293,7 +9321,7 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.4.4, fdir@npm:^6.4.6": +"fdir@npm:^6.4.4": version: 6.4.6 resolution: "fdir@npm:6.4.6" peerDependencies: @@ -9305,6 +9333,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f + languageName: node + linkType: hard + "figures@npm:^1.7.0": version: 1.7.0 resolution: "figures@npm:1.7.0" @@ -12209,13 +12249,6 @@ __metadata: languageName: node linkType: hard -"loupe@npm:^3.1.0, loupe@npm:^3.1.4": - version: 3.2.0 - resolution: "loupe@npm:3.2.0" - checksum: 10c0/f572fd9e38db8d36ae9eede305480686e310d69bc40394b6842838ebc6c3860a0e35ab30182f33606ab2d8a685d9ff6436649269f8218a1c3385ca329973cb2c - languageName: node - linkType: hard - "lowercase-keys@npm:^2.0.0": version: 2.0.0 resolution: "lowercase-keys@npm:2.0.0" @@ -12289,6 +12322,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.19": + version: 0.30.21 + resolution: "magic-string@npm:0.30.21" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10c0/299378e38f9a270069fc62358522ddfb44e94244baa0d6a8980ab2a9b2490a1d03b236b447eee309e17eb3bddfa482c61259d47960eb018a904f0ded52780c4a + languageName: node + linkType: hard + "magicast@npm:^0.3.5": version: 0.3.5 resolution: "magicast@npm:0.3.5" @@ -14099,13 +14141,6 @@ __metadata: languageName: node linkType: hard -"pathval@npm:^2.0.0": - version: 2.0.1 - resolution: "pathval@npm:2.0.1" - checksum: 10c0/460f4709479fbf2c45903a65655fc8f0a5f6d808f989173aeef5fdea4ff4f303dc13f7870303999add60ec49d4c14733895c0a869392e9866f1091fa64fd7581 - languageName: node - linkType: hard - "picocolors@npm:1.1.1, picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -15326,30 +15361,32 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.40.0": - version: 4.46.1 - resolution: "rollup@npm:4.46.1" +"rollup@npm:^4.43.0": + version: 4.52.5 + resolution: "rollup@npm:4.52.5" dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.46.1" - "@rollup/rollup-android-arm64": "npm:4.46.1" - "@rollup/rollup-darwin-arm64": "npm:4.46.1" - "@rollup/rollup-darwin-x64": "npm:4.46.1" - "@rollup/rollup-freebsd-arm64": "npm:4.46.1" - "@rollup/rollup-freebsd-x64": "npm:4.46.1" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.46.1" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.46.1" - "@rollup/rollup-linux-arm64-gnu": "npm:4.46.1" - "@rollup/rollup-linux-arm64-musl": "npm:4.46.1" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.46.1" - "@rollup/rollup-linux-ppc64-gnu": "npm:4.46.1" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.46.1" - "@rollup/rollup-linux-riscv64-musl": "npm:4.46.1" - "@rollup/rollup-linux-s390x-gnu": "npm:4.46.1" - "@rollup/rollup-linux-x64-gnu": "npm:4.46.1" - "@rollup/rollup-linux-x64-musl": "npm:4.46.1" - "@rollup/rollup-win32-arm64-msvc": "npm:4.46.1" - "@rollup/rollup-win32-ia32-msvc": "npm:4.46.1" - "@rollup/rollup-win32-x64-msvc": "npm:4.46.1" + "@rollup/rollup-android-arm-eabi": "npm:4.52.5" + "@rollup/rollup-android-arm64": "npm:4.52.5" + "@rollup/rollup-darwin-arm64": "npm:4.52.5" + "@rollup/rollup-darwin-x64": "npm:4.52.5" + "@rollup/rollup-freebsd-arm64": "npm:4.52.5" + "@rollup/rollup-freebsd-x64": "npm:4.52.5" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.52.5" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.52.5" + "@rollup/rollup-linux-arm64-gnu": "npm:4.52.5" + "@rollup/rollup-linux-arm64-musl": "npm:4.52.5" + "@rollup/rollup-linux-loong64-gnu": "npm:4.52.5" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.52.5" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.52.5" + "@rollup/rollup-linux-riscv64-musl": "npm:4.52.5" + "@rollup/rollup-linux-s390x-gnu": "npm:4.52.5" + "@rollup/rollup-linux-x64-gnu": "npm:4.52.5" + "@rollup/rollup-linux-x64-musl": "npm:4.52.5" + "@rollup/rollup-openharmony-arm64": "npm:4.52.5" + "@rollup/rollup-win32-arm64-msvc": "npm:4.52.5" + "@rollup/rollup-win32-ia32-msvc": "npm:4.52.5" + "@rollup/rollup-win32-x64-gnu": "npm:4.52.5" + "@rollup/rollup-win32-x64-msvc": "npm:4.52.5" "@types/estree": "npm:1.0.8" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -15373,7 +15410,7 @@ __metadata: optional: true "@rollup/rollup-linux-arm64-musl": optional: true - "@rollup/rollup-linux-loongarch64-gnu": + "@rollup/rollup-linux-loong64-gnu": optional: true "@rollup/rollup-linux-ppc64-gnu": optional: true @@ -15387,17 +15424,21 @@ __metadata: optional: true "@rollup/rollup-linux-x64-musl": optional: true + "@rollup/rollup-openharmony-arm64": + optional: true "@rollup/rollup-win32-arm64-msvc": optional: true "@rollup/rollup-win32-ia32-msvc": optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true "@rollup/rollup-win32-x64-msvc": optional: true fsevents: optional: true bin: rollup: dist/bin/rollup - checksum: 10c0/84297d63a97bf8fc131039d0f600787ada8baaa38b744820f7d29da601bce9f70a14aa12da80843b2b49eb8660718576f441ee9952954efa16009d7ccebf0000 + checksum: 10c0/faf1697b305d13a149bb64a2bb7378344becc7c8580f56225c4c00adbf493d82480a44b3e3b1cc82a3ac5d1d4cab6dfc89e6635443895a2dc488969075f5b94d languageName: node linkType: hard @@ -16421,15 +16462,6 @@ __metadata: languageName: node linkType: hard -"strip-literal@npm:^3.0.0": - version: 3.0.0 - resolution: "strip-literal@npm:3.0.0" - dependencies: - js-tokens: "npm:^9.0.1" - checksum: 10c0/d81657f84aba42d4bbaf2a677f7e7f34c1f3de5a6726db8bc1797f9c0b303ba54d4660383a74bde43df401cf37cce1dff2c842c55b077a4ceee11f9e31fba828 - languageName: node - linkType: hard - "supports-color@npm:^2.0.0": version: 2.0.0 resolution: "supports-color@npm:2.0.0" @@ -16676,10 +16708,13 @@ __metadata: languageName: node linkType: hard -"tinypool@npm:^1.1.1": - version: 1.1.1 - resolution: "tinypool@npm:1.1.1" - checksum: 10c0/bf26727d01443061b04fa863f571016950888ea994ba0cd8cba3a1c51e2458d84574341ab8dbc3664f1c3ab20885c8cf9ff1cc4b18201f04c2cde7d317fff69b +"tinyglobby@npm:^0.2.15": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.3" + checksum: 10c0/869c31490d0d88eedb8305d178d4c75e7463e820df5a9b9d388291daf93e8b1eb5de1dad1c1e139767e4269fe75f3b10d5009b2cc14db96ff98986920a186844 languageName: node linkType: hard @@ -16690,10 +16725,10 @@ __metadata: languageName: node linkType: hard -"tinyspy@npm:^4.0.3": - version: 4.0.3 - resolution: "tinyspy@npm:4.0.3" - checksum: 10c0/0a92a18b5350945cc8a1da3a22c9ad9f4e2945df80aaa0c43e1b3a3cfb64d8501e607ebf0305e048e3c3d3e0e7f8eb10cea27dc17c21effb73e66c4a3be36373 +"tinyrainbow@npm:^3.0.3": + version: 3.0.3 + resolution: "tinyrainbow@npm:3.0.3" + checksum: 10c0/1e799d35cd23cabe02e22550985a3051dc88814a979be02dc632a159c393a998628eacfc558e4c746b3006606d54b00bcdea0c39301133956d10a27aa27e988c languageName: node linkType: hard @@ -16755,6 +16790,8 @@ __metadata: resolution: "traefik-proxy-dashboard@workspace:." dependencies: "@eslint/js": "npm:^9.32.0" + "@noble/ed25519": "npm:^3.0.0" + "@noble/hashes": "npm:^2.0.1" "@testing-library/dom": "npm:^10.4.1" "@testing-library/jest-dom": "npm:^6.4.2" "@testing-library/react": "npm:^14.2.1" @@ -16768,6 +16805,7 @@ __metadata: "@typescript-eslint/parser": "npm:^8.38.0" "@vitejs/plugin-react": "npm:^4.7.0" "@vitest/coverage-v8": "npm:^3.2.4" + "@vitest/web-worker": "npm:^4.0.2" chart.js: "npm:^4.4.1" eslint: "npm:^9.32.0" eslint-config-prettier: "npm:^10.1.8" @@ -16800,7 +16838,7 @@ __metadata: usehooks-ts: "npm:^2.14.0" vite: "npm:^5.4.19" vite-tsconfig-paths: "npm:^5.1.4" - vitest: "npm:^3.2.4" + vitest: "npm:^4.0.3" vitest-canvas-mock: "npm:^0.3.3" languageName: unknown linkType: soft @@ -17519,21 +17557,6 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:3.2.4": - version: 3.2.4 - resolution: "vite-node@npm:3.2.4" - dependencies: - cac: "npm:^6.7.14" - debug: "npm:^4.4.1" - es-module-lexer: "npm:^1.7.0" - pathe: "npm:^2.0.3" - vite: "npm:^5.0.0 || ^6.0.0 || ^7.0.0-0" - bin: - vite-node: vite-node.mjs - checksum: 10c0/6ceca67c002f8ef6397d58b9539f80f2b5d79e103a18367288b3f00a8ab55affa3d711d86d9112fce5a7fa658a212a087a005a045eb8f4758947dd99af2a6c6b - languageName: node - linkType: hard - "vite-tsconfig-paths@npm:^5.1.4": version: 5.1.4 resolution: "vite-tsconfig-paths@npm:5.1.4" @@ -17550,61 +17573,6 @@ __metadata: languageName: node linkType: hard -"vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0": - version: 7.0.6 - resolution: "vite@npm:7.0.6" - dependencies: - esbuild: "npm:^0.25.0" - fdir: "npm:^6.4.6" - fsevents: "npm:~2.3.3" - picomatch: "npm:^4.0.3" - postcss: "npm:^8.5.6" - rollup: "npm:^4.40.0" - tinyglobby: "npm:^0.2.14" - peerDependencies: - "@types/node": ^20.19.0 || >=22.12.0 - jiti: ">=1.21.0" - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: ">=0.54.8" - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - dependenciesMeta: - fsevents: - optional: true - peerDependenciesMeta: - "@types/node": - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - bin: - vite: bin/vite.js - checksum: 10c0/3b14dfa661281b4843789884199ba2a9cca940a7666970036fe3fb1abff52b88e63e8be5ab419dd04d9f96c0415ee0f1e3ec8ebe357041648af7ccd8e348b6ad - languageName: node - linkType: hard - "vite@npm:^5.1.5": version: 5.4.17 resolution: "vite@npm:5.4.17" @@ -17691,6 +17659,61 @@ __metadata: languageName: node linkType: hard +"vite@npm:^6.0.0 || ^7.0.0": + version: 7.1.12 + resolution: "vite@npm:7.1.12" + dependencies: + esbuild: "npm:^0.25.0" + fdir: "npm:^6.5.0" + fsevents: "npm:~2.3.3" + picomatch: "npm:^4.0.3" + postcss: "npm:^8.5.6" + rollup: "npm:^4.43.0" + tinyglobby: "npm:^0.2.15" + peerDependencies: + "@types/node": ^20.19.0 || >=22.12.0 + jiti: ">=1.21.0" + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + bin: + vite: bin/vite.js + checksum: 10c0/cef4d4b4a84e663e09b858964af36e916892ac8540068df42a05ced637ceeae5e9ef71c72d54f3cfc1f3c254af16634230e221b6e2327c2a66d794bb49203262 + languageName: node + linkType: hard + "vitest-canvas-mock@npm:^0.3.3": version: 0.3.3 resolution: "vitest-canvas-mock@npm:0.3.3" @@ -17702,39 +17725,38 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^3.2.4": - version: 3.2.4 - resolution: "vitest@npm:3.2.4" +"vitest@npm:^4.0.3": + version: 4.0.3 + resolution: "vitest@npm:4.0.3" dependencies: - "@types/chai": "npm:^5.2.2" - "@vitest/expect": "npm:3.2.4" - "@vitest/mocker": "npm:3.2.4" - "@vitest/pretty-format": "npm:^3.2.4" - "@vitest/runner": "npm:3.2.4" - "@vitest/snapshot": "npm:3.2.4" - "@vitest/spy": "npm:3.2.4" - "@vitest/utils": "npm:3.2.4" - chai: "npm:^5.2.0" - debug: "npm:^4.4.1" - expect-type: "npm:^1.2.1" - magic-string: "npm:^0.30.17" + "@vitest/expect": "npm:4.0.3" + "@vitest/mocker": "npm:4.0.3" + "@vitest/pretty-format": "npm:4.0.3" + "@vitest/runner": "npm:4.0.3" + "@vitest/snapshot": "npm:4.0.3" + "@vitest/spy": "npm:4.0.3" + "@vitest/utils": "npm:4.0.3" + debug: "npm:^4.4.3" + es-module-lexer: "npm:^1.7.0" + expect-type: "npm:^1.2.2" + magic-string: "npm:^0.30.19" pathe: "npm:^2.0.3" - picomatch: "npm:^4.0.2" + picomatch: "npm:^4.0.3" std-env: "npm:^3.9.0" tinybench: "npm:^2.9.0" tinyexec: "npm:^0.3.2" - tinyglobby: "npm:^0.2.14" - tinypool: "npm:^1.1.1" - tinyrainbow: "npm:^2.0.0" - vite: "npm:^5.0.0 || ^6.0.0 || ^7.0.0-0" - vite-node: "npm:3.2.4" + tinyglobby: "npm:^0.2.15" + tinyrainbow: "npm:^3.0.3" + vite: "npm:^6.0.0 || ^7.0.0" why-is-node-running: "npm:^2.3.0" peerDependencies: "@edge-runtime/vm": "*" "@types/debug": ^4.1.12 - "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 - "@vitest/browser": 3.2.4 - "@vitest/ui": 3.2.4 + "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 4.0.3 + "@vitest/browser-preview": 4.0.3 + "@vitest/browser-webdriverio": 4.0.3 + "@vitest/ui": 4.0.3 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -17744,7 +17766,11 @@ __metadata: optional: true "@types/node": optional: true - "@vitest/browser": + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": optional: true "@vitest/ui": optional: true @@ -17754,7 +17780,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10c0/5bf53ede3ae6a0e08956d72dab279ae90503f6b5a05298a6a5e6ef47d2fd1ab386aaf48fafa61ed07a0ebfe9e371772f1ccbe5c258dd765206a8218bf2eb79eb + checksum: 10c0/3462e797fc3100d6adae9d5beaa4784a109d4fe5fc1403fb251ebdf7ca95988c1f1447bbaeac3f0046aae555a611f1dcb9da0bcb9d30af7ee99ac5392427fd2b languageName: node linkType: hard