polish operations quality workspace
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
import OperationsPage from './OperationsPage';
|
||||
|
||||
@@ -14,6 +15,15 @@ vi.mock('../hooks/useMobileLayout', () => ({ useMobileLayout: () => layout.mobil
|
||||
|
||||
afterEach(() => { cleanup(); layout.mobile = false; Object.values(mocks).forEach((mock) => mock.mockReset()); });
|
||||
|
||||
function LocationProbe() {
|
||||
const location = useLocation();
|
||||
return <output data-testid="operations-location">{location.pathname}{location.search}</output>;
|
||||
}
|
||||
|
||||
function renderOperations(client: QueryClient, initialEntries = ['/operations']) {
|
||||
return render(<MemoryRouter initialEntries={initialEntries}><QueryClientProvider client={client}><OperationsPage /><LocationProbe /></QueryClientProvider></MemoryRouter>);
|
||||
}
|
||||
|
||||
function seedSession() {
|
||||
mocks.session.mockResolvedValue({ name: '平台管理员', role: 'admin', userType: 'admin', authMode: 'enforce', menuKeys: ['operations'] });
|
||||
mocks.reconciliationSummary.mockResolvedValue({
|
||||
@@ -56,7 +66,7 @@ test('renders reconciliation queue, loads evidence on demand and records review
|
||||
version: 2
|
||||
});
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
renderOperations(client);
|
||||
|
||||
expect(await screen.findByText('质量问题队列')).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '运行与数据质量', level: 5 })).toBeInTheDocument();
|
||||
@@ -64,8 +74,8 @@ test('renders reconciliation queue, loads evidence on demand and records review
|
||||
const navigation = screen.getByRole('navigation', { name: '运维质量视图' });
|
||||
expect(navigation).toHaveClass('v2-ops-navigation');
|
||||
expect(within(navigation).getByText('每日自动对账')).toBeInTheDocument();
|
||||
expect(within(navigation).getByText('证据处置')).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: '差异处置', selected: true })).toHaveClass('semi-button');
|
||||
expect(await within(navigation).findByText('1 项待复核')).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: '差异处置 1', selected: true })).toHaveClass('semi-button');
|
||||
expect(screen.getByRole('tabpanel', { name: '差异处置' })).toHaveAttribute('tabindex', '0');
|
||||
expect(document.querySelector('.v2-ops-page')).toHaveClass('is-reconciliation');
|
||||
expect(screen.queryByText('先选择一辆车')).not.toBeInTheDocument();
|
||||
@@ -73,7 +83,8 @@ test('renders reconciliation queue, loads evidence on demand and records review
|
||||
expect(screen.getByText('质量问题队列').closest('.v2-workspace-panel-header')).toHaveClass('v2-reconcile-heading');
|
||||
expect(document.querySelector('.v2-reconcile-kpi-card small')?.textContent).toBe('待复核');
|
||||
expect(document.querySelectorAll('.v2-reconcile-kpi-card')).toHaveLength(4);
|
||||
expect(screen.getByText('超过 24 小时').closest('.semi-card')).toHaveClass('v2-reconcile-sla');
|
||||
expect(screen.getByText('SLA 超时').closest('.semi-card')).toHaveClass('v2-reconcile-kpi-card', 'is-sla', 'is-overdue');
|
||||
expect(screen.queryByText('活跃问题', { exact: true })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument();
|
||||
const trendButton = screen.getByRole('button', { name: '30 天趋势' });
|
||||
expect(trendButton).toHaveAttribute('aria-expanded', 'false');
|
||||
@@ -125,7 +136,7 @@ test('renders only the compact mobile reconciliation surface and defers secondar
|
||||
});
|
||||
mocks.sourceReadiness.mockResolvedValue({ totalVehicles: 1, boundVehicles: 1, identityRequiredVehicles: 0, onlineVehicles: 1, sources: [] });
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
renderOperations(client);
|
||||
|
||||
expect(await screen.findByText('多来源实时位置漂移')).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-reconcile-mobile-card.semi-card')).toBeInTheDocument();
|
||||
@@ -173,9 +184,10 @@ test('reconciles service identities with bound and identity-required vehicles',
|
||||
}]
|
||||
});
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
renderOperations(client, ['/operations?view=health']);
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: '全局健康' }));
|
||||
expect(screen.getByRole('tab', { name: '全局健康', selected: true })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('operations-location')).toHaveTextContent('/operations?view=health');
|
||||
expect(await screen.findByText('1024 / 234')).toBeInTheDocument();
|
||||
expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('15 秒刷新运行证据')).toBeInTheDocument();
|
||||
expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('运行正常')).toBeInTheDocument();
|
||||
@@ -207,6 +219,7 @@ test('reconciles service identities with bound and identity-required vehicles',
|
||||
expect(screen.getByText('验收标准').closest('.semi-card')).toHaveClass('v2-ops-source-card');
|
||||
expect(screen.queryByText('单车多来源诊断')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('tab', { name: '单车诊断' }));
|
||||
expect(screen.getByTestId('operations-location')).toHaveTextContent('/operations?view=diagnostic');
|
||||
expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('按需读取单车来源')).toBeInTheDocument();
|
||||
expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('精确诊断')).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '诊断车辆', level: 5 })).toBeInTheDocument();
|
||||
@@ -228,7 +241,7 @@ test('keeps health evidence visible when source readiness fails and retries that
|
||||
kafkaLag: 0, activeConnections: 10, redisOnlineKeys: 5, platformRelease: 'test-release', sources: []
|
||||
});
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
renderOperations(client);
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: '全局健康' }));
|
||||
expect(await screen.findByText('来源就绪度暂时不可用')).toBeInTheDocument();
|
||||
@@ -269,7 +282,7 @@ test('fuzzy searches a vehicle and renders all source diagnosis evidence', async
|
||||
mocks.vehicleSourceDiagnostic.mockResolvedValue(diagnostic);
|
||||
mocks.updateVehicleSourcePolicy.mockResolvedValue(diagnostic);
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
renderOperations(client);
|
||||
fireEvent.click(screen.getByRole('tab', { name: '单车诊断' }));
|
||||
fireEvent.change(screen.getByLabelText('按车牌或 VIN 搜索诊断车辆'), { target: { value: '粤A' } });
|
||||
const candidateButton = await waitFor(() => {
|
||||
@@ -341,7 +354,7 @@ test('allows provider maintenance but keeps canonical source policy read only',
|
||||
mocks.vehicleSourceDiagnostic.mockResolvedValue(diagnostic);
|
||||
mocks.updateVehicleSourcePolicy.mockResolvedValue(diagnostic);
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(<QueryClientProvider client={client}><OperationsPage /></QueryClientProvider>);
|
||||
renderOperations(client);
|
||||
fireEvent.click(screen.getByRole('tab', { name: '单车诊断' }));
|
||||
fireEvent.change(screen.getByLabelText('按车牌或 VIN 搜索诊断车辆'), { target: { value: '沪A' } });
|
||||
const candidateButton = await waitFor(() => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { IconRefresh, IconSearch, IconTickCircle } from '@douyinfe/semi-icons';
|
||||
import { Button, Card, CardGroup, Checkbox, Descriptions, Empty, Input, Progress, SideSheet, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { FormEvent, useDeferredValue, useEffect, useState } from 'react';
|
||||
import { FormEvent, useDeferredValue, useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { api } from '../../api/client';
|
||||
import type { VehicleCoverageRow, VehicleLocationSourceEvidence, VehicleSourceDiagnostic } from '../../api/types';
|
||||
import { InlineError } from '../shared/AsyncState';
|
||||
@@ -17,6 +18,7 @@ import { useSideSheetA11y } from '../hooks/useSideSheetA11y';
|
||||
import ReconciliationCenter from './ReconciliationCenter';
|
||||
|
||||
type OperationsWorkspace = 'reconciliation' | 'diagnostic' | 'health';
|
||||
const operationsWorkspaces = new Set<OperationsWorkspace>(['reconciliation', 'diagnostic', 'health']);
|
||||
|
||||
function statusLabel(status: string) {
|
||||
return { ok: '正常', warning: '关注', error: '异常' }[status] ?? status;
|
||||
@@ -324,18 +326,39 @@ function SourceDiagnosticWorkspace() {
|
||||
}
|
||||
|
||||
export default function OperationsPage() {
|
||||
const [workspace, setWorkspace] = useState<OperationsWorkspace>('reconciliation');
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const requestedWorkspace = searchParams.get('view') as OperationsWorkspace | null;
|
||||
const workspace = requestedWorkspace && operationsWorkspaces.has(requestedWorkspace) ? requestedWorkspace : 'reconciliation';
|
||||
const setWorkspace = (next: OperationsWorkspace) => {
|
||||
const copy = new URLSearchParams(searchParams);
|
||||
if (next === 'reconciliation') copy.delete('view');
|
||||
else copy.set('view', next);
|
||||
setSearchParams(copy, { replace: true });
|
||||
};
|
||||
const health = useQuery({ queryKey: ['ops-health-v2'], queryFn: ({ signal }) => api.opsHealth(signal), refetchInterval: 15_000, staleTime: 8_000, gcTime: QUERY_MEMORY.summaryGcTime, ...LIVE_QUERY_POLICY });
|
||||
const readiness = useQuery({ queryKey: ['ops-source-readiness-v2'], queryFn: ({ signal }) => api.sourceReadiness(signal), refetchInterval: 30_000, staleTime: 15_000, gcTime: QUERY_MEMORY.summaryGcTime, ...LIVE_QUERY_POLICY });
|
||||
const reconciliation = useQuery({ queryKey: ['reconciliation-summary', 30], queryFn: ({ signal }) => api.reconciliationSummary(30, signal), staleTime: 60_000, gcTime: QUERY_MEMORY.summaryGcTime });
|
||||
const data = health.data; const sources = readiness.data;
|
||||
const refresh = () => Promise.all([health.refetch(), readiness.refetch()]);
|
||||
const refresh = () => Promise.all([health.refetch(), readiness.refetch(), reconciliation.refetch()]);
|
||||
const linkIssueCount = data?.linkHealth.filter((item) => item.status !== 'ok').length ?? 0;
|
||||
const runtimeIssueCount = data ? Number(!data.mysqlWritable) + Number(!data.tdengineWritable) + Number(data.runtime.dataMode !== 'production') : 0;
|
||||
const capacityIssueCount = data?.capacityFindings.length ?? 0;
|
||||
const healthIssueCount = linkIssueCount + runtimeIssueCount + capacityIssueCount + Number((data?.kafkaLag ?? 0) > 0);
|
||||
const overallStatus = !data ? 'unknown' : healthIssueCount > 0 ? 'warning' : 'ok';
|
||||
const sortedLinks = useMemo(() => [...(data?.linkHealth ?? [])].sort((left, right) => {
|
||||
const priority = { error: 0, warning: 1, ok: 2 } as Record<string, number>;
|
||||
return (priority[left.status] ?? 3) - (priority[right.status] ?? 3);
|
||||
}), [data?.linkHealth]);
|
||||
const sortedSources = useMemo(() => [...(sources?.sources ?? [])].sort((left, right) => {
|
||||
const priority = { error: 0, warning: 1, ok: 2 } as Record<string, number>;
|
||||
return (priority[left.severity] ?? 3) - (priority[right.severity] ?? 3);
|
||||
}), [sources?.sources]);
|
||||
const workspaceContext = workspace === 'reconciliation'
|
||||
? { description: '每日自动对账', status: '证据处置', color: 'blue' as const }
|
||||
? {
|
||||
description: '每日自动对账',
|
||||
status: reconciliation.data ? `${reconciliation.data.pending.toLocaleString('zh-CN')} 项待复核` : '读取差异证据',
|
||||
color: reconciliation.data?.overSla ? 'orange' as const : 'blue' as const
|
||||
}
|
||||
: workspace === 'diagnostic'
|
||||
? { description: '按需读取单车来源', status: '精确诊断', color: 'cyan' as const }
|
||||
: {
|
||||
@@ -362,9 +385,9 @@ export default function OperationsPage() {
|
||||
value={workspace}
|
||||
onChange={setWorkspace}
|
||||
items={[
|
||||
{ key: 'reconciliation', label: '差异处置' },
|
||||
{ key: 'reconciliation', label: '差异处置', count: reconciliation.data?.pending || undefined },
|
||||
{ key: 'diagnostic', label: '单车诊断' },
|
||||
{ key: 'health', label: '全局健康' }
|
||||
{ key: 'health', label: '全局健康', count: healthIssueCount || undefined }
|
||||
]}
|
||||
/>
|
||||
<div className="v2-ops-navigation-meta" aria-live="polite">
|
||||
@@ -392,7 +415,7 @@ export default function OperationsPage() {
|
||||
<OpsMetric label="服务身份 / 在线" value={sources ? `${sources.totalVehicles} / ${sources.onlineVehicles}` : '—'} detail={sources ? `已绑定 ${sources.boundVehicles} · 待绑定 ${sources.identityRequiredVehicles}` : '档案与快照并集'} />
|
||||
</section>
|
||||
</Card>
|
||||
<div className="v2-ops-grid"><Card className="v2-ops-panel v2-ops-links" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="数据链路" description="按链路查看当前状态与服务端诊断证据" meta={<HealthTag status={!data ? 'unknown' : linkIssueCount > 0 ? 'warning' : 'ok'}>{!data ? '读取中' : linkIssueCount > 0 ? `${linkIssueCount} 条异常` : '链路正常'}</HealthTag>} /><div className="v2-ops-link-list" role="list">{data?.linkHealth.length ? data.linkHealth.map((item) => <article className={`v2-ops-link-card is-${item.status}`} key={item.name} role="listitem"><span className="v2-ops-link-status"><i /><strong>{item.name}</strong></span><p>{item.detail || '无补充信息'}</p><HealthTag status={item.status} /></article>) : <Empty className="v2-ops-link-empty" title={data ? '暂无链路探针' : '正在读取链路状态'} description={data ? '当前响应没有可展示的数据链路证据。' : '全局健康数据返回后将在这里更新。'} />}</div></Card>
|
||||
<div className="v2-ops-grid"><Card className="v2-ops-panel v2-ops-links" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="数据链路" description="按链路查看当前状态与服务端诊断证据" meta={<HealthTag status={!data ? 'unknown' : linkIssueCount > 0 ? 'warning' : 'ok'}>{!data ? '读取中' : linkIssueCount > 0 ? `${linkIssueCount} 条异常` : '链路正常'}</HealthTag>} /><div className="v2-ops-link-list" role="list">{sortedLinks.length ? sortedLinks.map((item) => <article className={`v2-ops-link-card is-${item.status}`} key={item.name} role="listitem"><span className="v2-ops-link-status"><i /><strong>{item.name}</strong></span><p>{item.detail || '无补充信息'}</p><HealthTag status={item.status} /></article>) : <Empty className="v2-ops-link-empty" title={data ? '暂无链路探针' : '正在读取链路状态'} description={data ? '当前响应没有可展示的数据链路证据。' : '全局健康数据返回后将在这里更新。'} />}</div></Card>
|
||||
<Card className="v2-ops-panel v2-ops-runtime" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="运行时安全" description="生产模式、写入探针与代理配置" meta={<HealthTag status={!data ? 'unknown' : runtimeIssueCount + capacityIssueCount > 0 ? 'warning' : 'ok'}>{!data ? '读取中' : runtimeIssueCount + capacityIssueCount > 0 ? `${runtimeIssueCount + capacityIssueCount} 项关注` : '配置正常'}</HealthTag>} /><Descriptions className="v2-ops-runtime-descriptions" align="left" size="small" data={[
|
||||
{ key: '生产数据模式', value: <HealthTag status={data?.runtime.dataMode === 'production' ? 'ok' : 'error'}>{data?.runtime.dataMode === 'production' ? '已启用' : '未启用'}</HealthTag> },
|
||||
{ key: 'MySQL 写探针', value: <HealthTag status={data?.mysqlWritable ? 'ok' : 'error'}>{data?.mysqlWritable ? '正常' : '异常'}</HealthTag> },
|
||||
@@ -400,7 +423,7 @@ export default function OperationsPage() {
|
||||
{ key: '请求超时', value: `${data?.runtime.requestTimeoutMs ?? '—'} ms` },
|
||||
{ key: '高德安全代理', value: <HealthTag status={data?.runtime.amapSecurityProxyEnabled && !data?.runtime.amapSecurityCodeExposed ? 'ok' : 'warning'}>{data?.runtime.amapSecurityProxyEnabled ? '服务端代理' : '未启用'}</HealthTag> }
|
||||
]} />{data?.capacityFindings?.length ? <div className="v2-ops-capacity-findings">{data.capacityFindings.map((item) => <Card className="v2-ops-capacity-finding" key={item}><HealthTag status="warning">待处理</HealthTag><p>{item}</p></Card>)}</div> : <Empty className="v2-ops-capacity-empty" image={<IconTickCircle />} title="容量检查通过" description="当前没有需要处理的容量风险。" />}</Card></div>
|
||||
<Card className="v2-ops-panel v2-ops-sources" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="协议来源就绪度" description="在线覆盖、当前证据、处置动作与验收口径" meta={<Tag color="blue" type="light" size="small">{sources ? `${sources.sources.length} 个协议` : '正在读取'}</Tag>} />{sources ? sources.sources.length ? <CardGroup className="v2-ops-source-list" type="grid" spacing={0}>{sources.sources.map((source) => {
|
||||
<Card className="v2-ops-panel v2-ops-sources" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="协议来源就绪度" description="在线覆盖、当前证据、处置动作与验收口径" meta={<Tag color="blue" type="light" size="small">{sources ? `${sources.sources.length} 个协议` : '正在读取'}</Tag>} />{sources ? sortedSources.length ? <CardGroup className="v2-ops-source-list" type="grid" spacing={0}>{sortedSources.map((source) => {
|
||||
const onlineRate = sourceOnlineRate(source.online, source.total, source.onlineRate);
|
||||
return <Card className={`v2-ops-source-card is-${source.severity}`} key={source.protocol} title={<span className="v2-ops-source-title"><Tag color="blue" type="light" size="small">{source.protocol}</Tag><small>{source.role}</small></span>} headerExtraContent={<HealthTag status={source.severity}>{source.status || statusLabel(source.severity)}</HealthTag>} headerLine>
|
||||
<div className="v2-ops-source-coverage"><span><strong>在线覆盖</strong><b>{source.online.toLocaleString('zh-CN')} / {source.total.toLocaleString('zh-CN')}</b></span><Progress aria-label={`${source.protocol} 在线覆盖率`} percent={onlineRate} showInfo={false} stroke="var(--v2-blue)" /><small>{number(onlineRate)}% 在线{source.missingVehicles ? ` · ${source.missingVehicles.toLocaleString('zh-CN')} 辆待恢复` : ''}</small></div>
|
||||
|
||||
@@ -357,16 +357,11 @@ export default function ReconciliationCenter() {
|
||||
{summary.isError ? <InlineError message={summary.error.message} onRetry={() => summary.refetch()} /> : null}
|
||||
<div className="v2-reconcile-overview">
|
||||
<div className="v2-reconcile-kpis">
|
||||
<Card className="v2-reconcile-kpi-card is-priority" bodyStyle={{ padding: 0 }}><small>待复核</small><strong>{data?.pending.toLocaleString('zh-CN') ?? '—'}</strong><span>尚未形成结论</span></Card>
|
||||
<Card className="v2-reconcile-kpi-card" bodyStyle={{ padding: 0 }}><small>活跃问题</small><strong>{data?.active.toLocaleString('zh-CN') ?? '—'}</strong><span>待复核与已确认来源</span></Card>
|
||||
<Card className="v2-reconcile-kpi-card is-priority" bodyStyle={{ padding: 0 }}><small>待复核</small><strong>{data?.pending.toLocaleString('zh-CN') ?? '—'}</strong><span>活跃问题 {data?.active.toLocaleString('zh-CN') ?? '—'}</span></Card>
|
||||
<Card className={`v2-reconcile-kpi-card is-sla${data?.overSla ? ' is-overdue' : ''}`} bodyStyle={{ padding: 0 }}><small>SLA 超时</small><strong>{data?.overSla.toLocaleString('zh-CN') ?? '—'}</strong><span>{data?.overSla ? '超过 24 小时,优先复核' : '当前没有超时差异'}</span></Card>
|
||||
<Card className="v2-reconcile-kpi-card" bodyStyle={{ padding: 0 }}><small>已确认来源</small><strong>{data?.confirmed.toLocaleString('zh-CN') ?? '—'}</strong><span>保留为活跃差异</span></Card>
|
||||
<Card className="v2-reconcile-kpi-card" bodyStyle={{ padding: 0 }}><small>已自动恢复</small><strong>{data?.recovered.toLocaleString('zh-CN') ?? '—'}</strong><span>规则已不再命中</span></Card>
|
||||
</div>
|
||||
<Card className={`v2-reconcile-sla${data?.overSla ? ' is-overdue' : ''}`} bodyStyle={{ padding: 0 }}>
|
||||
<span><Tag color={data?.overSla ? 'red' : 'green'} type="light" size="small">SLA</Tag><small>超过 24 小时</small></span>
|
||||
<strong>{data?.overSla.toLocaleString('zh-CN') ?? '—'}</strong>
|
||||
<p>{data?.overSla ? '优先复核高等级差异' : '当前没有超时差异'}</p>
|
||||
</Card>
|
||||
</div>
|
||||
<div className={`v2-reconcile-layout${trendExpanded ? ' is-trend-open' : ''}`}>
|
||||
<div className="v2-reconcile-main">
|
||||
|
||||
@@ -1697,7 +1697,7 @@
|
||||
.v2-reconcile-overview {
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
grid-template-columns: minmax(0, 4fr) minmax(170px, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0;
|
||||
border-bottom: 1px solid #e3eaf2;
|
||||
background: #fff;
|
||||
@@ -1860,7 +1860,7 @@
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.v2-reconcile-overview {
|
||||
grid-template-columns: minmax(0, 4fr) minmax(156px, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.v2-assigned-vins {
|
||||
@@ -9074,7 +9074,7 @@
|
||||
|
||||
.v2-reconcile-overview {
|
||||
height: 68px;
|
||||
grid-template-columns: minmax(0, 4fr) minmax(66px, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 0;
|
||||
border-bottom: 1px solid #e3eaf2;
|
||||
background: #fff;
|
||||
@@ -10849,6 +10849,23 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.v2-ops-navigation .v2-ops-tabs .v2-segmented-tab-count {
|
||||
min-width: 21px;
|
||||
min-height: 19px;
|
||||
border: 1px solid #d5e2f2;
|
||||
background: #fff;
|
||||
padding-inline: 5px;
|
||||
color: #57708f;
|
||||
font-size: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.v2-ops-navigation .v2-ops-tabs > .semi-button.is-active .v2-segmented-tab-count {
|
||||
border-color: rgba(255, 255, 255, .28);
|
||||
background: rgba(255, 255, 255, .18);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.v2-ops-navigation-meta {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -10999,6 +11016,10 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.v2-ops-navigation .v2-ops-tabs .v2-segmented-tab-count {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.v2-ops-navigation-meta {
|
||||
display: none;
|
||||
}
|
||||
@@ -11778,6 +11799,19 @@
|
||||
color: #155fc5;
|
||||
}
|
||||
|
||||
.v2-reconcile-overview .v2-reconcile-kpi-card.is-sla {
|
||||
background: linear-gradient(145deg, #fff 0%, #fff9f8 100%);
|
||||
}
|
||||
|
||||
.v2-reconcile-overview .v2-reconcile-kpi-card.is-sla.is-overdue {
|
||||
box-shadow: inset 0 -3px #e2554f;
|
||||
}
|
||||
|
||||
.v2-reconcile-overview .v2-reconcile-kpi-card.is-sla.is-overdue small,
|
||||
.v2-reconcile-overview .v2-reconcile-kpi-card.is-sla.is-overdue strong {
|
||||
color: #c2413d;
|
||||
}
|
||||
|
||||
.v2-reconcile-result-bar {
|
||||
display: flex;
|
||||
min-height: 34px;
|
||||
|
||||
Reference in New Issue
Block a user