feat(platform): harden telemetry pipeline and unify Semi UI workspaces
This commit is contained in:
@@ -19,8 +19,8 @@ describe('route recovery boundary', () => {
|
||||
render(<PlatformErrorBoundary><BrokenShell /></PlatformErrorBoundary>);
|
||||
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('平台外壳运行异常');
|
||||
expect(screen.getByRole('button', { name: /重新加载平台/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: /返回全局监控/ })).toHaveAttribute('href', '/monitor');
|
||||
expect(screen.getByRole('button', { name: /重新加载平台/ })).toHaveClass('semi-button');
|
||||
expect(screen.getByRole('button', { name: /返回全局监控/ })).toHaveClass('semi-button');
|
||||
});
|
||||
|
||||
it('recognizes deployment-related lazy chunk failures', () => {
|
||||
@@ -35,8 +35,8 @@ describe('route recovery boundary', () => {
|
||||
const BrokenPage = () => { throw new Error('render exploded'); };
|
||||
render(<MemoryRouter future={ROUTER_FUTURE} initialEntries={['/history']}><RoutePage page={BrokenPage} label="历史数据" /></MemoryRouter>);
|
||||
expect(screen.getByRole('alert')).toHaveTextContent('当前模块暂时无法显示');
|
||||
expect(screen.getByRole('button', { name: /刷新当前页面/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: /返回全局监控/ })).toHaveAttribute('href', '/monitor');
|
||||
expect(screen.getByRole('button', { name: /刷新当前页面/ })).toHaveClass('semi-button');
|
||||
expect(screen.getByRole('button', { name: /返回全局监控/ })).toHaveClass('semi-button');
|
||||
});
|
||||
|
||||
it('shows a deployment recovery page when an automatic chunk reload already ran', () => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { IconAlertTriangle, IconHome, IconRefresh } from '@douyinfe/semi-icons';
|
||||
import { IconAlertTriangle } from '@douyinfe/semi-icons';
|
||||
import { Component, type ElementType, type ErrorInfo, type ReactNode, Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { PageLoading } from '../shared/AsyncState';
|
||||
import { RecoveryActions } from '../shared/RecoveryActions';
|
||||
|
||||
const CHUNK_RELOAD_KEY = 'vehicle-platform:route-chunk-reload';
|
||||
const CHUNK_RELOAD_COOLDOWN_MS = 2 * 60_000;
|
||||
@@ -64,11 +65,12 @@ class RecoverableRouteBoundary extends Component<{ children: ReactNode; routeKey
|
||||
<h2>{chunkTimeout ? '当前模块没有在预期时间内加载完成' : chunkError ? '正在等待加载最新页面资源' : '当前模块暂时无法显示'}</h2>
|
||||
<p>{chunkTimeout ? '系统已等待并尝试恢复,可能是网络暂时不稳定。刷新后会重新加载当前模块,不会丢失服务端数据。' : chunkError ? '通常是发布后旧标签页仍引用上一版本资源。刷新后会恢复,不会丢失服务端数据。' : '页面已被安全隔离,侧栏和其他模块仍可使用。更换筛选条件会自动重试,也可刷新当前页面。'}</p>
|
||||
<code>{error.message || error.name}</code>
|
||||
<footer>
|
||||
<button type="button" onClick={this.props.onRetry}><IconRefresh />重试加载模块</button>
|
||||
<button type="button" onClick={() => window.location.reload()}><IconRefresh />刷新当前页面</button>
|
||||
<a href="/monitor"><IconHome />返回全局监控</a>
|
||||
</footer>
|
||||
<RecoveryActions
|
||||
primaryLabel="重试加载模块"
|
||||
onPrimary={this.props.onRetry}
|
||||
secondaryLabel="刷新当前页面"
|
||||
onSecondary={() => window.location.reload()}
|
||||
/>
|
||||
</div>
|
||||
</section>;
|
||||
}
|
||||
@@ -91,10 +93,7 @@ export class PlatformErrorBoundary extends Component<{ children: ReactNode }, {
|
||||
<h2>工作台暂时无法继续显示</h2>
|
||||
<p>系统已阻止异常扩散。请重新加载最新版本;服务端数据不会因页面恢复操作而改变。</p>
|
||||
<code>{error.message || error.name}</code>
|
||||
<footer>
|
||||
<button type="button" onClick={() => window.location.reload()}><IconRefresh />重新加载平台</button>
|
||||
<a href="/monitor"><IconHome />返回全局监控</a>
|
||||
</footer>
|
||||
<RecoveryActions primaryLabel="重新加载平台" onPrimary={() => window.location.reload()} />
|
||||
</div>
|
||||
</section>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { MonitorViewport } from '../hooks/useMonitorData';
|
||||
import { normalizeMonitorBounds } from '../domain/monitor';
|
||||
|
||||
export const MONITOR_RETURN_PARAM = 'monitorReturn';
|
||||
|
||||
@@ -38,19 +39,10 @@ function decimal(value: string | null, fallback: number, minimum: number, maximu
|
||||
return Number.isFinite(parsed) && parsed >= minimum && parsed <= maximum ? parsed : fallback;
|
||||
}
|
||||
|
||||
function validBounds(value: string | null) {
|
||||
if (!value) return '';
|
||||
const coordinates = value.split(',').map(Number);
|
||||
if (coordinates.length !== 4 || coordinates.some((coordinate) => !Number.isFinite(coordinate))) return '';
|
||||
const [west, south, east, north] = coordinates;
|
||||
if (west < -180 || east > 180 || south < -90 || north > 90 || west >= east || south >= north) return '';
|
||||
return coordinates.map((coordinate) => coordinate.toFixed(6)).join(',');
|
||||
}
|
||||
|
||||
export function parseMonitorRouteContext(search: string | URLSearchParams): MonitorRouteContext {
|
||||
const params = typeof search === 'string' ? new URLSearchParams(search) : search;
|
||||
const selectedVin = params.get('selectedVin')?.trim().toUpperCase() ?? '';
|
||||
const bounds = validBounds(params.get('bounds'));
|
||||
const bounds = normalizeMonitorBounds(params.get('bounds'));
|
||||
const hasViewport = params.has('zoom') || Boolean(bounds);
|
||||
return {
|
||||
mode: params.get('mode') === 'list' ? 'list' : 'map',
|
||||
@@ -80,7 +72,8 @@ export function buildMonitorPath(context: Omit<MonitorRouteContext, 'hasViewport
|
||||
params.set('detail', context.detailOpen ? 'open' : 'collapsed');
|
||||
}
|
||||
params.set('zoom', String(Math.round(context.viewport.zoom * 100) / 100));
|
||||
if (validBounds(context.viewport.bounds)) params.set('bounds', validBounds(context.viewport.bounds));
|
||||
const bounds = normalizeMonitorBounds(context.viewport.bounds);
|
||||
if (bounds) params.set('bounds', bounds);
|
||||
if (context.listOffset) params.set('listOffset', String(context.listOffset));
|
||||
if (context.listLimit !== 50) params.set('listLimit', String(context.listLimit));
|
||||
const query = params.toString();
|
||||
|
||||
Reference in New Issue
Block a user