feat(platform): summarize quality issues
This commit is contained in:
@@ -6,6 +6,7 @@ import type {
|
||||
MileageSummary,
|
||||
OpsHealth,
|
||||
Page,
|
||||
QualitySummary,
|
||||
QualityIssueRow,
|
||||
RawFrameRow,
|
||||
RealtimeLocationRow,
|
||||
@@ -35,6 +36,7 @@ export const api = {
|
||||
rawFrames: (params = new URLSearchParams()) => request<Page<RawFrameRow>>(`/api/history/raw-frames?${params.toString()}`),
|
||||
mileageSummary: (params = new URLSearchParams()) => request<MileageSummary>(`/api/mileage/summary?${params.toString()}`),
|
||||
dailyMileage: (params = new URLSearchParams()) => request<Page<DailyMileageRow>>(`/api/mileage/daily?${params.toString()}`),
|
||||
qualitySummary: (params = new URLSearchParams()) => request<QualitySummary>(`/api/quality/summary?${params.toString()}`),
|
||||
qualityIssues: (params = new URLSearchParams()) => request<Page<QualityIssueRow>>(`/api/quality/issues?${params.toString()}`),
|
||||
opsHealth: () => request<OpsHealth>('/api/ops/health')
|
||||
};
|
||||
|
||||
@@ -150,6 +150,20 @@ export interface QualityIssueRow {
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface QualitySummary {
|
||||
issueVehicleCount: number;
|
||||
issueRecordCount: number;
|
||||
errorCount: number;
|
||||
warningCount: number;
|
||||
protocols: QualityBucketStat[];
|
||||
issueTypes: QualityBucketStat[];
|
||||
}
|
||||
|
||||
export interface QualityBucketStat {
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface OpsHealth {
|
||||
linkHealth: LinkHealth[];
|
||||
kafkaLag: number | null;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Card, Col, Form, Row, Select, Space, Table, Tag, Toast } from '@douyinfe/semi-ui';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { OpsHealth, QualityIssueRow } from '../api/types';
|
||||
import type { OpsHealth, QualitySummary, QualityIssueRow } from '../api/types';
|
||||
import { PageHeader } from '../components/PageHeader';
|
||||
|
||||
const statusColor: Record<string, 'green' | 'orange' | 'red' | 'grey'> = {
|
||||
@@ -18,19 +18,37 @@ function storageReadStatus(health: OpsHealth | null) {
|
||||
return health?.tdengineWritable && health.mysqlWritable ? 'ok' : 'error';
|
||||
}
|
||||
|
||||
const emptySummary: QualitySummary = {
|
||||
issueVehicleCount: 0,
|
||||
issueRecordCount: 0,
|
||||
errorCount: 0,
|
||||
warningCount: 0,
|
||||
protocols: [],
|
||||
issueTypes: []
|
||||
};
|
||||
|
||||
function qualityParams(values: Record<string, string>) {
|
||||
const params = new URLSearchParams();
|
||||
if (values?.keyword) params.set('keyword', values.keyword);
|
||||
if (values?.protocol) params.set('protocol', values.protocol);
|
||||
return params;
|
||||
}
|
||||
|
||||
export function Quality({ onHealthLoaded }: { onHealthLoaded?: (health: OpsHealth) => void }) {
|
||||
const [issues, setIssues] = useState<QualityIssueRow[]>([]);
|
||||
const [summary, setSummary] = useState<QualitySummary>(emptySummary);
|
||||
const [health, setHealth] = useState<OpsHealth | null>(null);
|
||||
const [loadingIssues, setLoadingIssues] = useState(true);
|
||||
const [loadingSummary, setLoadingSummary] = useState(true);
|
||||
const [loadingHealth, setLoadingHealth] = useState(true);
|
||||
const [filters, setFilters] = useState<Record<string, string>>({});
|
||||
const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 20, total: 0 });
|
||||
|
||||
const loadIssues = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
|
||||
setLoadingIssues(true);
|
||||
const params = new URLSearchParams({ limit: String(pageSize), offset: String((page - 1) * pageSize) });
|
||||
if (values?.keyword) params.set('keyword', values.keyword);
|
||||
if (values?.protocol) params.set('protocol', values.protocol);
|
||||
const params = qualityParams(values);
|
||||
params.set('limit', String(pageSize));
|
||||
params.set('offset', String((page - 1) * pageSize));
|
||||
api.qualityIssues(params)
|
||||
.then((nextPage) => {
|
||||
setIssues(nextPage.items);
|
||||
@@ -40,6 +58,14 @@ export function Quality({ onHealthLoaded }: { onHealthLoaded?: (health: OpsHealt
|
||||
.finally(() => setLoadingIssues(false));
|
||||
};
|
||||
|
||||
const loadSummary = (values: Record<string, string> = filters) => {
|
||||
setLoadingSummary(true);
|
||||
api.qualitySummary(qualityParams(values))
|
||||
.then(setSummary)
|
||||
.catch((error: Error) => Toast.error(error.message))
|
||||
.finally(() => setLoadingSummary(false));
|
||||
};
|
||||
|
||||
const loadHealth = () => {
|
||||
setLoadingHealth(true);
|
||||
api.opsHealth()
|
||||
@@ -52,6 +78,7 @@ export function Quality({ onHealthLoaded }: { onHealthLoaded?: (health: OpsHealt
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadSummary({});
|
||||
loadIssues({}, 1, pagination.pageSize);
|
||||
loadHealth();
|
||||
}, []);
|
||||
@@ -59,6 +86,22 @@ export function Quality({ onHealthLoaded }: { onHealthLoaded?: (health: OpsHealt
|
||||
return (
|
||||
<div className="vp-page">
|
||||
<PageHeader title="数据质量" description="断链、VIN 缺失、字段缺失和链路健康的排查入口" />
|
||||
<div className="vp-kpi-grid">
|
||||
{[
|
||||
{ label: '问题车辆', value: summary.issueVehicleCount.toLocaleString() },
|
||||
{ label: '问题记录', value: summary.issueRecordCount.toLocaleString() },
|
||||
{ label: '错误 / 警告', value: `${summary.errorCount}/${summary.warningCount}` },
|
||||
{
|
||||
label: '主要问题',
|
||||
value: summary.issueTypes.length > 0 ? `${summary.issueTypes[0].name} ${summary.issueTypes[0].count}` : '-'
|
||||
}
|
||||
].map((item) => (
|
||||
<Card key={item.label} bordered loading={loadingSummary}>
|
||||
<div className="vp-kpi-value">{item.value}</div>
|
||||
<div className="vp-kpi-label">{item.label}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Row gutter={16}>
|
||||
<Col span={8}><Card bordered title="Kafka Lag">{formatLag(health?.kafkaLag)}</Card></Col>
|
||||
<Col span={8}><Card bordered title="Redis 在线 Key">{formatLag(health?.redisOnlineKeys)}</Card></Col>
|
||||
@@ -72,6 +115,7 @@ export function Quality({ onHealthLoaded }: { onHealthLoaded?: (health: OpsHealt
|
||||
<Form layout="horizontal" onSubmit={(values) => {
|
||||
const nextFilters = values as Record<string, string>;
|
||||
setFilters(nextFilters);
|
||||
loadSummary(nextFilters);
|
||||
loadIssues(nextFilters, 1, pagination.pageSize);
|
||||
}} style={{ marginBottom: 12 }}>
|
||||
<Form.Input field="keyword" label="关键词" placeholder="手机号 / 来源地址 / 车牌" style={{ width: 240 }} />
|
||||
@@ -82,6 +126,7 @@ export function Quality({ onHealthLoaded }: { onHealthLoaded?: (health: OpsHealt
|
||||
<Button htmlType="submit" theme="solid" type="primary">筛选</Button>
|
||||
<Button onClick={() => {
|
||||
setFilters({});
|
||||
loadSummary({});
|
||||
loadIssues({}, 1, pagination.pageSize);
|
||||
}}>重置</Button>
|
||||
</Space>
|
||||
|
||||
Reference in New Issue
Block a user