refactor(ui): converge semi operations workspace
This commit is contained in:
@@ -605,6 +605,48 @@ test('sourceReadiness reads ops source readiness plan', async () => {
|
|||||||
expect(result.sources[0].protocol).toBe('GB32960');
|
expect(result.sources[0].protocol).toBe('GB32960');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('normalizes nullable operations evidence arrays at the API boundary', async () => {
|
||||||
|
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
linkHealth: null,
|
||||||
|
kafkaLag: 0,
|
||||||
|
activeConnections: 10,
|
||||||
|
capacityFindings: null,
|
||||||
|
redisOnlineKeys: 5,
|
||||||
|
tdengineWritable: true,
|
||||||
|
mysqlWritable: true,
|
||||||
|
runtime: { platformRelease: 'test', dataMode: 'production', requestTimeoutMs: 5000 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} as Response)
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
data: {
|
||||||
|
totalVehicles: 1024,
|
||||||
|
boundVehicles: 1024,
|
||||||
|
identityRequiredVehicles: 0,
|
||||||
|
onlineVehicles: 234,
|
||||||
|
kafkaLag: 0,
|
||||||
|
activeConnections: 10,
|
||||||
|
redisOnlineKeys: 5,
|
||||||
|
platformRelease: 'test',
|
||||||
|
sources: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} as Response);
|
||||||
|
|
||||||
|
const [health, readiness] = await Promise.all([api.opsHealth(), api.sourceReadiness()]);
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(health.linkHealth).toEqual([]);
|
||||||
|
expect(health.capacityFindings).toEqual([]);
|
||||||
|
expect(readiness.sources).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test('api errors include backend message, detail, and trace id', async () => {
|
test('api errors include backend message, detail, and trace id', async () => {
|
||||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|||||||
@@ -179,6 +179,21 @@ function withSignal(init: RequestInit | undefined, signal?: AbortSignal) {
|
|||||||
return signal ? { ...init, signal } : init;
|
return signal ? { ...init, signal } : init;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeOpsHealth(value: OpsHealth): OpsHealth {
|
||||||
|
return {
|
||||||
|
...value,
|
||||||
|
linkHealth: Array.isArray(value.linkHealth) ? value.linkHealth : [],
|
||||||
|
capacityFindings: Array.isArray(value.capacityFindings) ? value.capacityFindings : []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSourceReadiness(value: SourceReadinessPlan): SourceReadinessPlan {
|
||||||
|
return {
|
||||||
|
...value,
|
||||||
|
sources: Array.isArray(value.sources) ? value.sources : []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function responseErrorMessage(response: Response) {
|
async function responseErrorMessage(response: Response) {
|
||||||
try {
|
try {
|
||||||
const envelope = (await response.json()) as ApiErrorEnvelope;
|
const envelope = (await response.json()) as ApiErrorEnvelope;
|
||||||
@@ -347,6 +362,6 @@ export const api = {
|
|||||||
alertEvents: (params = new URLSearchParams()) => request<Page<QualityIssueRow>>(`/api/alert-events?${params.toString()}`),
|
alertEvents: (params = new URLSearchParams()) => request<Page<QualityIssueRow>>(`/api/alert-events?${params.toString()}`),
|
||||||
alertEventNotificationPlan: (params = new URLSearchParams()) => request<QualityNotificationPlan>(`/api/alert-events/notification-plan?${params.toString()}`),
|
alertEventNotificationPlan: (params = new URLSearchParams()) => request<QualityNotificationPlan>(`/api/alert-events/notification-plan?${params.toString()}`),
|
||||||
reverseGeocode: (params = new URLSearchParams(), signal?: AbortSignal) => request<MapReverseGeocode>(`/api/map/reverse-geocode?${params.toString()}`, withSignal(undefined, signal)),
|
reverseGeocode: (params = new URLSearchParams(), signal?: AbortSignal) => request<MapReverseGeocode>(`/api/map/reverse-geocode?${params.toString()}`, withSignal(undefined, signal)),
|
||||||
opsHealth: (signal?: AbortSignal) => request<OpsHealth>('/api/ops/health', withSignal(undefined, signal)),
|
opsHealth: (signal?: AbortSignal) => request<OpsHealth>('/api/ops/health', withSignal(undefined, signal)).then(normalizeOpsHealth),
|
||||||
sourceReadiness: (signal?: AbortSignal) => request<SourceReadinessPlan>('/api/ops/source-readiness', withSignal(undefined, signal))
|
sourceReadiness: (signal?: AbortSignal) => request<SourceReadinessPlan>('/api/ops/source-readiness', withSignal(undefined, signal)).then(normalizeSourceReadiness)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -253,6 +253,27 @@ test('reconciles service identities with bound and identity-required vehicles',
|
|||||||
expect(screen.queryByText('统一车辆视角')).not.toBeInTheDocument();
|
expect(screen.queryByText('统一车辆视角')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('normalizes nullable health evidence arrays without crashing the workspace', async () => {
|
||||||
|
seedSession();
|
||||||
|
mocks.opsHealth.mockResolvedValue({
|
||||||
|
linkHealth: null, kafkaLag: 0, activeConnections: 10, capacityFindings: null, redisOnlineKeys: 5,
|
||||||
|
tdengineWritable: true, mysqlWritable: true,
|
||||||
|
runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
|
||||||
|
});
|
||||||
|
mocks.sourceReadiness.mockResolvedValue({
|
||||||
|
totalVehicles: 1024, boundVehicles: 1024, identityRequiredVehicles: 0, onlineVehicles: 234,
|
||||||
|
kafkaLag: 0, activeConnections: 10, redisOnlineKeys: 5, platformRelease: 'test-release', sources: null
|
||||||
|
});
|
||||||
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
|
renderOperations(client, ['/operations?view=health']);
|
||||||
|
|
||||||
|
expect(await screen.findByRole('heading', { name: '运行概览', level: 5 })).toBeInTheDocument();
|
||||||
|
expect(await screen.findAllByText('暂无协议来源')).toHaveLength(2);
|
||||||
|
expect(screen.getByText('暂无链路探针')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('容量检查通过')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('当前模块暂时无法显示')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
test('includes protocol readiness in the global health verdict and prioritizes its evidence', async () => {
|
test('includes protocol readiness in the global health verdict and prioritizes its evidence', async () => {
|
||||||
seedSession();
|
seedSession();
|
||||||
mocks.opsHealth.mockResolvedValue({
|
mocks.opsHealth.mockResolvedValue({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { FormEvent, useDeferredValue, useEffect, useMemo, useState } from 'react';
|
import { FormEvent, useDeferredValue, useEffect, useMemo, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { api } from '../../api/client';
|
import { api } from '../../api/client';
|
||||||
import type { VehicleCoverageRow, VehicleLocationSourceEvidence, VehicleSourceDiagnostic } from '../../api/types';
|
import type { LinkHealth, SourceReadinessRow, VehicleCoverageRow, VehicleLocationSourceEvidence, VehicleSourceDiagnostic } from '../../api/types';
|
||||||
import { InlineError } from '../shared/AsyncState';
|
import { InlineError } from '../shared/AsyncState';
|
||||||
import { MobileFilterToggle } from '../shared/MobileFilterToggle';
|
import { MobileFilterToggle } from '../shared/MobileFilterToggle';
|
||||||
import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet';
|
import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet';
|
||||||
@@ -22,6 +22,9 @@ import ReconciliationCenter from './ReconciliationCenter';
|
|||||||
|
|
||||||
type OperationsWorkspace = 'reconciliation' | 'diagnostic' | 'health';
|
type OperationsWorkspace = 'reconciliation' | 'diagnostic' | 'health';
|
||||||
const operationsWorkspaces = new Set<OperationsWorkspace>(['reconciliation', 'diagnostic', 'health']);
|
const operationsWorkspaces = new Set<OperationsWorkspace>(['reconciliation', 'diagnostic', 'health']);
|
||||||
|
const emptyLinkHealthRows: LinkHealth[] = [];
|
||||||
|
const emptyCapacityFindings: string[] = [];
|
||||||
|
const emptySourceReadinessRows: SourceReadinessRow[] = [];
|
||||||
|
|
||||||
function statusLabel(status: string) {
|
function statusLabel(status: string) {
|
||||||
return { ok: '正常', warning: '关注', error: '异常' }[status] ?? status;
|
return { ok: '正常', warning: '关注', error: '异常' }[status] ?? status;
|
||||||
@@ -443,14 +446,17 @@ export default function OperationsPage() {
|
|||||||
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 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 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 data = health.data; const sources = readiness.data;
|
||||||
|
const linkHealthRows = data?.linkHealth ?? emptyLinkHealthRows;
|
||||||
|
const capacityFindings = data?.capacityFindings ?? emptyCapacityFindings;
|
||||||
|
const sourceRows = sources?.sources ?? emptySourceReadinessRows;
|
||||||
const refresh = () => Promise.all([health.refetch(), readiness.refetch(), reconciliation.refetch()]);
|
const refresh = () => Promise.all([health.refetch(), readiness.refetch(), reconciliation.refetch()]);
|
||||||
const linkIssueCount = data?.linkHealth.filter((item) => item.status !== 'ok').length ?? 0;
|
const linkIssueCount = linkHealthRows.filter((item) => item.status !== 'ok').length;
|
||||||
const runtimeIssueCount = data ? Number(!data.mysqlWritable) + Number(!data.tdengineWritable) + Number(data.runtime.dataMode !== 'production') : 0;
|
const runtimeIssueCount = data ? Number(!data.mysqlWritable) + Number(!data.tdengineWritable) + Number(data.runtime.dataMode !== 'production') : 0;
|
||||||
const capacityIssueCount = data?.capacityFindings.length ?? 0;
|
const capacityIssueCount = capacityFindings.length;
|
||||||
const infrastructureIssueCount = linkIssueCount + runtimeIssueCount + capacityIssueCount + Number((data?.kafkaLag ?? 0) > 0);
|
const infrastructureIssueCount = linkIssueCount + runtimeIssueCount + capacityIssueCount + Number((data?.kafkaLag ?? 0) > 0);
|
||||||
const sourceIssueCount = sources?.sources.filter((item) => item.severity !== 'ok').length ?? 0;
|
const sourceIssueCount = sourceRows.filter((item) => item.severity !== 'ok').length;
|
||||||
const sourceErrorCount = sources?.sources.filter((item) => item.severity === 'error').length ?? 0;
|
const sourceErrorCount = sourceRows.filter((item) => item.severity === 'error').length;
|
||||||
const sourceHealthyCount = sources?.sources.filter((item) => item.severity === 'ok').length ?? 0;
|
const sourceHealthyCount = sourceRows.filter((item) => item.severity === 'ok').length;
|
||||||
const readinessUnavailable = readiness.isError;
|
const readinessUnavailable = readiness.isError;
|
||||||
const healthIssueCount = infrastructureIssueCount + sourceIssueCount + Number(readinessUnavailable);
|
const healthIssueCount = infrastructureIssueCount + sourceIssueCount + Number(readinessUnavailable);
|
||||||
const healthEvidencePending = !data || readiness.isPending;
|
const healthEvidencePending = !data || readiness.isPending;
|
||||||
@@ -485,9 +491,9 @@ export default function OperationsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '协议就绪 / 总数',
|
label: '协议就绪 / 总数',
|
||||||
value: sources?.sources.length ? `${sourceHealthyCount} / ${sources.sources.length}` : readinessUnavailable ? '不可用' : '—',
|
value: sourceRows.length ? `${sourceHealthyCount} / ${sourceRows.length}` : readinessUnavailable ? '不可用' : '—',
|
||||||
note: readinessUnavailable ? '来源证据读取失败' : sourceIssueCount > 0 ? `${sourceIssueCount} 个协议需要处置` : sources?.sources.length ? '全部协议达到当前口径' : '等待协议覆盖证据',
|
note: readinessUnavailable ? '来源证据读取失败' : sourceIssueCount > 0 ? `${sourceIssueCount} 个协议需要处置` : sourceRows.length ? '全部协议达到当前口径' : '等待协议覆盖证据',
|
||||||
tone: readinessUnavailable || sourceErrorCount > 0 ? 'danger' : sourceIssueCount > 0 ? 'warning' : sources?.sources.length ? 'success' : 'neutral',
|
tone: readinessUnavailable || sourceErrorCount > 0 ? 'danger' : sourceIssueCount > 0 ? 'warning' : sourceRows.length ? 'success' : 'neutral',
|
||||||
emphasis: 'primary'
|
emphasis: 'primary'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -503,14 +509,14 @@ export default function OperationsPage() {
|
|||||||
emphasis: 'secondary'
|
emphasis: 'secondary'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
const sortedLinks = useMemo(() => [...(data?.linkHealth ?? [])].sort((left, right) => {
|
const sortedLinks = useMemo(() => [...linkHealthRows].sort((left, right) => {
|
||||||
const priority = { error: 0, warning: 1, ok: 2 } as Record<string, number>;
|
const priority = { error: 0, warning: 1, ok: 2 } as Record<string, number>;
|
||||||
return (priority[left.status] ?? 3) - (priority[right.status] ?? 3);
|
return (priority[left.status] ?? 3) - (priority[right.status] ?? 3);
|
||||||
}), [data?.linkHealth]);
|
}), [linkHealthRows]);
|
||||||
const sortedSources = useMemo(() => [...(sources?.sources ?? [])].sort((left, right) => {
|
const sortedSources = useMemo(() => [...sourceRows].sort((left, right) => {
|
||||||
const priority = { error: 0, warning: 1, ok: 2 } as Record<string, number>;
|
const priority = { error: 0, warning: 1, ok: 2 } as Record<string, number>;
|
||||||
return (priority[left.severity] ?? 3) - (priority[right.severity] ?? 3);
|
return (priority[left.severity] ?? 3) - (priority[right.severity] ?? 3);
|
||||||
}), [sources?.sources]);
|
}), [sourceRows]);
|
||||||
const workspaceContext = workspace === 'reconciliation'
|
const workspaceContext = workspace === 'reconciliation'
|
||||||
? {
|
? {
|
||||||
description: '每日自动对账',
|
description: '每日自动对账',
|
||||||
@@ -577,7 +583,7 @@ export default function OperationsPage() {
|
|||||||
</span>}
|
</span>}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className={`v2-ops-panel v2-ops-sources${sourceIssueCount || readinessUnavailable ? ' has-attention' : ''}`} bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="协议来源就绪度" description="优先展示覆盖不足的协议,并给出处置动作与验收口径" meta={<HealthTag status={readinessUnavailable ? 'error' : sourceIssueCount > 0 ? 'warning' : sources ? 'ok' : 'unknown'}>{readinessUnavailable ? '证据不可用' : sources ? sourceIssueCount > 0 ? `${sourceIssueCount} 个协议需关注` : `${sources.sources.length} 个协议正常` : '正在读取'}</HealthTag>} />{sources ? sortedSources.length ? <CardGroup className="v2-ops-source-list" type="grid" spacing={0}>{sortedSources.map((source) => {
|
<Card className={`v2-ops-panel v2-ops-sources${sourceIssueCount || readinessUnavailable ? ' has-attention' : ''}`} bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="协议来源就绪度" description="优先展示覆盖不足的协议,并给出处置动作与验收口径" meta={<HealthTag status={readinessUnavailable ? 'error' : sourceIssueCount > 0 ? 'warning' : sourceRows.length ? 'ok' : 'unknown'}>{readinessUnavailable ? '证据不可用' : sources ? sourceIssueCount > 0 ? `${sourceIssueCount} 个协议需关注` : sourceRows.length ? `${sourceRows.length} 个协议正常` : '暂无协议来源' : '正在读取'}</HealthTag>} />{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);
|
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>
|
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>
|
<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>
|
||||||
@@ -594,7 +600,7 @@ export default function OperationsPage() {
|
|||||||
{ key: 'TDengine 写探针', value: <HealthTag status={data?.tdengineWritable ? 'ok' : 'error'}>{data?.tdengineWritable ? '正常' : '异常'}</HealthTag> },
|
{ key: 'TDengine 写探针', value: <HealthTag status={data?.tdengineWritable ? 'ok' : 'error'}>{data?.tdengineWritable ? '正常' : '异常'}</HealthTag> },
|
||||||
{ key: '请求超时', value: `${data?.runtime.requestTimeoutMs ?? '—'} ms` },
|
{ key: '请求超时', value: `${data?.runtime.requestTimeoutMs ?? '—'} ms` },
|
||||||
{ key: '高德安全代理', value: <HealthTag status={data?.runtime.amapSecurityProxyEnabled && !data?.runtime.amapSecurityCodeExposed ? 'ok' : 'warning'}>{data?.runtime.amapSecurityProxyEnabled ? '服务端代理' : '未启用'}</HealthTag> }
|
{ 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>
|
]} />{capacityFindings.length ? <div className="v2-ops-capacity-findings">{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>
|
||||||
</> : null}
|
</> : null}
|
||||||
</section>
|
</section>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -25149,3 +25149,100 @@
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Semi UI operations health summary.
|
||||||
|
* Health facts must remain legible on a phone: the shared compact rail hides
|
||||||
|
* secondary metrics and its context by default, which removed vehicle coverage
|
||||||
|
* and compressed the two primary metrics into narrow slivers.
|
||||||
|
*/
|
||||||
|
.v2-ops-overview.semi-card,
|
||||||
|
.v2-ops-panel.semi-card {
|
||||||
|
border-color: #dce5ef;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 5px 18px rgba(31, 53, 80, .04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-list > span {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 680px) {
|
||||||
|
.v2-ops-health-metric-rail.is-queue.has-context > .semi-card-body {
|
||||||
|
min-height: 0;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-list {
|
||||||
|
width: 100%;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-list > span,
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-list > span.is-secondary {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 68px;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
grid-template-rows: 14px 24px 14px;
|
||||||
|
align-content: center;
|
||||||
|
gap: 0;
|
||||||
|
padding: 8px 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-list > span:nth-child(odd) {
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-list > span:nth-child(n + 3) {
|
||||||
|
border-top: 1px solid #e5ebf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-list > span > :is(small, strong, em) {
|
||||||
|
min-width: 0;
|
||||||
|
grid-column: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-list > span > small {
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-list > span > strong,
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-list > span.is-secondary > strong {
|
||||||
|
grid-row: 2;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-list > span > em {
|
||||||
|
display: block;
|
||||||
|
grid-row: 3;
|
||||||
|
font-size: 8px;
|
||||||
|
line-height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-context {
|
||||||
|
display: flex;
|
||||||
|
min-height: 46px;
|
||||||
|
border-top: 1px solid #e5ebf2;
|
||||||
|
border-left: 0;
|
||||||
|
padding: 7px 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-context > span {
|
||||||
|
width: 100%;
|
||||||
|
grid-template-columns: auto auto minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-ops-health-metric-rail .v2-workspace-metric-context em {
|
||||||
|
min-width: 0;
|
||||||
|
grid-column: 3;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user