feat(platform): keep canonical source summary slots
This commit is contained in:
@@ -261,6 +261,7 @@ func (m *MockStore) VehicleServiceSummary(ctx context.Context) (VehicleServiceSu
|
|||||||
for _, protocol := range protocolCounts {
|
for _, protocol := range protocolCounts {
|
||||||
summary.Protocols = append(summary.Protocols, *protocol)
|
summary.Protocols = append(summary.Protocols, *protocol)
|
||||||
}
|
}
|
||||||
|
summary.Protocols = completeProtocolStats(summary.Protocols)
|
||||||
for _, protocol := range canonicalVehicleProtocols {
|
for _, protocol := range canonicalVehicleProtocols {
|
||||||
missing := 0
|
missing := 0
|
||||||
for _, row := range coverage.Items {
|
for _, row := range coverage.Items {
|
||||||
@@ -271,7 +272,6 @@ func (m *MockStore) VehicleServiceSummary(ctx context.Context) (VehicleServiceSu
|
|||||||
summary.MissingSources = append(summary.MissingSources, MissingSourceStat{Protocol: protocol, Count: missing})
|
summary.MissingSources = append(summary.MissingSources, MissingSourceStat{Protocol: protocol, Count: missing})
|
||||||
}
|
}
|
||||||
sort.Slice(summary.ServiceStatuses, func(i, j int) bool { return summary.ServiceStatuses[i].Status < summary.ServiceStatuses[j].Status })
|
sort.Slice(summary.ServiceStatuses, func(i, j int) bool { return summary.ServiceStatuses[i].Status < summary.ServiceStatuses[j].Status })
|
||||||
sort.Slice(summary.Protocols, func(i, j int) bool { return summary.Protocols[i].Protocol < summary.Protocols[j].Protocol })
|
|
||||||
return summary, nil
|
return summary, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -923,7 +923,10 @@ func (s *ProductionStore) protocolStats(ctx context.Context) ([]ProtocolStat, er
|
|||||||
}
|
}
|
||||||
out = append(out, row)
|
out = append(out, row)
|
||||||
}
|
}
|
||||||
return out, rows.Err()
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return completeProtocolStats(out), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsedFieldsFromString(value string) map[string]any {
|
func parsedFieldsFromString(value string) map[string]any {
|
||||||
|
|||||||
@@ -58,6 +58,37 @@ type Service struct {
|
|||||||
|
|
||||||
var canonicalVehicleProtocols = []string{"GB32960", "JT808", "YUTONG_MQTT"}
|
var canonicalVehicleProtocols = []string{"GB32960", "JT808", "YUTONG_MQTT"}
|
||||||
|
|
||||||
|
func completeProtocolStats(stats []ProtocolStat) []ProtocolStat {
|
||||||
|
byProtocol := make(map[string]ProtocolStat, len(stats)+len(canonicalVehicleProtocols))
|
||||||
|
for _, stat := range stats {
|
||||||
|
protocol := strings.TrimSpace(stat.Protocol)
|
||||||
|
if protocol == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stat.Protocol = protocol
|
||||||
|
byProtocol[protocol] = stat
|
||||||
|
}
|
||||||
|
out := make([]ProtocolStat, 0, len(byProtocol)+len(canonicalVehicleProtocols))
|
||||||
|
seen := map[string]struct{}{}
|
||||||
|
for _, protocol := range canonicalVehicleProtocols {
|
||||||
|
stat := byProtocol[protocol]
|
||||||
|
stat.Protocol = protocol
|
||||||
|
out = append(out, stat)
|
||||||
|
seen[protocol] = struct{}{}
|
||||||
|
}
|
||||||
|
extra := make([]string, 0)
|
||||||
|
for protocol := range byProtocol {
|
||||||
|
if _, ok := seen[protocol]; !ok {
|
||||||
|
extra = append(extra, protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(extra)
|
||||||
|
for _, protocol := range extra {
|
||||||
|
out = append(out, byProtocol[protocol])
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func missingCanonicalProtocols(protocols []string) []string {
|
func missingCanonicalProtocols(protocols []string) []string {
|
||||||
missing := make([]string, 0, len(canonicalVehicleProtocols))
|
missing := make([]string, 0, len(canonicalVehicleProtocols))
|
||||||
for _, protocol := range canonicalVehicleProtocols {
|
for _, protocol := range canonicalVehicleProtocols {
|
||||||
|
|||||||
@@ -83,3 +83,22 @@ func TestVehicleServiceSummaryCountsProtocolOnlineByProtocolSlot(t *testing.T) {
|
|||||||
t.Fatalf("GB32960 online count must use GB32960 source status only, got %+v", gb32960)
|
t.Fatalf("GB32960 online count must use GB32960 source status only, got %+v", gb32960)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompleteProtocolStatsIncludesCanonicalSlots(t *testing.T) {
|
||||||
|
stats := completeProtocolStats([]ProtocolStat{{Protocol: "JT808", Online: 2, Total: 5}})
|
||||||
|
byProtocol := map[string]ProtocolStat{}
|
||||||
|
for _, stat := range stats {
|
||||||
|
byProtocol[stat.Protocol] = stat
|
||||||
|
}
|
||||||
|
for _, protocol := range canonicalVehicleProtocols {
|
||||||
|
if _, ok := byProtocol[protocol]; !ok {
|
||||||
|
t.Fatalf("canonical protocol %s should be present, got %+v", protocol, stats)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if byProtocol["JT808"].Online != 2 || byProtocol["JT808"].Total != 5 {
|
||||||
|
t.Fatalf("existing protocol stats should be preserved, got %+v", byProtocol["JT808"])
|
||||||
|
}
|
||||||
|
if byProtocol["GB32960"].Online != 0 || byProtocol["GB32960"].Total != 0 {
|
||||||
|
t.Fatalf("missing canonical protocol should be exposed as zero slot, got %+v", byProtocol["GB32960"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,6 +44,13 @@ function formatCount(value?: number | null) {
|
|||||||
return value == null ? '0' : value.toLocaleString();
|
return value == null ? '0' : value.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatProtocolRate(row: ProtocolStat) {
|
||||||
|
if (!Number.isFinite(row.total) || row.total <= 0) {
|
||||||
|
return '0%';
|
||||||
|
}
|
||||||
|
return `${Math.round((row.online / row.total) * 100)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
function rowServiceStatus(row: { serviceStatus?: { title: string; severity: string }; onlineSourceCount: number; sourceCount: number }) {
|
function rowServiceStatus(row: { serviceStatus?: { title: string; severity: string }; onlineSourceCount: number; sourceCount: number }) {
|
||||||
if (row.serviceStatus) {
|
if (row.serviceStatus) {
|
||||||
return {
|
return {
|
||||||
@@ -201,7 +208,7 @@ export function Dashboard({ onOpenVehicle, onOpenQuality, onOpenVehicles }: { on
|
|||||||
{ title: '总数', dataIndex: 'total' },
|
{ title: '总数', dataIndex: 'total' },
|
||||||
{
|
{
|
||||||
title: '在线率',
|
title: '在线率',
|
||||||
render: (_: unknown, row: ProtocolStat) => `${Math.round((row.online / row.total) * 100)}%`
|
render: (_: unknown, row: ProtocolStat) => formatProtocolRate(row)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '缺失车辆',
|
title: '缺失车辆',
|
||||||
|
|||||||
@@ -96,7 +96,11 @@ test('dashboard renders vehicle service summary metrics', async () => {
|
|||||||
{ status: 'healthy', title: '服务正常', count: 161 },
|
{ status: 'healthy', title: '服务正常', count: 161 },
|
||||||
{ status: 'degraded', title: '部分来源离线', count: 41 }
|
{ status: 'degraded', title: '部分来源离线', count: 41 }
|
||||||
],
|
],
|
||||||
protocols: [{ protocol: 'JT808', online: 157, total: 388 }]
|
protocols: [
|
||||||
|
{ protocol: 'JT808', online: 157, total: 388 },
|
||||||
|
{ protocol: 'YUTONG_MQTT', online: 0, total: 0 }
|
||||||
|
],
|
||||||
|
missingSources: [{ protocol: 'YUTONG_MQTT', count: 1033 }]
|
||||||
},
|
},
|
||||||
traceId: 'trace-test',
|
traceId: 'trace-test',
|
||||||
timestamp: 1783094400000
|
timestamp: 1783094400000
|
||||||
@@ -116,7 +120,7 @@ test('dashboard renders vehicle service summary metrics', async () => {
|
|||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
expect(await screen.findByText('总车辆')).toBeInTheDocument();
|
expect(await screen.findByText('总车辆')).toBeInTheDocument();
|
||||||
expect(screen.getByText('1,033')).toBeInTheDocument();
|
expect(screen.getAllByText('1,033').length).toBeGreaterThanOrEqual(1);
|
||||||
expect(screen.getByText('单源车辆')).toBeInTheDocument();
|
expect(screen.getByText('单源车辆')).toBeInTheDocument();
|
||||||
expect(screen.getByText('391')).toBeInTheDocument();
|
expect(screen.getByText('391')).toBeInTheDocument();
|
||||||
expect(screen.getByText('多源车辆')).toBeInTheDocument();
|
expect(screen.getByText('多源车辆')).toBeInTheDocument();
|
||||||
@@ -125,6 +129,9 @@ test('dashboard renders vehicle service summary metrics', async () => {
|
|||||||
expect(screen.getByText('461')).toBeInTheDocument();
|
expect(screen.getByText('461')).toBeInTheDocument();
|
||||||
expect(screen.getByText('身份未绑定')).toBeInTheDocument();
|
expect(screen.getByText('身份未绑定')).toBeInTheDocument();
|
||||||
expect(screen.getByText('来源证据在线分布')).toBeInTheDocument();
|
expect(screen.getByText('来源证据在线分布')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('YUTONG_MQTT')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('0%')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('NaN%')).not.toBeInTheDocument();
|
||||||
expect(fetchMock).toHaveBeenCalledWith('/api/vehicle-service/summary', undefined);
|
expect(fetchMock).toHaveBeenCalledWith('/api/vehicle-service/summary', undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user