This is an automated email from the ASF dual-hosted git repository. zqr10159 pushed a commit to branch 2.0.0 in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
commit f82c340a16560ffebac73e63e7c2175b5ed4161a Author: Logic <[email protected]> AuthorDate: Wed Aug 26 10:10:38 2026 +0800 refactor: preserve shared React runtime refinements --- web-app/src/app/administrative-app-routes.test.tsx | 10 +++-- .../app/refine/refine-resource-registry.test.ts | 4 +- .../src/app/refine/refine-resource-registry.tsx | 14 ++++--- web-app/src/app/refine/refine-runtime.test.tsx | 1 + web-app/src/app/refine/refine-runtime.tsx | 20 ++++++++- .../src/app/refine/session-query-runtime.test.tsx | 25 ++++++++++++ web-app/src/app/refine/session-query-runtime.tsx | 11 ++++- web-app/src/app/theme/hertzbeat-theme.test.ts | 21 +++++++++- web-app/src/core/auth/session-cache-boundary.ts | 17 ++++++++ .../core/auth/session-foreground-revalidation.ts | 21 ++-------- .../auth/pages/login-page-style-contract.test.ts | 26 +++++++++--- .../src/features/auth/pages/login-page.module.css | 47 ++++++++++++++-------- .../monitor-definition-catalog.module.css | 2 +- .../monitor-definition-layout-contract.test.ts | 8 ++++ .../controller/topology-query-keys.test.ts | 2 +- .../topology/runtime/topology-g6-lifecycle.test.ts | 21 ++++++++++ .../topology/runtime/topology-g6-lifecycle.ts | 5 +++ .../src/layout/shell/hertzbeat-shell.module.css | 2 +- .../layout/shell/shell-navigation-style.test.ts | 8 ++++ .../notification-workspace-navigation.module.css | 2 +- .../notification-workspace-style-contract.test.ts | 7 ++-- web-app/src/shared/theme/hertzbeat-theme.ts | 24 +++++++++++ 22 files changed, 234 insertions(+), 64 deletions(-) diff --git a/web-app/src/app/administrative-app-routes.test.tsx b/web-app/src/app/administrative-app-routes.test.tsx index f8fe189657..799daf2f23 100644 --- a/web-app/src/app/administrative-app-routes.test.tsx +++ b/web-app/src/app/administrative-app-routes.test.tsx @@ -137,12 +137,14 @@ describe('actual administrative app routes', () => { await waitFor(() => expect(probes.pluginApi).toHaveBeenCalledOnce()); }); - it('admits ADMIN to the guarded Deployment loader and page API', async () => { + it('keeps the incomplete Deployment route unavailable to ADMIN without loading feature code or APIs', async () => { renderAppRoute('/settings/deployment', 'ADMIN'); - expect(await screen.findByTestId('deployment-page')).toBeInTheDocument(); - expect(probes.deploymentLoader).toHaveBeenCalledOnce(); - await waitFor(() => expect(probes.deploymentApi).toHaveBeenCalledOnce()); + expect( + (await screen.findByText(i18n.t('common.permission.additionalRequiredTitle'))).closest('[data-state]') + ).toHaveAttribute('data-state', 'permission'); + expect(probes.deploymentLoader).not.toHaveBeenCalled(); + expect(probes.deploymentApi).not.toHaveBeenCalled(); }); it('converges the legacy Plugin path on the guarded canonical route', async () => { diff --git a/web-app/src/app/refine/refine-resource-registry.test.ts b/web-app/src/app/refine/refine-resource-registry.test.ts index 6711ec55a5..eddf133780 100644 --- a/web-app/src/app/refine/refine-resource-registry.test.ts +++ b/web-app/src/app/refine/refine-resource-registry.test.ts @@ -72,7 +72,7 @@ describe('Refine shell resource registry', () => { 'shell-administration' ]); expect(navigationChildren(tree, 'shell-basic-monitoring')).toEqual(['monitors', 'bulletin', 'monitor-definitions']); - expect(navigationChildren(tree, 'shell-application-observability')).toEqual(['explore', 'instrumentation']); + expect(navigationChildren(tree, 'shell-application-observability')).toEqual(['instrumentation', 'explore']); expect(navigationChildren(tree, 'shell-resources')).toEqual(['entities', 'topology']); expect(navigationChildren(tree, 'shell-alerting')).toEqual([ 'alerts', @@ -88,7 +88,6 @@ describe('Refine shell resource registry', () => { ]); expect(navigationChildren(tree, 'shell-administration')).toEqual([ 'system-config', - 'deployment-settings', 'plugins', 'collectors', 'tokens', @@ -113,6 +112,7 @@ describe('Refine shell resource registry', () => { const names = flattenNavigationNames(buildShellNavigation(refineResources, ['ADMIN'])); expect(names).not.toContain('settings'); + expect(names).not.toContain('deployment-settings'); expect(names.some(name => name.startsWith('monitor-category:'))).toBe(false); expect(names.some(name => name.startsWith('monitor-app:'))).toBe(false); }); diff --git a/web-app/src/app/refine/refine-resource-registry.tsx b/web-app/src/app/refine/refine-resource-registry.tsx index 01be7ef074..fbf400712a 100644 --- a/web-app/src/app/refine/refine-resource-registry.tsx +++ b/web-app/src/app/refine/refine-resource-registry.tsx @@ -162,17 +162,17 @@ const staticRefineResources: ResourceProps[] = [ order: 20, timePolicy: 'global' }), - routedNavigationResource('explore', { + routedNavigationResource('instrumentation', { parent: 'shell-application-observability', - icon: <AreaChartOutlined />, + icon: <ApiOutlined />, order: 10, - timePolicy: 'route_owned' + timePolicy: 'none' }), - routedNavigationResource('instrumentation', { + routedNavigationResource('explore', { parent: 'shell-application-observability', - icon: <ApiOutlined />, + icon: <AreaChartOutlined />, order: 20, - timePolicy: 'none' + timePolicy: 'route_owned' }), routedNavigationResource('alerts', { parent: 'shell-alerting', @@ -288,6 +288,8 @@ const staticRefineResources: ResourceProps[] = [ routedNavigationResource('deployment-settings', { parent: 'shell-administration', icon: <DeploymentUnitOutlined />, + capability: 'unsupported', + navigation: false, order: 15, timePolicy: 'none' }), diff --git a/web-app/src/app/refine/refine-runtime.test.tsx b/web-app/src/app/refine/refine-runtime.test.tsx index d1226fe6b4..986342f153 100644 --- a/web-app/src/app/refine/refine-runtime.test.tsx +++ b/web-app/src/app/refine/refine-runtime.test.tsx @@ -144,6 +144,7 @@ describe('production Refine runtime', () => { expect(mountedClients).toContain(observedClient); expect(mountedClients.filter(client => client.getQueryData(sessionQueryKey))).toEqual([observedClient]); expect(observedClient.getQueryData(sessionQueryKey)).toEqual(authenticatedSession); + expect(monitorApi.loadMonitorNavigationApps).toHaveBeenCalledTimes(1); }); }); diff --git a/web-app/src/app/refine/refine-runtime.tsx b/web-app/src/app/refine/refine-runtime.tsx index f525918fb6..781a1c8b65 100644 --- a/web-app/src/app/refine/refine-runtime.tsx +++ b/web-app/src/app/refine/refine-runtime.tsx @@ -19,7 +19,7 @@ import { Refine } from '@refinedev/core'; import { useNotificationProvider } from '@refinedev/antd'; import routerProvider from '@refinedev/react-router'; import { QueryClient } from '@tanstack/react-query'; -import { useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { Outlet } from 'react-router-dom'; import { SessionProvider } from '@/core/auth/session-provider'; @@ -87,6 +87,9 @@ function RefineGenerationRuntime({ queryClient: QueryClient; }) { const [monitorApps, setMonitorApps] = useState<readonly MonitorApp[]>([]); + const updateMonitorApps = useCallback((next: readonly MonitorApp[]) => { + setMonitorApps(current => (sameMonitorApps(current, next) ? current : next)); + }, []); const resources = useMemo(() => buildRefineResources(monitorApps), [monitorApps]); return ( <Refine @@ -104,9 +107,22 @@ function RefineGenerationRuntime({ }} > <SessionProvider> - <MonitorNavigationResourceLoader onChange={setMonitorApps} /> + <MonitorNavigationResourceLoader onChange={updateMonitorApps} /> <Outlet /> </SessionProvider> </Refine> ); } + +function sameMonitorApps(current: readonly MonitorApp[], next: readonly MonitorApp[]) { + return ( + current.length === next.length && + current.every( + (app, index) => + app.category === next[index]?.category && + app.value === next[index]?.value && + app.label === next[index]?.label && + app.hide === next[index]?.hide + ) + ); +} diff --git a/web-app/src/app/refine/session-query-runtime.test.tsx b/web-app/src/app/refine/session-query-runtime.test.tsx index a3fbcbcea1..fb394af70a 100644 --- a/web-app/src/app/refine/session-query-runtime.test.tsx +++ b/web-app/src/app/refine/session-query-runtime.test.tsx @@ -232,6 +232,31 @@ describe('SessionQueryRuntime', () => { expect(fetchMock).toHaveBeenCalledTimes(2); }); + it('keeps the current QueryClient when a safe-read refresh confirms the same identity', async () => { + const clients: QueryClient[] = []; + sessionApi.refreshSession.mockResolvedValue({ ...userA, roles: [...userA.roles] }); + const fetchMock = vi + .fn<typeof fetch>() + .mockResolvedValueOnce(new Response(null, { status: 401 })) + .mockResolvedValueOnce(new Response('{}', { status: 200 })); + vi.stubGlobal('fetch', fetchMock); + renderRuntime(clients); + fireEvent.click(screen.getByRole('button', { name: 'Publish user A' })); + const currentClient = clients.at(-1); + currentClient?.setQueryData(['protected', 'workspace-a'], 'operator-a'); + convergence.broadcast.mockClear(); + + await expect(apiFetch('/api/protected')).resolves.toMatchObject({ status: 200 }); + + expect(sessionApi.refreshSession).toHaveBeenCalledOnce(); + expect(clients).toHaveLength(2); + expect(clients.at(-1)).toBe(currentClient); + expect(currentClient?.getQueryData(sessionQueryKey)).toEqual(userA); + expect(currentClient?.getQueryData(['protected', 'workspace-a'])).toBe('operator-a'); + expect(convergence.broadcast).not.toHaveBeenCalled(); + expect(fetchMock).toHaveBeenCalledTimes(2); + }); + it('converges a failed safe-read refresh to a new anonymous QueryClient', async () => { const clients: QueryClient[] = []; sessionApi.refreshSession.mockRejectedValue(new SessionRequestError('error', { status: 200 })); diff --git a/web-app/src/app/refine/session-query-runtime.tsx b/web-app/src/app/refine/session-query-runtime.tsx index e6cb920078..ca7dfde9d8 100644 --- a/web-app/src/app/refine/session-query-runtime.tsx +++ b/web-app/src/app/refine/session-query-runtime.tsx @@ -19,7 +19,11 @@ import type { QueryClient } from '@tanstack/react-query'; import { useCallback, useLayoutEffect, useRef, useState, type ReactNode } from 'react'; import { flushSync } from 'react-dom'; -import { createCheckingSessionQueryClient, createSessionQueryClient } from '@/core/auth/session-cache-boundary'; +import { + createCheckingSessionQueryClient, + createSessionQueryClient, + hasSessionIdentityBoundaryChanged +} from '@/core/auth/session-cache-boundary'; import { createSessionConvergenceChannel } from '@/core/auth/session-convergence-channel'; import { anonymousSession, @@ -137,7 +141,10 @@ function useSessionRefresh( if (!mountedRef.current || runtimeRef.current.generation !== generation) { return { status: 'retired' } satisfies BrowserSessionRefreshResult; } - replaceIdentity(refreshedSession, options); + const currentSession = runtimeRef.current.queryClient.getQueryData<UiSession>(sessionQueryKey); + if (hasSessionIdentityBoundaryChanged(currentSession, refreshedSession)) { + replaceIdentity(refreshedSession, options); + } return { status: refreshedSession.authenticated ? 'renewed' : 'rejected' } satisfies BrowserSessionRefreshResult; diff --git a/web-app/src/app/theme/hertzbeat-theme.test.ts b/web-app/src/app/theme/hertzbeat-theme.test.ts index 9d86d46b69..f5781ab1c2 100644 --- a/web-app/src/app/theme/hertzbeat-theme.test.ts +++ b/web-app/src/app/theme/hertzbeat-theme.test.ts @@ -16,6 +16,13 @@ describe('HertzBeat semantic theme', () => { expect(theme.token).toMatchObject({ colorPrimary: '#9b5bb3', + colorError: '#e15b5d', + colorErrorText: '#e15b5d', + colorErrorTextActive: '#e15b5d', + colorErrorTextHover: '#e46a6b', + colorLink: '#b97bca', + colorLinkActive: '#b97bca', + colorLinkHover: '#c18ad0', colorBgElevated: '#14171e', borderRadius: 6, borderRadiusLG: 8, @@ -29,6 +36,9 @@ describe('HertzBeat semantic theme', () => { }); expect(theme.components?.Button).toMatchObject({ borderRadius: 5, + colorError: '#e15b5d', + colorErrorActive: '#e15b5d', + colorErrorHover: '#e46a6b', controlHeight: 32, defaultShadow: 'none', fontWeight: 600, @@ -68,7 +78,16 @@ describe('HertzBeat semantic theme', () => { components: { Menu: { itemSelectedBg: '#211a26' } } }); expect(createHertzBeatTheme('default')).toMatchObject({ - token: { colorBgElevated: '#ffffff' }, + token: { + colorBgElevated: '#ffffff', + colorError: '#b42318', + colorErrorText: '#b42318', + colorErrorTextActive: '#8f1c13', + colorErrorTextHover: '#9f2117', + colorLink: '#71357f', + colorLinkActive: '#512459', + colorLinkHover: '#63306f' + }, components: { Input: { hoverBorderColor: '#89919e' }, Menu: { itemSelectedBg: '#f7f0f8' }, diff --git a/web-app/src/core/auth/session-cache-boundary.ts b/web-app/src/core/auth/session-cache-boundary.ts index 265dd70fed..85a9fe0d68 100644 --- a/web-app/src/core/auth/session-cache-boundary.ts +++ b/web-app/src/core/auth/session-cache-boundary.ts @@ -32,3 +32,20 @@ export function createSessionQueryClient(createQueryClient: () => QueryClient, n export function createCheckingSessionQueryClient(createQueryClient: () => QueryClient) { return createQueryClient(); } + +export function hasSessionIdentityBoundaryChanged(current: UiSession | undefined, next: UiSession) { + return ( + current === undefined || + current.authenticated !== next.authenticated || + current.username !== next.username || + current.workspaceId !== next.workspaceId || + current.expiresAt !== next.expiresAt || + !haveSameRoles(current.roles, next.roles) + ); +} + +function haveSameRoles(current: string[], next: string[]) { + if (current.length !== next.length) return false; + const nextRoles = new Set(next); + return current.every(role => nextRoles.has(role)); +} diff --git a/web-app/src/core/auth/session-foreground-revalidation.ts b/web-app/src/core/auth/session-foreground-revalidation.ts index b33ccba438..798205e190 100644 --- a/web-app/src/core/auth/session-foreground-revalidation.ts +++ b/web-app/src/core/auth/session-foreground-revalidation.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { hasSessionIdentityBoundaryChanged } from './session-cache-boundary'; import { anonymousSession, getSession, SessionRequestError, type UiSession } from './session-api'; import type { ReplaceSessionIdentity } from './session-identity-context'; @@ -64,7 +65,9 @@ export function startForegroundSessionRevalidation({ try { const nextSession = await getSession({ signal }); if (!ownsCurrentGeneration(owner.generation)) return; - if (hasSessionIdentityBoundaryChanged(owner.session, nextSession)) replaceIdentity(nextSession); + if (hasSessionIdentityBoundaryChanged(owner.session, nextSession)) { + replaceIdentity(nextSession); + } } catch (reason) { if (!ownsCurrentGeneration(owner.generation) || signal.aborted) return; if (isAuthoritativeSessionRejection(reason)) replaceIdentity(anonymousSession); @@ -91,22 +94,6 @@ export function startForegroundSessionRevalidation({ }; } -function hasSessionIdentityBoundaryChanged(current: UiSession, next: UiSession) { - return ( - current.authenticated !== next.authenticated || - current.username !== next.username || - current.workspaceId !== next.workspaceId || - current.expiresAt !== next.expiresAt || - !haveSameRoles(current.roles, next.roles) - ); -} - -function haveSameRoles(current: string[], next: string[]) { - if (current.length !== next.length) return false; - const nextRoles = new Set(next); - return current.every(role => nextRoles.has(role)); -} - function isAuthoritativeSessionRejection(reason: unknown) { return reason instanceof SessionRequestError && (reason.status === 401 || reason.status === 403); } diff --git a/web-app/src/features/auth/pages/login-page-style-contract.test.ts b/web-app/src/features/auth/pages/login-page-style-contract.test.ts index 30b0952039..afcd56721e 100644 --- a/web-app/src/features/auth/pages/login-page-style-contract.test.ts +++ b/web-app/src/features/auth/pages/login-page-style-contract.test.ts @@ -10,24 +10,25 @@ import { describe, expect, it } from 'vitest'; import stylesheet from './login-page.module.css?raw'; describe('login page visual contract', () => { - it('uses the original artwork on desktop and preserves its light-theme treatment', () => { + it('uses the original artwork across passport breakpoints and preserves its light-theme treatment', () => { expect(stylesheet).toContain("url('/assets/bg.png')"); - expect(stylesheet).toMatch(/@media \(min-width: 960px\)[\s\S]*\.page\s*{[^}]*background-image:\s*url/s); + expect(stylesheet).toMatch(/\.page\s*{[^}]*background-image:\s*url/s); + expect(stylesheet).not.toMatch(/@media \(min-width: 960px\)[\s\S]*\.page\s*{[^}]*background-image:\s*url/s); expect(stylesheet).not.toContain(":root[data-theme='default']"); expect(stylesheet).toMatch(/@media \(min-width: 960px\)/); }); it('keeps the form narrow, the explanation desktop-only, and the passport route responsive', () => { expect(stylesheet).toMatch(/\.formRegion\s*{[^}]*width:\s*min\(368px, 100%\)/s); - expect(stylesheet).toMatch(/\.introductionBrand\s*{[^}]*width:\s*280px/s); + expect(stylesheet).toMatch(/\.introductionBrand\s*{[^}]*width:\s*300px[^}]*height:\s*75px/s); expect(stylesheet).toMatch(/@media \(min-width: 960px\)[\s\S]*\.brandHeader\s*{[^}]*display:\s*none/s); expect(stylesheet).toMatch(/@media \(max-width: 959px\)[\s\S]*\.introduction\s*{[^}]*display:\s*none/s); expect(stylesheet).toMatch(/body:has\(\[data-passport-page='true'\]\)[^}]*min-width:\s*0/s); }); it('keeps each capability on one fitted line and rolls the completed line before typing the next', () => { - expect(stylesheet).toMatch(/\.introductionPositioning\s*{[^}]*margin-bottom:\s*clamp\(26px,\s*2\.5vw,\s*32px\)/s); - expect(stylesheet).toMatch(/\.introductionPhraseStage\s*{[^}]*block-size:\s*1\.45em/s); + expect(stylesheet).toMatch(/\.introductionPositioning\s*{[^}]*margin-bottom:\s*clamp\(44px,\s*4vw,\s*56px\)/s); + expect(stylesheet).toMatch(/\.introductionPhraseStage\s*{[^}]*block-size:\s*1\.55em[^}]*font-size:\s*36px/s); expect(stylesheet).toMatch(/\.introductionTypingLine\s*{[^}]*white-space:\s*nowrap/s); expect(stylesheet).toContain('@keyframes passportIntroductionRollOut'); expect(stylesheet).toContain('@keyframes passportIntroductionRollIn'); @@ -43,8 +44,21 @@ describe('login page visual contract', () => { it('uses a translucent glass login panel without a secondary introduction paragraph', () => { expect(stylesheet).not.toContain('.introductionDescription'); expect(stylesheet).toMatch( - /@media \(min-width: 960px\)[\s\S]*\.panel\s*{[^}]*background:\s*color-mix\(in srgb, var\(--hb-bg-raised\) 58%, transparent\)/s + /\.panel\s*{[^}]*background:\s*color-mix\(in srgb, var\(--hb-bg-raised\) 58%, transparent\)/s ); expect(stylesheet).toMatch(/\.panel\s*{[^}]*backdrop-filter:\s*blur\(18px\) saturate\(1\.15\)/s); }); + + it('gives the desktop form enough vertical weight without widening it', () => { + expect(stylesheet).toMatch(/@media \(min-width: 960px\)[\s\S]*\.panel\s*{[^}]*padding:\s*36px/s); + expect(stylesheet).toMatch( + /@media \(min-width: 960px\)[\s\S]*\.panel\s*>\s*:global\(h2\.ant-typography\)\s*{[^}]*margin-bottom:\s*22px/s + ); + expect(stylesheet).toMatch( + /@media \(min-width: 960px\)[\s\S]*\.panel\s+:global\(\.ant-input\)[^}]*min-height:\s*42px/s + ); + expect(stylesheet).toMatch( + /@media \(min-width: 960px\)[\s\S]*\.panel\s+:global\(\.ant-btn\)[^}]*min-height:\s*42px/s + ); + }); }); diff --git a/web-app/src/features/auth/pages/login-page.module.css b/web-app/src/features/auth/pages/login-page.module.css index 6f006ae49e..a362283d7e 100644 --- a/web-app/src/features/auth/pages/login-page.module.css +++ b/web-app/src/features/auth/pages/login-page.module.css @@ -11,6 +11,10 @@ padding: 40px clamp(24px, 5vw, 72px); overflow-x: hidden; background: var(--hb-bg-canvas); + background-image: url('/assets/bg.png'); + background-repeat: no-repeat; + background-position: center; + background-size: cover; } .shell { @@ -82,15 +86,15 @@ .introductionBrand { display: block; - width: 280px; - height: 70px; - margin-bottom: 4px; + width: 300px; + height: 75px; + margin-bottom: 12px; } .introductionPositioning { display: block; max-width: 540px; - margin-bottom: clamp(26px, 2.5vw, 32px); + margin-bottom: clamp(44px, 4vw, 56px); font-size: clamp(16px, 1.45vw, 20px); font-weight: 520; letter-spacing: -0.01em; @@ -101,9 +105,9 @@ position: relative; display: block; width: 100%; - block-size: 1.45em; + block-size: 1.55em; overflow: hidden; - font-size: 34px; + font-size: 36px; line-height: 1.18; } @@ -133,7 +137,7 @@ .introductionMeasure { position: absolute; visibility: hidden; - font-size: 34px; + font-size: 36px; white-space: nowrap; pointer-events: none; } @@ -190,10 +194,12 @@ .panel { width: 100%; padding: 30px; - border: 1px solid var(--hb-border-subtle); + border: 1px solid color-mix(in srgb, var(--hb-bg-raised) 72%, transparent); border-radius: var(--hb-radius-surface); - background: var(--hb-bg-raised); + background: color-mix(in srgb, var(--hb-bg-raised) 58%, transparent); box-shadow: var(--hb-shadow-surface); + -webkit-backdrop-filter: blur(18px) saturate(1.15); + backdrop-filter: blur(18px) saturate(1.15); } .brand { @@ -238,11 +244,7 @@ } .page { - background-image: url('/assets/bg.png'); - background-repeat: no-repeat; - background-position: center; background-attachment: fixed; - background-size: cover; } .introduction { @@ -251,10 +253,21 @@ } .panel { - border-color: color-mix(in srgb, var(--hb-bg-raised) 72%, transparent); - background: color-mix(in srgb, var(--hb-bg-raised) 58%, transparent); - -webkit-backdrop-filter: blur(18px) saturate(1.15); - backdrop-filter: blur(18px) saturate(1.15); + padding: 36px; + } + + .panel > :global(h2.ant-typography) { + margin-bottom: 22px; + } + + .panel :global(.ant-input), + .panel :global(.ant-input-affix-wrapper), + .panel :global(.ant-btn) { + min-height: 42px; + } + + .panel :global(.ant-input-affix-wrapper .ant-input) { + min-height: auto; } } diff --git a/web-app/src/features/settings/monitor-definition/components/monitor-definition-catalog.module.css b/web-app/src/features/settings/monitor-definition/components/monitor-definition-catalog.module.css index cfffab09b4..291e43edc9 100644 --- a/web-app/src/features/settings/monitor-definition/components/monitor-definition-catalog.module.css +++ b/web-app/src/features/settings/monitor-definition/components/monitor-definition-catalog.module.css @@ -30,7 +30,7 @@ .item.itemSelected:hover { border-radius: 0; color: var(--hb-text-primary); - background: var(--hb-nav-selected); + background: transparent; box-shadow: inset 2px 0 0 var(--hb-brand-accent); } diff --git a/web-app/src/features/settings/monitor-definition/pages/monitor-definition-layout-contract.test.ts b/web-app/src/features/settings/monitor-definition/pages/monitor-definition-layout-contract.test.ts index 48bed6caff..88ac9cbea0 100644 --- a/web-app/src/features/settings/monitor-definition/pages/monitor-definition-layout-contract.test.ts +++ b/web-app/src/features/settings/monitor-definition/pages/monitor-definition-layout-contract.test.ts @@ -41,6 +41,14 @@ describe('monitor definition split layout contract', () => { ); expect(pageStyles).toMatch(/@media\s*\(max-width:\s*760px\)[\s\S]*\.workspace\s*\{[^}]*overflow:\s*visible/); }); + + it('uses a line-only marker for the selected catalog definition', () => { + const selected = catalogStyles.match(/\.item\.itemSelected,[\s\S]*?\{[^}]*\}/)?.[0] ?? ''; + + expect(selected).toMatch(/box-shadow:\s*inset 2px 0 0 var\(--hb-brand-accent\)/); + expect(selected).toMatch(/background:\s*transparent/); + expect(selected).not.toMatch(/background:\s*var\(--hb-nav-selected\)/); + }); }); function cssRule(source: string, name: string) { diff --git a/web-app/src/features/topology/controller/topology-query-keys.test.ts b/web-app/src/features/topology/controller/topology-query-keys.test.ts index 2f0f1e35da..0299a21cfa 100644 --- a/web-app/src/features/topology/controller/topology-query-keys.test.ts +++ b/web-app/src/features/topology/controller/topology-query-keys.test.ts @@ -22,7 +22,7 @@ describe('topology query keys', () => { const base = topologyQueryKeys.graph(fullQuery, 3); expect(base).toEqual([ 'topology', - { context: '["","","","","prod","",""]', window: '1000:2000', refreshRevision: 3 }, + { context: '["","","","","","","prod","",""]', window: '1000:2000', refreshRevision: 3 }, { focusEntityId: 10, depth: 2, diff --git a/web-app/src/features/topology/runtime/topology-g6-lifecycle.test.ts b/web-app/src/features/topology/runtime/topology-g6-lifecycle.test.ts new file mode 100644 index 0000000000..b1b5c37e63 --- /dev/null +++ b/web-app/src/features/topology/runtime/topology-g6-lifecycle.test.ts @@ -0,0 +1,21 @@ +/* Licensed to the Apache Software Foundation (ASF) under the Apache License, Version 2.0. */ + +import { describe, expect, it } from 'vitest'; + +import { removeGeneratedCanvasTabStops } from './topology-g6-lifecycle'; + +describe('Topology G6 accessibility lifecycle', () => { + it('keeps generated drawing surfaces out of the positive tab order', () => { + const layer = document.createElement('div'); + const interactiveCanvas = document.createElement('canvas'); + const decorativeCanvas = document.createElement('canvas'); + interactiveCanvas.tabIndex = 1; + decorativeCanvas.tabIndex = 1; + layer.append(interactiveCanvas, decorativeCanvas); + + removeGeneratedCanvasTabStops(layer); + + expect(interactiveCanvas).toHaveAttribute('tabindex', '-1'); + expect(decorativeCanvas).toHaveAttribute('tabindex', '-1'); + }); +}); diff --git a/web-app/src/features/topology/runtime/topology-g6-lifecycle.ts b/web-app/src/features/topology/runtime/topology-g6-lifecycle.ts index 05ca1165ad..58a8301b46 100644 --- a/web-app/src/features/topology/runtime/topology-g6-lifecycle.ts +++ b/web-app/src/features/topology/runtime/topology-g6-lifecycle.ts @@ -89,6 +89,7 @@ export async function initializeCandidateGraph( bindTopologyInteractionEvents(graph, module, resources.input, ownsGraph); resources.observer = observeSize(container, graph); await graph.render(); + removeGeneratedCanvasTabStops(layer); if (cancelled()) return; const previousViewport = refs.graph.current ? readTopologyViewport(refs.graph.current) : refs.viewport.current; await restoreOrFitTopologyGraph(graph, previousViewport); @@ -156,6 +157,10 @@ function activateGraphLayer(layer: HTMLDivElement) { layer.style.visibility = 'visible'; } +export function removeGeneratedCanvasTabStops(layer: HTMLElement) { + for (const canvas of layer.querySelectorAll('canvas')) canvas.tabIndex = -1; +} + function observeSize(container: HTMLDivElement, graph: TopologyG6Graph) { if (typeof ResizeObserver === 'undefined') return undefined; const observer = new ResizeObserver(entries => { diff --git a/web-app/src/layout/shell/hertzbeat-shell.module.css b/web-app/src/layout/shell/hertzbeat-shell.module.css index 2b0cd2f7f7..332423729a 100644 --- a/web-app/src/layout/shell/hertzbeat-shell.module.css +++ b/web-app/src/layout/shell/hertzbeat-shell.module.css @@ -365,7 +365,7 @@ button.timePolicy:hover { border-left-color: var(--hb-brand-accent); border-radius: 0; color: var(--hb-text-primary); - background: var(--hb-nav-selected); + background: transparent; font-weight: 600; } diff --git a/web-app/src/layout/shell/shell-navigation-style.test.ts b/web-app/src/layout/shell/shell-navigation-style.test.ts index d3ed2b4289..3d0b650508 100644 --- a/web-app/src/layout/shell/shell-navigation-style.test.ts +++ b/web-app/src/layout/shell/shell-navigation-style.test.ts @@ -22,4 +22,12 @@ describe('shell navigation visual hierarchy', () => { it('gives expanded first-level areas a restrained shared open state', () => { expect(shellStyles).toMatch(/\.navigationParentOpen\s*\{[^}]*background:\s*var\(--hb-nav-hover\)/); }); + + it('marks the active destination with a line instead of a filled selection block', () => { + const activeRule = shellStyles.match(/\.navigationLinkActive\s*\{[^}]*\}/)?.[0] ?? ''; + + expect(activeRule).toMatch(/border-left-color:\s*var\(--hb-brand-accent\)/); + expect(activeRule).toMatch(/background:\s*transparent/); + expect(activeRule).not.toMatch(/background:\s*var\(--hb-nav-selected\)/); + }); }); diff --git a/web-app/src/shared/notification-workspace/notification-workspace-navigation.module.css b/web-app/src/shared/notification-workspace/notification-workspace-navigation.module.css index f43c1cadeb..b4b7a5f504 100644 --- a/web-app/src/shared/notification-workspace/notification-workspace-navigation.module.css +++ b/web-app/src/shared/notification-workspace/notification-workspace-navigation.module.css @@ -41,7 +41,7 @@ .current a { border-left-color: var(--hb-brand-accent); - background: var(--hb-nav-selected); + background: transparent; } .index { diff --git a/web-app/src/shared/notification-workspace/notification-workspace-style-contract.test.ts b/web-app/src/shared/notification-workspace/notification-workspace-style-contract.test.ts index baf7569b76..a84ed078d3 100644 --- a/web-app/src/shared/notification-workspace/notification-workspace-style-contract.test.ts +++ b/web-app/src/shared/notification-workspace/notification-workspace-style-contract.test.ts @@ -21,9 +21,10 @@ describe('notification workspace layout contract', () => { expect(dependencyRule).not.toMatch(/overflow:\s*hidden/); }); - it('uses semantic hover and selected surfaces instead of the page canvas', () => { + it('uses a hover surface and a line-only current-step marker', () => { expect(styles).toMatch(/\.steps\s+a:hover\s*\{[^}]*background:\s*var\(--hb-bg-hover\)/s); - expect(styles).toMatch(/\.current\s+a\s*\{[^}]*background:\s*var\(--hb-nav-selected\)/s); - expect(styles).not.toMatch(/\.current\s+a\s*\{[^}]*background:\s*var\(--hb-bg-canvas\)/s); + expect(styles).toMatch(/\.current\s+a\s*\{[^}]*border-left-color:\s*var\(--hb-brand-accent\)/s); + expect(styles).toMatch(/\.current\s+a\s*\{[^}]*background:\s*transparent/s); + expect(styles).not.toMatch(/\.current\s+a\s*\{[^}]*background:\s*var\(--hb-nav-selected\)/s); }); }); diff --git a/web-app/src/shared/theme/hertzbeat-theme.ts b/web-app/src/shared/theme/hertzbeat-theme.ts index 5a823644d0..c82bab986a 100644 --- a/web-app/src/shared/theme/hertzbeat-theme.ts +++ b/web-app/src/shared/theme/hertzbeat-theme.ts @@ -33,6 +33,13 @@ function themeTokens(palette: HertzBeatPalette): NonNullable<ThemeConfig['token' colorBgElevated: palette.raised, colorBorder: palette.border, colorBorderSecondary: palette.border, + colorError: palette.error, + colorErrorText: palette.errorText, + colorErrorTextActive: palette.errorTextActive, + colorErrorTextHover: palette.errorTextHover, + colorLink: palette.link, + colorLinkActive: palette.linkActive, + colorLinkHover: palette.linkHover, colorPrimary: '#9b5bb3', colorPrimaryActive: '#7f448f', colorPrimaryHover: '#a96abd', @@ -54,6 +61,9 @@ function controlComponents(palette: HertzBeatPalette): NonNullable<ThemeConfig[' return { Button: { borderRadius: 5, + colorError: palette.errorText, + colorErrorActive: palette.errorTextActive, + colorErrorHover: palette.errorTextHover, controlHeight: 32, defaultActiveBorderColor: palette.activeBorder, defaultActiveColor: palette.text, @@ -133,8 +143,15 @@ const darkPalette = { border: '#282d38', canvas: '#0d0f14', chrome: '#101218', + error: '#e15b5d', + errorText: '#e15b5d', + errorTextActive: '#e15b5d', + errorTextHover: '#e46a6b', muted: '#929aaa', hover: '#181b22', + link: '#b97bca', + linkActive: '#b97bca', + linkHover: '#c18ad0', raised: '#14171e', selected: '#211a26', selectedText: '#f4edf6', @@ -148,8 +165,15 @@ const lightPalette = { border: '#dfe3e8', canvas: '#f5f6f8', chrome: '#ffffff', + error: '#b42318', + errorText: '#b42318', + errorTextActive: '#8f1c13', + errorTextHover: '#9f2117', muted: '#697180', hover: '#f1f2f5', + link: '#71357f', + linkActive: '#512459', + linkHover: '#63306f', raised: '#ffffff', selected: '#f7f0f8', selectedText: '#71357f', --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
