fix: simplify top-level settings
This commit is contained in:
@@ -3,7 +3,7 @@ import { cleanup, fireEvent, render, screen, within } from '@testing-library/rea
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { AppShell, type InstanceContext } from './app-shell.js';
|
import { AppShell, resolveRoute, type InstanceContext } from './app-shell.js';
|
||||||
import type { AuditDataSource } from './audit/audit-page.js';
|
import type { AuditDataSource } from './audit/audit-page.js';
|
||||||
import type { AutomationDataSource as ScheduleDataSource } from './automation/automation-page.js';
|
import type { AutomationDataSource as ScheduleDataSource } from './automation/automation-page.js';
|
||||||
import type { EventStreamClient } from './events/event-stream-client.js';
|
import type { EventStreamClient } from './events/event-stream-client.js';
|
||||||
@@ -203,7 +203,7 @@ describe('React AppShell and Fleet vertical slice', () => {
|
|||||||
expect(within(bravo).getByText('需要认证')).toBeTruthy();
|
expect(within(bravo).getByText('需要认证')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps every global workspace reachable and exposes settings subsections', async () => {
|
it('keeps every global workspace reachable and links settings directly to security', async () => {
|
||||||
const jobsDataSource: JobsDataSource = { load: vi.fn().mockResolvedValue(emptyPage) };
|
const jobsDataSource: JobsDataSource = { load: vi.fn().mockResolvedValue(emptyPage) };
|
||||||
const scheduleDataSource: ScheduleDataSource = {
|
const scheduleDataSource: ScheduleDataSource = {
|
||||||
listSchedules: vi.fn().mockResolvedValue([]),
|
listSchedules: vi.fn().mockResolvedValue([]),
|
||||||
@@ -234,7 +234,7 @@ describe('React AppShell and Fleet vertical slice', () => {
|
|||||||
'/automation',
|
'/automation',
|
||||||
);
|
);
|
||||||
expect(within(navigation).getByRole('link', { name: '设置' }).getAttribute('href')).toBe(
|
expect(within(navigation).getByRole('link', { name: '设置' }).getAttribute('href')).toBe(
|
||||||
'/settings/instances',
|
'/settings/system',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
within(navigation).getByRole('link', { name: '自动化' }).getAttribute('aria-current'),
|
within(navigation).getByRole('link', { name: '自动化' }).getAttribute('aria-current'),
|
||||||
@@ -254,13 +254,31 @@ describe('React AppShell and Fleet vertical slice', () => {
|
|||||||
expect(await screen.findByText(/没有审计事件符合当前查询/i)).toBeTruthy();
|
expect(await screen.findByText(/没有审计事件符合当前查询/i)).toBeTruthy();
|
||||||
|
|
||||||
rerender(<AppShell pathname="/settings/system" eventStreamClient={quietEventStreamClient} />);
|
rerender(<AppShell pathname="/settings/system" eventStreamClient={quietEventStreamClient} />);
|
||||||
const settingsNavigation = screen.getByRole('navigation', { name: '设置导航' });
|
expect(screen.getByRole('heading', { name: '密码保护' })).toBeTruthy();
|
||||||
expect(within(settingsNavigation).getByRole('link', { name: '实例管理' })).toBeTruthy();
|
expect(screen.queryByRole('navigation', { name: '设置导航' })).toBeNull();
|
||||||
expect(
|
});
|
||||||
within(settingsNavigation)
|
|
||||||
.getByRole('link', { name: '系统与安全' })
|
it('redirects legacy top-level instance settings without loading Fleet', () => {
|
||||||
.getAttribute('aria-current'),
|
expect(resolveRoute('/settings/instances')).toMatchObject({
|
||||||
).toBe('page');
|
kind: 'redirect',
|
||||||
|
to: '/settings/system',
|
||||||
|
});
|
||||||
|
expect(resolveRoute('/settings/instances/bravo')).toMatchObject({
|
||||||
|
kind: 'settings-instance-detail',
|
||||||
|
params: { instanceId: 'bravo' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const load = vi.fn().mockResolvedValue(snapshot);
|
||||||
|
render(
|
||||||
|
<AppShell
|
||||||
|
pathname="/settings/instances"
|
||||||
|
fleetDataSource={source(load)}
|
||||||
|
eventStreamClient={quietEventStreamClient}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole('heading', { name: '密码保护' })).toBeTruthy();
|
||||||
|
expect(load).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides placeholder dev version badge in the top bar', () => {
|
it('hides placeholder dev version badge in the top bar', () => {
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import {
|
|||||||
type NotificationsDataSource,
|
type NotificationsDataSource,
|
||||||
} from './instances/notifications-module.js';
|
} from './instances/notifications-module.js';
|
||||||
import { OtaModule, type OtaDataSource } from './instances/ota-module.js';
|
import { OtaModule, type OtaDataSource } from './instances/ota-module.js';
|
||||||
import { InstanceSettingsPage } from './settings/instance-settings-page.js';
|
|
||||||
import {
|
import {
|
||||||
InstanceDetail,
|
InstanceDetail,
|
||||||
INSTANCE_MODULE_LABELS,
|
INSTANCE_MODULE_LABELS,
|
||||||
@@ -69,7 +68,6 @@ export type RouteKind =
|
|||||||
| 'job-detail'
|
| 'job-detail'
|
||||||
| 'audit'
|
| 'audit'
|
||||||
| 'audit-detail'
|
| 'audit-detail'
|
||||||
| 'settings-instances'
|
|
||||||
| 'settings-instance-detail'
|
| 'settings-instance-detail'
|
||||||
| 'settings-system'
|
| 'settings-system'
|
||||||
| 'not-found';
|
| 'not-found';
|
||||||
@@ -139,13 +137,14 @@ function decode(value: string): string {
|
|||||||
export function resolveRoute(input: string): ResolvedRoute {
|
export function resolveRoute(input: string): ResolvedRoute {
|
||||||
const pathname = normalize(input);
|
const pathname = normalize(input);
|
||||||
if (pathname === '/') return { kind: 'redirect', pathname, to: '/fleet' };
|
if (pathname === '/') return { kind: 'redirect', pathname, to: '/fleet' };
|
||||||
|
if (pathname === '/settings/instances')
|
||||||
|
return { kind: 'redirect', pathname, to: '/settings/system' };
|
||||||
const staticRoutes: Readonly<Record<string, RouteKind>> = {
|
const staticRoutes: Readonly<Record<string, RouteKind>> = {
|
||||||
'/fleet': 'fleet',
|
'/fleet': 'fleet',
|
||||||
'/automation': 'automation',
|
'/automation': 'automation',
|
||||||
'/instances/new': 'instance-new',
|
'/instances/new': 'instance-new',
|
||||||
'/jobs': 'jobs',
|
'/jobs': 'jobs',
|
||||||
'/audit': 'audit',
|
'/audit': 'audit',
|
||||||
'/settings/instances': 'settings-instances',
|
|
||||||
'/settings/system': 'settings-system',
|
'/settings/system': 'settings-system',
|
||||||
};
|
};
|
||||||
if (staticRoutes[pathname]) return { kind: staticRoutes[pathname], pathname };
|
if (staticRoutes[pathname]) return { kind: staticRoutes[pathname], pathname };
|
||||||
@@ -270,14 +269,6 @@ function Page({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
if (route.kind === 'settings-instances')
|
|
||||||
return (
|
|
||||||
<InstanceSettingsPage
|
|
||||||
{...(fleetDataSource ? { dataSource: fleetDataSource } : {})}
|
|
||||||
{...(fleetData ? { initialData: fleetData } : {})}
|
|
||||||
refreshSignal={fleetRefreshSignal}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
if (route.kind === 'settings-system') return <ConsoleAuthSettings />;
|
if (route.kind === 'settings-system') return <ConsoleAuthSettings />;
|
||||||
if (route.kind === 'not-found')
|
if (route.kind === 'not-found')
|
||||||
return (
|
return (
|
||||||
@@ -405,7 +396,6 @@ function Page({
|
|||||||
'job-detail': '任务详情',
|
'job-detail': '任务详情',
|
||||||
audit: '审计',
|
audit: '审计',
|
||||||
'audit-detail': '审计事件',
|
'audit-detail': '审计事件',
|
||||||
'settings-instances': '实例设置',
|
|
||||||
'settings-instance-detail': '实例设置',
|
'settings-instance-detail': '实例设置',
|
||||||
'settings-system': '系统设置',
|
'settings-system': '系统设置',
|
||||||
};
|
};
|
||||||
@@ -432,7 +422,7 @@ const GLOBAL_NAVIGATION: readonly {
|
|||||||
}[] = [
|
}[] = [
|
||||||
{ section: 'fleet', href: '/fleet', label: '节点', icon: 'grid' },
|
{ section: 'fleet', href: '/fleet', label: '节点', icon: 'grid' },
|
||||||
{ section: 'automation', href: '/automation', label: '自动化', icon: 'jobs' },
|
{ section: 'automation', href: '/automation', label: '自动化', icon: 'jobs' },
|
||||||
{ section: 'settings', href: '/settings/instances', label: '设置', icon: 'settings' },
|
{ section: 'settings', href: '/settings/system', label: '设置', icon: 'settings' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const STREAM_LABELS = {
|
const STREAM_LABELS = {
|
||||||
@@ -693,22 +683,6 @@ export function AppShell({
|
|||||||
</header>
|
</header>
|
||||||
<div className="app-layout app-layout-single">
|
<div className="app-layout app-layout-single">
|
||||||
<main id="main-content" tabIndex={-1}>
|
<main id="main-content" tabIndex={-1}>
|
||||||
{currentSection === 'settings' ? (
|
|
||||||
<nav className="settings-navigation" aria-label="设置导航">
|
|
||||||
<a
|
|
||||||
href="/settings/instances"
|
|
||||||
aria-current={route.kind === 'settings-system' ? undefined : 'page'}
|
|
||||||
>
|
|
||||||
实例管理
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/settings/system"
|
|
||||||
aria-current={route.kind === 'settings-system' ? 'page' : undefined}
|
|
||||||
>
|
|
||||||
系统与安全
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
) : null}
|
|
||||||
{routeInstanceId && instanceLoading ? (
|
{routeInstanceId && instanceLoading ? (
|
||||||
<p role="status" aria-label="实例加载状态">
|
<p role="status" aria-label="实例加载状态">
|
||||||
正在加载实例…
|
正在加载实例…
|
||||||
|
|||||||
@@ -341,30 +341,6 @@ main {
|
|||||||
background: #fbfcfd;
|
background: #fbfcfd;
|
||||||
box-shadow: 0 5px 22px rgba(20, 35, 55, 0.055);
|
box-shadow: 0 5px 22px rgba(20, 35, 55, 0.055);
|
||||||
}
|
}
|
||||||
.settings-navigation {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.3rem;
|
|
||||||
margin: -0.35rem 0 1rem;
|
|
||||||
padding-bottom: 0.75rem;
|
|
||||||
border-bottom: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
.settings-navigation a {
|
|
||||||
min-height: 2.35rem;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.4rem 0.75rem;
|
|
||||||
color: var(--muted);
|
|
||||||
border-radius: 7px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.settings-navigation a:hover,
|
|
||||||
.settings-navigation a[aria-current='page'] {
|
|
||||||
color: var(--primary);
|
|
||||||
background: var(--primary-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Shared page headers */
|
/* Shared page headers */
|
||||||
.fleet-heading,
|
.fleet-heading,
|
||||||
main > section > header,
|
main > section > header,
|
||||||
@@ -2879,26 +2855,6 @@ main {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-navigation {
|
|
||||||
width: fit-content;
|
|
||||||
gap: 0.35rem;
|
|
||||||
margin: 0 0 1.6rem;
|
|
||||||
padding: 0.35rem;
|
|
||||||
border: 2px solid #d8cdb7;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #fffdf5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-navigation a {
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-navigation a:hover,
|
|
||||||
.settings-navigation a[aria-current='page'] {
|
|
||||||
color: #725d42;
|
|
||||||
background: #fff0ae;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fleet-heading,
|
.fleet-heading,
|
||||||
main > section > header,
|
main > section > header,
|
||||||
.page-heading {
|
.page-heading {
|
||||||
|
|||||||
Reference in New Issue
Block a user