feat(web): localize and redesign operations console

This commit is contained in:
chick
2026-07-18 22:22:41 +08:00
parent d575ab4d9d
commit abfd07f8a3
34 changed files with 1536 additions and 1058 deletions
+41 -41
View File
@@ -20,7 +20,7 @@ const snapshot: FleetSnapshot = {
{
id: 'bravo',
name: 'Bravo',
url: 'http://user:secret@bravo.example:8080/private',
url: 'http://user:***@bravo.example:8080/private',
tags: ['west'],
},
{ id: 'alpha', name: 'Alpha', url: 'https://alpha.example/admin' },
@@ -69,8 +69,8 @@ describe('React AppShell and Fleet vertical slice', () => {
const dataSource = source(() => new Promise((done) => (resolve = done)));
render(<AppShell pathname="/fleet" version="0.1.0" fleetDataSource={dataSource} />);
expect(screen.getByRole('status', { name: 'Fleet loading status' }).textContent).toContain(
'Loading instances',
expect(screen.getByRole('status', { name: '实例加载状态' }).textContent).toContain(
'正在加载实例',
);
resolve(snapshot);
@@ -80,10 +80,10 @@ describe('React AppShell and Fleet vertical slice', () => {
);
expect(
within(screen.getByRole('row', { name: /Bravo/ }))
.getByRole('link', { name: 'Open Bravo origin' })
.getByRole('link', { name: '打开 Bravo 的源站' })
.getAttribute('href'),
).toBe('http://bravo.example:8080');
expect(screen.getByText('Version 0.1.0')).toBeTruthy();
expect(screen.getByText('版本 0.1.0')).toBeTruthy();
expect(
consoleError.mock.calls.some((call) =>
call.some((argument) => String(argument).includes('unique "key" prop')),
@@ -96,23 +96,23 @@ describe('React AppShell and Fleet vertical slice', () => {
render(<AppShell pathname="/fleet" fleetDataSource={source(async () => snapshot)} />);
await screen.findByRole('row', { name: /Alpha/ });
await user.click(screen.getByRole('button', { name: /Sort by latency/i }));
await user.click(screen.getByRole('button', { name: /按延迟排序/i }));
let rows = screen.getAllByRole('row').slice(1);
expect(rows[0]?.textContent).toContain('Alpha');
await user.click(screen.getByRole('button', { name: /Sort by latency/i }));
await user.click(screen.getByRole('button', { name: /按延迟排序/i }));
rows = screen.getAllByRole('row').slice(1);
expect(rows[0]?.textContent).toContain('Bravo');
await user.selectOptions(screen.getByRole('combobox', { name: 'Status' }), 'auth');
await user.selectOptions(screen.getByRole('combobox', { name: '状态' }), 'auth');
expect(screen.queryByRole('row', { name: /Alpha/ })).toBeNull();
await user.click(screen.getByRole('checkbox', { name: 'Select all visible instances' }));
expect(
(screen.getByRole('checkbox', { name: 'Select Bravo' }) as HTMLInputElement).checked,
).toBe(true);
expect(screen.getByText('1 selected')).toBeTruthy();
await user.click(screen.getByRole('checkbox', { name: '选择当前页全部实例' }));
expect((screen.getByRole('checkbox', { name: '选择 Bravo' }) as HTMLInputElement).checked).toBe(
true,
);
expect(screen.getByText('已选择 1 项')).toBeTruthy();
await user.type(screen.getByRole('searchbox', { name: 'Search instances' }), 'missing');
expect(screen.getByText(/No instances match your search/i)).toBeTruthy();
await user.type(screen.getByRole('searchbox', { name: '搜索实例' }), 'missing');
expect(screen.getByText(/没有实例符合搜索与筛选条件/i)).toBeTruthy();
});
it('exposes error, retry, configured-empty, and unsafe-origin states without claiming a backend', async () => {
@@ -126,10 +126,10 @@ describe('React AppShell and Fleet vertical slice', () => {
});
const first = render(<AppShell pathname="/fleet" fleetDataSource={source(load)} />);
expect((await screen.findByRole('alert')).textContent).toContain('collector unavailable');
await user.click(screen.getByRole('button', { name: 'Retry loading instances' }));
expect((await screen.findByRole('alert')).textContent).toContain('实例加载失败');
await user.click(screen.getByRole('button', { name: '重试加载实例' }));
expect(await screen.findByRole('row', { name: /Unsafe/ })).toBeTruthy();
expect(screen.queryByRole('link', { name: 'Open Unsafe origin' })).toBeNull();
expect(screen.queryByRole('link', { name: '打开 Unsafe 的源站' })).toBeNull();
first.unmount();
const { unmount } = render(
@@ -138,7 +138,7 @@ describe('React AppShell and Fleet vertical slice', () => {
fleetDataSource={source(async () => ({ instances: [], statuses: new Map() }))}
/>,
);
expect(await screen.findByText(/No instances are configured/i)).toBeTruthy();
expect(await screen.findByText(/尚未配置实例/i)).toBeTruthy();
unmount();
});
@@ -147,20 +147,20 @@ describe('React AppShell and Fleet vertical slice', () => {
render(<AppShell pathname="/fleet" fleetDataSource={source(async () => snapshot)} />);
await screen.findByRole('row', { name: /Alpha/ });
await user.selectOptions(screen.getByRole('combobox', { name: 'Capability' }), 'sms');
await user.selectOptions(screen.getByRole('combobox', { name: '能力' }), 'sms');
expect(screen.queryByRole('row', { name: /Alpha/ })).toBeNull();
expect(screen.getByRole('row', { name: /Bravo/ }).textContent).toContain('clock drift');
await user.click(screen.getByRole('button', { name: 'Choose columns' }));
await user.click(screen.getByRole('checkbox', { name: 'Show anomalies column' }));
expect(screen.queryByRole('columnheader', { name: 'Anomalies' })).toBeNull();
await user.click(screen.getByRole('button', { name: '选择列' }));
await user.click(screen.getByRole('checkbox', { name: '显示异常列' }));
expect(screen.queryByRole('columnheader', { name: '异常' })).toBeNull();
await user.click(screen.getByRole('checkbox', { name: 'Select Bravo' }));
expect(
(screen.getByRole('button', { name: 'Batch actions' }) as HTMLButtonElement).disabled,
).toBe(false);
await user.click(screen.getByRole('button', { name: 'Batch actions' }));
expect(screen.getByText('Choose an action for 1 selected instance.')).toBeTruthy();
await user.click(screen.getByRole('checkbox', { name: '选择 Bravo' }));
expect((screen.getByRole('button', { name: '批量操作' }) as HTMLButtonElement).disabled).toBe(
false,
);
await user.click(screen.getByRole('button', { name: '批量操作' }));
expect(screen.getByText('请为已选择的 1 项选择操作。')).toBeTruthy();
});
it('paginates fleet rows and selects only the current page', async () => {
@@ -177,12 +177,12 @@ describe('React AppShell and Fleet vertical slice', () => {
await screen.findByRole('row', { name: /Instance 01/ });
expect(screen.queryByRole('row', { name: /Instance 11/ })).toBeNull();
expect(screen.getByText('Page 1 of 2')).toBeTruthy();
await user.click(screen.getByRole('checkbox', { name: 'Select all visible instances' }));
expect(screen.getByText('10 selected')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Next page' }));
expect(screen.getByText('第 1 页,共 2 页')).toBeTruthy();
await user.click(screen.getByRole('checkbox', { name: '选择当前页全部实例' }));
expect(screen.getByText('已选择 10 项')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '下一页' }));
expect(await screen.findByRole('row', { name: /Instance 11/ })).toBeTruthy();
expect(screen.getByText('Page 2 of 2')).toBeTruthy();
expect(screen.getByText('第 2 页,共 2 页')).toBeTruthy();
});
it('binds instance context to the route owner and never leaks mismatched context', () => {
@@ -197,27 +197,27 @@ describe('React AppShell and Fleet vertical slice', () => {
const { rerender } = render(
<AppShell pathname="/instances/owner/messages" instance={instance} />,
);
expect(screen.getByRole('heading', { name: 'Messages' })).toBeTruthy();
expect(screen.getByRole('heading', { name: '消息' })).toBeTruthy();
expect(screen.getByText('Owner modem')).toBeTruthy();
expect(screen.getByRole('link', { name: 'Open original site' }).getAttribute('href')).toBe(
expect(screen.getByRole('link', { name: '打开源站' }).getAttribute('href')).toBe(
'https://owner.example',
);
rerender(<AppShell pathname="/instances/someone-else/messages" instance={instance} />);
expect(screen.queryByText('Owner modem')).toBeNull();
expect(screen.getByText(/Instance context is unavailable/i)).toBeTruthy();
expect(screen.getByText(/此路由缺少实例上下文/i)).toBeTruthy();
});
it.each([
{
pathname: '/jobs',
endpoint: '/api/v1/jobs?page=1&pageSize=25&sort=createdAt&direction=desc',
emptyMessage: /No jobs match the current query/i,
emptyMessage: /没有任务符合当前查询/i,
},
{
pathname: '/audit',
endpoint: '/api/v1/audit?sort=occurredAt&direction=desc&page=1&pageSize=25',
emptyMessage: /No audit events match the current query/i,
emptyMessage: /没有审计事件符合当前查询/i,
},
])(
'uses the default HTTP data source for $pathname',
@@ -247,12 +247,12 @@ describe('React AppShell and Fleet vertical slice', () => {
{
pathname: '/jobs',
sourceProp: 'jobsDataSource' as const,
emptyMessage: /No jobs match the current query/i,
emptyMessage: /没有任务符合当前查询/i,
},
{
pathname: '/audit',
sourceProp: 'auditDataSource' as const,
emptyMessage: /No audit events match the current query/i,
emptyMessage: /没有审计事件符合当前查询/i,
},
])(
'uses an explicitly injected data source instead of global fetch for $pathname',