feat: 完善服务监控与下游重投
This commit is contained in:
@@ -3,6 +3,7 @@ import type { SecurityAlert, SecurityBlock, SecurityOverview, SecurityProtectedN
|
||||
|
||||
export const adminSecurityDetectionApi = {
|
||||
getSecurityOverview: (range = '24h') => request<SecurityOverview>(withQuery('/admin/security-detection/overview', { range })),
|
||||
getSecurityNotificationSummary: () => request<{ count: number; criticalCount: number }>('/admin/security-detection/notification-summary'),
|
||||
listSecurityAlerts: (query: Record<string, string | number | undefined> = {}) => request<{ items: SecurityAlert[]; total: number }> (withQuery('/admin/security-detection/alerts', query)),
|
||||
listSecurityRules: () => request<SecurityRule[]>('/admin/security-detection/rules'),
|
||||
updateSecurityRule: (id: string, body: Partial<SecurityRule>) => request<SecurityRule>(`/admin/security-detection/rules/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||||
|
||||
@@ -12,6 +12,18 @@ export type InfrastructureServiceStatus = {
|
||||
status: 'healthy' | 'unhealthy' | 'unknown';
|
||||
};
|
||||
|
||||
export type InfrastructureServiceMetricGroup = {
|
||||
key: string;
|
||||
name: string;
|
||||
available: boolean;
|
||||
metrics: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
value: number | null;
|
||||
unit: 'percent' | 'seconds' | 'count' | 'per_second' | 'bytes';
|
||||
}>;
|
||||
};
|
||||
|
||||
export type InfrastructureAlert = {
|
||||
fingerprint: string;
|
||||
name: string;
|
||||
@@ -61,5 +73,6 @@ export type InfrastructureMonitoringOverview = {
|
||||
networkTransmitBytesPerSecond: InfrastructureMetricPoint[];
|
||||
};
|
||||
services: InfrastructureServiceStatus[];
|
||||
serviceMetrics: InfrastructureServiceMetricGroup[];
|
||||
alerts: InfrastructureAlert[];
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { AlertTriangle, BarChart3, CheckCircle2, Eye, RefreshCw, Search, TimerReset } from 'lucide-react';
|
||||
import { adminApi, type DownstreamDeliveryDashboard, type DownstreamDeliveryRecord, type DownstreamRequeuePreview, type DownstreamRequeueTask, type DownstreamRequeueTaskItem, type EnterpriseApplication, type TenantOption } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Tag, type DateRangeValue } from '@/components/ui';
|
||||
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Tag, Textarea, type DateRangeValue } from '@/components/ui';
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
|
||||
const statusTone: Record<string, 'neutral' | 'info' | 'success' | 'warning' | 'danger'> = {
|
||||
@@ -677,7 +677,7 @@ export function AdminDownstreamDeliveriesPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="surface admin-task-table-card report-task-table-card">
|
||||
<div className="surface admin-task-table-card report-task-table-card downstream-requeue-task-card">
|
||||
<div className="section-heading downstream-requeue-task-heading">
|
||||
<div><h2>后台重投任务</h2><p className="muted">按筛选快照安全恢复,支持暂停、继续、终止和完整结果追踪。</p></div>
|
||||
<div className="downstream-requeue-task-heading__actions">
|
||||
@@ -741,8 +741,20 @@ export function AdminDownstreamDeliveriesPage() {
|
||||
</div>
|
||||
<div className="downstream-requeue-preview__distribution"><span>状态分布</span><div>{Object.entries(taskPreview.statusCounts).map(([key, value]) => <Tag key={key} tone={statusTone[key] ?? 'neutral'}>{statusLabel[key] ?? key} {value}</Tag>)}</div></div>
|
||||
<Select label="执行速度" value={String(taskRate)} onChange={(event) => setTaskRate(Number(event.target.value))} options={[{ label: '平稳(每应用10条/秒)', value: '10' }, { label: '快速(每应用20条/秒)', value: '20' }, { label: '低速(每应用5条/秒)', value: '5' }]} />
|
||||
<label className="field"><span>任务原因 *</span><textarea value={taskReason} onChange={(event) => setTaskReason(event.target.value)} placeholder="请填写事故原因、工单号或处理说明(至少5个字)" rows={3} /></label>
|
||||
<div className="downstream-requeue-warning"><AlertTriangle size={20} /><div><strong>安全边界</strong><p>仅处理待投递、失败、未确认和拒绝记录;不会批量重投客户端已确认或正在等待 ACK 的记录。客户离线时进入等待,不计失败或跳过。</p></div></div>
|
||||
<Textarea
|
||||
className="downstream-requeue-reason"
|
||||
error={taskReason.length > 0 && taskReason.trim().length < 5 ? '任务原因至少填写 5 个字' : undefined}
|
||||
hint={`请填写事故原因、工单号或处理说明 · ${taskReason.length}/200`}
|
||||
id="downstream-requeue-task-reason"
|
||||
label="任务原因"
|
||||
maxLength={200}
|
||||
onChange={(event) => setTaskReason(event.target.value)}
|
||||
placeholder="例如:工单 INC-20260814,重新投递客户已确认的历史回执"
|
||||
required
|
||||
rows={4}
|
||||
value={taskReason}
|
||||
/>
|
||||
<div className="downstream-requeue-warning"><AlertTriangle size={20} /><div><strong>重复投递风险</strong><p>任务支持待投递、失败、未确认、拒绝和客户端已确认记录;已确认记录会再次发送,可能导致客户端重复处理。正在等待 ACK 的记录仍不会并发重投,客户离线时进入等待。</p></div></div>
|
||||
</div>
|
||||
</Modal>
|
||||
) : null}
|
||||
|
||||
@@ -11,20 +11,13 @@
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.system-monitoring-title-row h1 {
|
||||
color: var(--color-text-strong);
|
||||
font-size: 24px;
|
||||
line-height: 1.25;
|
||||
margin: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.system-monitoring-title-row p {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
margin: 4px 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.system-monitoring-controls,
|
||||
@@ -191,6 +184,24 @@
|
||||
grid-template-columns: minmax(0, 1fr) 300px;
|
||||
}
|
||||
|
||||
.system-monitoring-service-metrics { padding: 18px; }
|
||||
.system-monitoring-service-metrics > header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.system-monitoring-service-metrics > header > div { align-items: center; color: var(--color-text-strong); display: flex; gap: 8px; }
|
||||
.system-monitoring-service-metrics > header > span { color: var(--color-text-muted); font-size: 12px; }
|
||||
.system-monitoring-service-metric-grid { display: grid; gap: 12px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.system-monitoring-service-metric-grid article { background: var(--color-bg-subtle); border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 14px; }
|
||||
.system-monitoring-service-metric-title { align-items: center; display: flex; justify-content: space-between; margin-bottom: 9px; }
|
||||
.system-monitoring-service-metric-title > strong { color: var(--color-text-strong); font-size: 14px; }
|
||||
.system-monitoring-service-metric-row { align-items: center; border-top: 1px solid var(--color-border); display: flex; justify-content: space-between; min-height: 34px; }
|
||||
.system-monitoring-service-metric-row span { color: var(--color-text-muted); font-size: 12px; }
|
||||
.system-monitoring-service-metric-row strong { color: var(--color-text); font-size: 13px; }
|
||||
.system-monitoring-service-metric-empty { color: var(--color-text-subtle); font-size: 12px; line-height: 1.55; padding-top: 8px; }
|
||||
|
||||
.system-monitoring-chart-stack {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
@@ -285,6 +296,7 @@
|
||||
.system-monitoring-main-grid { grid-template-columns: minmax(0, 1fr); }
|
||||
.system-monitoring-services { order: -1; }
|
||||
.system-monitoring-service-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.system-monitoring-service-metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.system-monitoring-service { border-bottom: 0; border-right: 1px solid var(--color-border); padding: 11px 12px; }
|
||||
.system-monitoring-service:nth-child(3n) { border-right: 0; }
|
||||
}
|
||||
@@ -299,7 +311,9 @@
|
||||
.system-monitoring-health__fact { border-left: 0; border-top: 1px solid var(--color-border); padding: 12px 0 0; }
|
||||
.system-monitoring-metrics,
|
||||
.system-monitoring-chart-stack,
|
||||
.system-monitoring-service-list { grid-template-columns: minmax(0, 1fr); }
|
||||
.system-monitoring-service-list,
|
||||
.system-monitoring-service-metric-grid { grid-template-columns: minmax(0, 1fr); }
|
||||
.system-monitoring-service-metrics > header { align-items: flex-start; flex-direction: column; gap: 6px; }
|
||||
.system-monitoring-service { border-bottom: 1px solid var(--color-border); border-right: 0; padding: 13px 0; }
|
||||
.system-monitoring-chart-card { padding: 14px 10px; }
|
||||
}
|
||||
|
||||
@@ -69,6 +69,15 @@ function formatUptime(value: number | null) {
|
||||
return days > 0 ? `${days}天 ${hours}小时` : `${hours}小时`;
|
||||
}
|
||||
|
||||
function formatServiceMetric(value: number | null, unit: 'percent' | 'seconds' | 'count' | 'per_second' | 'bytes') {
|
||||
if (value === null) return '—';
|
||||
if (unit === 'percent') return `${value.toFixed(2)}%`;
|
||||
if (unit === 'seconds') return value < 1 ? `${Math.round(value * 1000)} ms` : `${value.toFixed(1)} s`;
|
||||
if (unit === 'per_second') return `${value.toFixed(value < 10 ? 2 : 1)}/s`;
|
||||
if (unit === 'bytes') return formatBytes(value);
|
||||
return Math.round(value).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function formatTime(value: string | null) {
|
||||
if (!value) return '暂无采样';
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
@@ -223,10 +232,7 @@ export function AdminSystemMonitoringPage() {
|
||||
<div>
|
||||
<Breadcrumb items={['系统管理', '系统监控']} />
|
||||
<div className="system-monitoring-title-row">
|
||||
<div>
|
||||
<h1>系统监控</h1>
|
||||
<p>服务器资源、核心服务与活动告警</p>
|
||||
</div>
|
||||
<p>服务器资源、核心服务与活动告警</p>
|
||||
<Tag tone={status.tone}>{status.label}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
@@ -277,6 +283,29 @@ export function AdminSystemMonitoringPage() {
|
||||
<article className="surface system-monitoring-metric"><div className="system-monitoring-metric__icon is-green"><Network size={19} /></div><div><span>网络吞吐</span><strong>{formatRate(totalNetworkRate(metrics?.networkReceiveBytesPerSecond, metrics?.networkTransmitBytesPerSecond))}</strong><small>接收 {formatRate(metrics?.networkReceiveBytesPerSecond ?? null)} · 发送 {formatRate(metrics?.networkTransmitBytesPerSecond ?? null)}</small></div></article>
|
||||
</div>
|
||||
|
||||
<section className="surface system-monitoring-service-metrics">
|
||||
<header>
|
||||
<div><Database size={18} /><strong>服务关键指标</strong></div>
|
||||
<span>固定低基数聚合,不含手机号、短信ID或SQL文本</span>
|
||||
</header>
|
||||
<div className="system-monitoring-service-metric-grid">
|
||||
{(overview?.serviceMetrics ?? []).map((group) => (
|
||||
<article key={group.key}>
|
||||
<div className="system-monitoring-service-metric-title">
|
||||
<strong>{group.name}</strong>
|
||||
<Tag tone={group.available ? 'success' : 'neutral'}>{group.available ? '已采集' : '待采集'}</Tag>
|
||||
</div>
|
||||
{group.metrics.length ? group.metrics.map((metric) => (
|
||||
<div className="system-monitoring-service-metric-row" key={metric.key}>
|
||||
<span>{metric.label}</span>
|
||||
<strong>{formatServiceMetric(metric.value, metric.unit)}</strong>
|
||||
</div>
|
||||
)) : <div className="system-monitoring-service-metric-empty">已监控服务可用性,待原生容量指标接入</div>}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="system-monitoring-main-grid">
|
||||
<div className="system-monitoring-chart-stack">
|
||||
<article className="surface system-monitoring-chart-card"><header><div><Cpu size={17} /><strong>CPU 趋势</strong></div><span>{formatPercent(metrics?.cpuUsagePercent ?? null)}</span></header>{overview?.trends.cpuUsagePercent.length ? <Chart height={230} option={cpuOption} /> : <EmptyChart />}</article>
|
||||
|
||||
@@ -48,6 +48,7 @@ export function AdminLayout() {
|
||||
function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
|
||||
const [pendingAudits, setPendingAudits] = useState(EMPTY_PENDING_AUDITS);
|
||||
const [retirementUnreadCount, setRetirementUnreadCount] = useState(0);
|
||||
const [securityAlertSummary, setSecurityAlertSummary] = useState({ count: 0, criticalCount: 0 });
|
||||
const [sessionLocked, setSessionLocked] = useState(Boolean(session.locked));
|
||||
const loadPendingAuditCount = useCallback(() => {
|
||||
const currentSession = readSession('admin');
|
||||
@@ -56,14 +57,16 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
|
||||
return;
|
||||
}
|
||||
// This runs globally and on a timer, so it must not fan out through the full dashboard aggregation.
|
||||
Promise.allSettled([adminApi.getPendingAudits(), adminApi.getSignatureRetirementUnreadCount()])
|
||||
.then(([audits, retirement]) => {
|
||||
Promise.allSettled([adminApi.getPendingAudits(), adminApi.getSignatureRetirementUnreadCount(), adminApi.getSecurityNotificationSummary()])
|
||||
.then(([audits, retirement, security]) => {
|
||||
setPendingAudits(audits.status === 'fulfilled' ? audits.value : EMPTY_PENDING_AUDITS);
|
||||
setRetirementUnreadCount(retirement.status === 'fulfilled' ? retirement.value.count : 0);
|
||||
setSecurityAlertSummary(security.status === 'fulfilled' ? security.value : { count: 0, criticalCount: 0 });
|
||||
})
|
||||
.catch(() => {
|
||||
setPendingAudits(EMPTY_PENDING_AUDITS);
|
||||
setRetirementUnreadCount(0);
|
||||
setSecurityAlertSummary({ count: 0, criticalCount: 0 });
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -78,11 +81,13 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
|
||||
window.addEventListener('focus', onFocus);
|
||||
window.addEventListener('cmpp-audit-count-refresh', onAuditRefresh);
|
||||
window.addEventListener('cmpp-retirement-count-refresh', onAuditRefresh);
|
||||
window.addEventListener('cmpp-security-alert-count-refresh', onAuditRefresh);
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
window.removeEventListener('focus', onFocus);
|
||||
window.removeEventListener('cmpp-audit-count-refresh', onAuditRefresh);
|
||||
window.removeEventListener('cmpp-retirement-count-refresh', onAuditRefresh);
|
||||
window.removeEventListener('cmpp-security-alert-count-refresh', onAuditRefresh);
|
||||
};
|
||||
}, [loadPendingAuditCount, session.portal, sessionLocked]);
|
||||
|
||||
@@ -96,7 +101,10 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
|
||||
userName={session.user.displayName}
|
||||
userRole="平台管理员"
|
||||
onSessionLockedChange={setSessionLocked}
|
||||
retirementAlert={{ count: retirementUnreadCount, to: '/admin/signature-retirement' }}
|
||||
alertNotifications={[
|
||||
{ label: '签名清退预警', count: retirementUnreadCount, description: '今日未读且未抑制', to: '/admin/signature-retirement' },
|
||||
{ label: '安全检测与封禁', count: securityAlertSummary.count, description: securityAlertSummary.criticalCount > 0 ? `${securityAlertSummary.criticalCount} 条严重告警待处置` : '待处置安全告警', to: '/admin/security-detection' },
|
||||
]}
|
||||
auditNotifications={[
|
||||
{ label: '企业认证待审', count: pendingAudits.enterpriseCertifications, to: '/admin/enterprise-audit' },
|
||||
{ label: '短信审核待审', count: pendingAudits.smsAudits, to: '/admin/sms-audit' },
|
||||
|
||||
+40
-13
@@ -50,6 +50,10 @@ export type AuditNotificationItem = {
|
||||
to: string;
|
||||
};
|
||||
|
||||
export type AlertNotificationItem = AuditNotificationItem & {
|
||||
description: string;
|
||||
};
|
||||
|
||||
type AppShellProps = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
@@ -60,7 +64,7 @@ type AppShellProps = {
|
||||
userRole: string;
|
||||
navSections: ShellNavSection[];
|
||||
auditNotifications?: AuditNotificationItem[];
|
||||
retirementAlert?: { count: number; to: string };
|
||||
alertNotifications?: AlertNotificationItem[];
|
||||
onSessionLockedChange?: (locked: boolean) => void;
|
||||
};
|
||||
|
||||
@@ -73,7 +77,7 @@ export function AppShell({
|
||||
userRole,
|
||||
navSections,
|
||||
auditNotifications = [],
|
||||
retirementAlert,
|
||||
alertNotifications = [],
|
||||
onSessionLockedChange,
|
||||
}: AppShellProps) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
@@ -81,6 +85,7 @@ export function AppShell({
|
||||
const [closedSections, setClosedSections] = useState<Record<string, boolean>>({});
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const [noticeOpen, setNoticeOpen] = useState(false);
|
||||
const [alertNoticeOpen, setAlertNoticeOpen] = useState(false);
|
||||
const [passwordModalOpen, setPasswordModalOpen] = useState(false);
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
@@ -107,6 +112,10 @@ export function AppShell({
|
||||
() => auditNotifications.reduce((sum, item) => sum + item.count, 0),
|
||||
[auditNotifications],
|
||||
);
|
||||
const alertTotal = useMemo(
|
||||
() => alertNotifications.reduce((sum, item) => sum + item.count, 0),
|
||||
[alertNotifications],
|
||||
);
|
||||
|
||||
async function changeOwnPassword() {
|
||||
if (!currentPassword || newPassword.length < 6) {
|
||||
@@ -417,23 +426,41 @@ export function AppShell({
|
||||
<button className="icon-button topbar-help" type="button" aria-label="帮助中心">
|
||||
<CircleHelp size={18} />
|
||||
</button>
|
||||
{retirementAlert ? (
|
||||
<Link
|
||||
aria-label={`今日未读且未抑制签名清退预警 ${retirementAlert.count} 条`}
|
||||
className={['icon-button', retirementAlert.count > 0 ? 'has-dot' : ''].filter(Boolean).join(' ')}
|
||||
title="今日未读且未抑制签名清退预警"
|
||||
to={retirementAlert.to}
|
||||
>
|
||||
<Bell size={18} />
|
||||
{retirementAlert.count > 0 ? <span className="notice-count">{retirementAlert.count}</span> : null}
|
||||
</Link>
|
||||
{alertNotifications.length ? (
|
||||
<div className="notice-menu-wrap">
|
||||
<button
|
||||
aria-expanded={alertNoticeOpen}
|
||||
aria-haspopup="menu"
|
||||
aria-label="预警通知"
|
||||
className={['icon-button', alertTotal > 0 ? 'has-dot' : ''].filter(Boolean).join(' ')}
|
||||
onClick={() => { setAlertNoticeOpen((open) => !open); setNoticeOpen(false); }}
|
||||
type="button"
|
||||
>
|
||||
<Bell size={18} />
|
||||
{alertTotal > 0 ? <span className="notice-count">{alertTotal}</span> : null}
|
||||
</button>
|
||||
{alertNoticeOpen ? (
|
||||
<div className="notice-popover notice-popover--alerts" role="menu">
|
||||
<div className="notice-popover__header">
|
||||
<strong>预警中心</strong>
|
||||
<span className={alertTotal === 0 ? 'is-zero' : ''}>{alertTotal} 条</span>
|
||||
</div>
|
||||
{alertNotifications.map((item) => (
|
||||
<NavLink key={item.to} onClick={() => setAlertNoticeOpen(false)} role="menuitem" to={item.to}>
|
||||
<span className="notice-popover__copy"><b>{item.label}</b><small>{item.description}</small></span>
|
||||
<strong className={item.count === 0 ? 'is-zero' : ''}>{item.count}</strong>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="notice-menu-wrap">
|
||||
<button
|
||||
aria-expanded={noticeOpen}
|
||||
aria-haspopup="menu"
|
||||
className={['icon-button', auditTotal > 0 ? 'has-dot' : ''].filter(Boolean).join(' ')}
|
||||
onClick={() => setNoticeOpen((open) => !open)}
|
||||
onClick={() => { setNoticeOpen((open) => !open); setAlertNoticeOpen(false); }}
|
||||
type="button"
|
||||
aria-label="通知"
|
||||
>
|
||||
|
||||
@@ -9424,10 +9424,14 @@
|
||||
.downstream-page-size { display: inline-flex; align-items: center; gap: 8px; color: var(--text-muted); font-size: 13px; }
|
||||
.downstream-page-size select { min-width: 76px; height: 36px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); color: var(--text); padding: 0 10px; }
|
||||
.downstream-requeue-task-heading { gap: 20px; }
|
||||
.downstream-requeue-task-card > .downstream-requeue-task-heading { padding: var(--space-6) var(--space-6) var(--space-5); }
|
||||
.downstream-requeue-task-heading__actions { display: flex; align-items: center; gap: 8px; }
|
||||
.downstream-requeue-task-heading__actions .ui-field { min-width: 150px; }
|
||||
.downstream-requeue-task-list { display: grid; border-top: 1px solid var(--border); }
|
||||
.downstream-requeue-task-list article { display: grid; grid-template-columns: minmax(190px, .9fr) minmax(260px, 1.35fr) minmax(250px, 1fr) auto; gap: 22px; align-items: center; padding: 18px 4px; border-bottom: 1px solid var(--border); }
|
||||
.downstream-requeue-task-list { display: grid; margin: 0 var(--space-6); overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--color-surface); }
|
||||
.downstream-requeue-task-list article { display: grid; grid-template-columns: minmax(190px, .9fr) minmax(260px, 1.35fr) minmax(250px, 1fr) auto; gap: 22px; align-items: center; padding: 18px var(--space-5); border-bottom: 1px solid var(--border); }
|
||||
.downstream-requeue-task-list article:last-of-type { border-bottom: 0; }
|
||||
.downstream-requeue-task-list > .muted { margin: 0; padding: var(--space-6); text-align: center; }
|
||||
.downstream-requeue-task-card > .ui-pagination { padding: var(--space-5) var(--space-6) var(--space-6); }
|
||||
.downstream-requeue-task-list article > div { min-width: 0; }
|
||||
.downstream-requeue-task-list__identity, .downstream-requeue-task-list__scope, .downstream-requeue-task-list__progress { display: grid; gap: 5px; }
|
||||
.downstream-requeue-task-list__identity > strong { color: var(--text-strong); font-size: 14px; font-variant-numeric: tabular-nums; }
|
||||
@@ -9447,6 +9451,8 @@
|
||||
.downstream-requeue-preview__distribution { display: grid; gap: 8px; }
|
||||
.downstream-requeue-preview__distribution > span { color: var(--text-muted); font-size: 13px; }
|
||||
.downstream-requeue-preview__distribution > div { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.downstream-requeue-reason { padding: 16px; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface-subtle, #f8fafc); }
|
||||
.downstream-requeue-reason .ui-textarea { min-height: 124px; background: var(--color-surface); line-height: 1.65; }
|
||||
.downstream-requeue-warning { display: flex; align-items: flex-start; gap: 10px; padding: 14px; border: 1px solid #fed7aa; border-radius: 10px; background: var(--warning-soft, #fff7ed); color: var(--warning-text, #9a3412); }
|
||||
.downstream-requeue-warning > div { display: grid; gap: 4px; }
|
||||
.downstream-requeue-warning p { margin: 0; color: inherit; line-height: 1.6; }
|
||||
@@ -9460,4 +9466,4 @@
|
||||
.downstream-requeue-detail-items__head { background: var(--surface-subtle, #f8fafc); color: var(--text-muted); font-size: 12px; font-weight: 600; }
|
||||
.downstream-requeue-detail-items time { color: var(--text-muted); font-size: 12px; }
|
||||
@media (max-width: 1100px) { .downstream-requeue-task-list article { grid-template-columns: 1fr 1.4fr; } .downstream-requeue-task-list__actions { justify-content: flex-start; } .downstream-requeue-preview__summary { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 780px) { .downstream-requeue-task-heading, .downstream-requeue-task-heading__actions { align-items: stretch; flex-direction: column; } .downstream-requeue-task-list article, .downstream-requeue-preview__summary, .downstream-requeue-detail-filter { grid-template-columns: 1fr; } .downstream-requeue-detail-items { border: 0; overflow: visible; gap: 10px; } .downstream-requeue-detail-items__head { display: none !important; } .downstream-requeue-detail-items > div { grid-template-columns: 1fr; gap: 6px; padding: 14px; border: 1px solid var(--border); border-radius: 10px; } }
|
||||
@media (max-width: 780px) { .downstream-requeue-task-card > .downstream-requeue-task-heading { padding: var(--space-5) var(--space-4) var(--space-4); } .downstream-requeue-task-heading, .downstream-requeue-task-heading__actions { align-items: stretch; flex-direction: column; } .downstream-requeue-task-list { margin: 0 var(--space-4); } .downstream-requeue-task-list article, .downstream-requeue-preview__summary, .downstream-requeue-detail-filter { grid-template-columns: 1fr; } .downstream-requeue-task-card > .ui-pagination { padding: var(--space-4); } .downstream-requeue-detail-items { border: 0; overflow: visible; gap: 10px; } .downstream-requeue-detail-items__head { display: none !important; } .downstream-requeue-detail-items > div { grid-template-columns: 1fr; gap: 6px; padding: 14px; border: 1px solid var(--border); border-radius: 10px; } }
|
||||
|
||||
@@ -571,6 +571,31 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.notice-popover--alerts {
|
||||
min-width: 292px;
|
||||
}
|
||||
|
||||
.notice-popover--alerts a {
|
||||
gap: var(--space-4);
|
||||
min-height: 58px;
|
||||
}
|
||||
|
||||
.notice-popover__copy {
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.notice-popover__copy b {
|
||||
color: var(--color-text-strong);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.notice-popover__copy small {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.page-heading__actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user