release: bump version to 0.27.5

This commit is contained in:
linkong
2026-04-14 17:34:04 +08:00
parent 40e51d5b20
commit d9adaf4134
20 changed files with 1364 additions and 202 deletions

View File

@@ -265,6 +265,7 @@ function Scrollbar({
{scrollbar.y.visible ? (
<div
className="scrollbar__thumb scrollbar__thumb--y"
tabIndex={0}
style={{
height: `${scrollbar.y.thumbSize}px`,
transform: `translateY(${scrollbar.y.thumbOffset}px)`,
@@ -284,6 +285,7 @@ function Scrollbar({
{scrollbar.x.visible ? (
<div
className="scrollbar__thumb scrollbar__thumb--x"
tabIndex={0}
style={{
width: `${scrollbar.x.thumbSize}px`,
transform: `translateX(${scrollbar.x.thumbOffset}px)`,

View File

@@ -0,0 +1,37 @@
import { forwardRef, useImperativeHandle, useRef, type CSSProperties, type ReactNode } from 'react'
import ScrollbarOverlay from './ScrollbarOverlay'
interface TableScrollRegionProps {
children: ReactNode
className?: string
style?: CSSProperties
targetSelector?: string
}
const TableScrollRegion = forwardRef<HTMLDivElement, TableScrollRegionProps>(function TableScrollRegion(
{
children,
className = '',
style,
targetSelector = '.ant-table-body',
},
ref,
) {
const containerRef = useRef<HTMLDivElement | null>(null)
useImperativeHandle(ref, () => containerRef.current as HTMLDivElement, [])
return (
<div
ref={containerRef}
className={['table-scroll-region', className].filter(Boolean).join(' ')}
style={style}
>
{children}
<ScrollbarOverlay containerRef={containerRef} targetSelector={targetSelector} />
</div>
)
})
export default TableScrollRegion

View File

@@ -140,7 +140,8 @@ body {
opacity: 0;
cursor: pointer;
pointer-events: auto;
transition: opacity 0.18s ease, background 0.18s ease;
outline: none;
transition: opacity 0.18s ease, background 0.18s ease, box-shadow 0.18s ease;
}
.scrollbar__thumb::after {
@@ -173,6 +174,7 @@ body {
}
.scrollbar:hover .scrollbar__track--visible .scrollbar__thumb,
.scrollbar:focus-within .scrollbar__track--visible .scrollbar__thumb,
.scrollbar__track--visible .scrollbar__thumb,
.scrollbar__track--dragging .scrollbar__thumb {
opacity: 1;
@@ -182,12 +184,15 @@ body {
pointer-events: auto;
}
.scrollbar__thumb:hover {
background: rgba(216, 226, 240, 0.68);
.scrollbar__thumb:hover,
.scrollbar__thumb:focus-visible {
background: rgba(125, 146, 174, 0.82);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
.scrollbar__track--dragging .scrollbar__thumb {
background: rgba(226, 235, 246, 0.82);
background: rgba(92, 115, 146, 0.9);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
}
.dashboard-brand {
@@ -398,9 +403,11 @@ body {
.playground-card__scroll {
height: 100%;
}
.playground-card__scroll .scrollbar__viewport {
height: 100%;
overflow: auto;
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
}
.playground-card__scroll::-webkit-scrollbar,
@@ -1251,25 +1258,21 @@ body {
.playground-result-modal__content {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
padding-right: 6px;
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
}
.playground-result-modal__content::-webkit-scrollbar {
width: 8px;
.playground-result-modal__content .scrollbar__viewport {
height: 100%;
overflow: auto;
}
.playground-result-modal__content::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.82);
border-radius: 999px;
border: 2px solid transparent;
background-clip: padding-box;
.playground-result__blocks-scroll .scrollbar__viewport {
height: 100%;
overflow: auto;
}
.playground-result-modal__content::-webkit-scrollbar-track {
background: transparent;
.playground-result__blocks-scroll.scrollbar {
min-height: 0;
}
@media (max-width: 1200px) {
@@ -1583,14 +1586,14 @@ body {
padding: 10px 12px !important;
}
.data-source-table-region .ant-table-body,
.data-source-table-region .ant-table-content {
.table-scroll-region .ant-table-body,
.table-scroll-region .ant-table-content {
scrollbar-width: none;
-ms-overflow-style: none;
}
.data-source-table-region .ant-table-body::-webkit-scrollbar,
.data-source-table-region .ant-table-content::-webkit-scrollbar {
.table-scroll-region .ant-table-body::-webkit-scrollbar,
.table-scroll-region .ant-table-content::-webkit-scrollbar {
width: 0;
height: 0;
}
@@ -1757,7 +1760,10 @@ body {
flex: 1 1 auto;
min-width: 0;
min-height: 0;
overflow: auto;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
padding-right: 4px;
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
@@ -1836,10 +1842,11 @@ body {
display: grid;
gap: 14px;
max-height: calc(100vh - 180px);
overflow: auto;
padding-right: 6px;
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
}
.bgp-page__brief-modal-body .scrollbar__viewport {
overflow: auto;
}
.bgp-page__brief-evidence {
@@ -1857,6 +1864,10 @@ body {
gap: 12px;
}
.alerts-brief-drawer .scrollbar__viewport {
overflow: auto;
}
.alerts-brief-drawer__loading {
min-height: 160px;
display: flex;
@@ -1887,8 +1898,17 @@ body {
flex: 1 1 auto;
min-width: 0;
min-height: 0;
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.alerts-tab-panel > .ant-space-item,
.system-alerts-page__stack > .ant-space-item,
.bgp-alerts-page__stack > .ant-space-item,
.situational-alerts-page__stack > .ant-space-item {
min-width: 0;
}
.system-alerts-page__table-card,
@@ -2128,29 +2148,71 @@ body {
line-height: 1;
}
.bgp-page__summary-grid--compact {
flex-wrap: nowrap !important;
.bgp-page__summary-scroll.scrollbar,
.alerts-summary-scroll.scrollbar {
overflow: hidden;
}
.bgp-page__summary-scroll .scrollbar__viewport,
.alerts-summary-scroll .scrollbar__viewport {
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 4px;
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.82) transparent;
}
.bgp-page__summary-grid--compact::-webkit-scrollbar {
width: 8px;
height: 8px;
.bgp-page__summary-grid,
.alerts-summary-grid {
display: flex;
flex-wrap: nowrap !important;
min-width: max-content;
}
.bgp-page__summary-grid--compact::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.82);
border-radius: 999px;
border: 2px solid transparent;
background-clip: padding-box;
.bgp-page__summary-cell,
.alerts-summary-cell {
flex: 0 0 auto;
}
.bgp-page__summary-grid--compact::-webkit-scrollbar-track {
background: transparent;
.situational-alerts-page__summary-grid {
display: flex;
flex-wrap: nowrap;
min-width: max-content;
}
.situational-alerts-page__panels-grid {
display: flex;
min-width: max-content;
}
.situational-alerts-page__panels-scroll .scrollbar__viewport {
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 4px;
}
@media (max-width: 1200px) {
.situational-alerts-page__summary-grid {
display: grid;
grid-template-columns: repeat(2, minmax(220px, 1fr));
min-width: 0;
width: 100%;
}
.situational-alerts-page__summary-grid .alerts-summary-cell {
width: auto !important;
}
}
@media (max-width: 560px) {
.situational-alerts-page__summary-grid {
display: flex;
flex-wrap: nowrap;
width: auto;
min-width: max-content;
}
.situational-alerts-page__summary-grid .alerts-summary-cell {
width: 220px !important;
}
}
.bgp-page__table-card,
@@ -2408,25 +2470,21 @@ body {
.settings-panel-scroll {
flex: 1 1 auto;
min-height: 0;
padding-right: 6px;
}
.settings-panel-scroll.scrollbar {
overflow: hidden;
}
.settings-panel-scroll .scrollbar__viewport {
overflow-y: auto;
overflow-x: hidden;
padding-right: 6px;
scrollbar-gutter: stable;
}
.settings-panel-scroll::-webkit-scrollbar {
width: 10px;
}
.settings-panel-scroll::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.8);
border-radius: 999px;
border: 2px solid transparent;
background-clip: padding-box;
}
.settings-panel-scroll::-webkit-scrollbar-track {
background: transparent;
.settings-panel-scroll .scrollbar__viewport > * {
min-width: 0;
}
.settings-pane .data-source-table-region .ant-table-container {
@@ -3273,7 +3331,6 @@ body {
.dashboard-restart-log {
max-height: 180px;
overflow-y: auto;
padding: 10px 12px;
border-radius: 12px;
background: #0f172a;
@@ -3281,16 +3338,11 @@ body {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12px;
line-height: 1.6;
scrollbar-width: thin;
}
.dashboard-restart-log::-webkit-scrollbar {
width: 8px;
}
.dashboard-restart-log::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgba(148, 163, 184, 0.55);
.dashboard-restart-log .scrollbar__viewport {
overflow-y: auto;
max-height: 180px;
}
@media (max-width: 768px) {

View File

@@ -5,10 +5,8 @@ import {
Alert,
Button,
Card,
Col,
Descriptions,
Modal,
Row,
Space,
Spin,
Statistic,
@@ -21,6 +19,8 @@ import {
} from 'antd'
import AppLayout from '../../components/AppLayout/AppLayout'
import Scrollbar from '../../components/Scrollbar/Scrollbar'
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
import type { BGPAnomaly, BGPBriefRecord, BGPIncident } from '../../services/situational-awareness'
import { getSituationalAwarenessGateway } from '../../services/situational-awareness'
import { formatDateTimeZhCN } from '../../utils/datetime'
@@ -171,20 +171,22 @@ export function BGPAlertsPanel() {
<Alert type="info" showIcon message="这里聚焦 BGP 风险信号本身,不等同于系统平台运行告警。" />
<Row gutter={[12, 12]}>
<Col xs={24} sm={12} lg={6}>
<Card><Statistic title="活跃事件" value={summary.activeIncidents} /></Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card><Statistic title="严重事件" value={summary.criticalIncidents} valueStyle={{ color: '#ff4d4f' }} /></Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card><Statistic title="活跃异常" value={summary.activeAnomalies} /></Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card><Statistic title="高风险异常" value={summary.highRiskAnomalies} valueStyle={{ color: '#fa8c16' }} /></Card>
</Col>
</Row>
<Scrollbar className="alerts-summary-scroll bgp-alerts-page__summary-scroll">
<div className="alerts-summary-grid" style={{ gap: '12px' }}>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="活跃事件" value={summary.activeIncidents} /></Card>
</div>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="严重事件" value={summary.criticalIncidents} valueStyle={{ color: '#ff4d4f' }} /></Card>
</div>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="活跃异常" value={summary.activeAnomalies} /></Card>
</div>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="高风险异常" value={summary.highRiskAnomalies} valueStyle={{ color: '#fa8c16' }} /></Card>
</div>
</div>
</Scrollbar>
<Card className="bgp-alerts-page__table-card">
<Tabs
@@ -194,7 +196,7 @@ export function BGPAlertsPanel() {
key: 'incidents',
label: 'BGP 事件',
children: (
<div className="table-scroll-region bgp-alerts-page__table-region">
<TableScrollRegion className="bgp-alerts-page__table-region">
<Table<BGPIncident>
columns={incidentColumns}
dataSource={incidents}
@@ -204,14 +206,14 @@ export function BGPAlertsPanel() {
scroll={{ x: 1200, y: 480 }}
tableLayout="fixed"
/>
</div>
</TableScrollRegion>
),
},
{
key: 'anomalies',
label: 'BGP 异常',
children: (
<div className="table-scroll-region bgp-alerts-page__table-region">
<TableScrollRegion className="bgp-alerts-page__table-region">
<Table<BGPAnomaly>
columns={anomalyColumns}
dataSource={anomalies}
@@ -221,7 +223,7 @@ export function BGPAlertsPanel() {
scroll={{ x: 1100, y: 480 }}
tableLayout="fixed"
/>
</div>
</TableScrollRegion>
),
},
]}
@@ -244,14 +246,14 @@ export function BGPAlertsPanel() {
<Spin tip="正在生成 BGP AI 简报..." />
</div>
) : brief ? (
<div className="bgp-page__brief-modal-body">
<Scrollbar className="bgp-page__brief-modal-body">
<Descriptions size="small" column={3} className="bgp-page__brief-meta">
<Descriptions.Item label="Provider">{brief.provider || '-'}</Descriptions.Item>
<Descriptions.Item label="模型">{brief.model || '-'}</Descriptions.Item>
<Descriptions.Item label="生成时间">{formatDateTimeZhCN(brief.generated_at)}</Descriptions.Item>
</Descriptions>
<Typography.Paragraph className="alerts-brief-content">{brief.content_markdown}</Typography.Paragraph>
</div>
</Scrollbar>
) : (
<Text type="secondary"> BGP </Text>
)}

View File

@@ -5,10 +5,8 @@ import {
Alert,
Button,
Card,
Col,
Descriptions,
Drawer,
Row,
Space,
Spin,
Statistic,
@@ -18,6 +16,7 @@ import {
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
import Scrollbar from '../../components/Scrollbar/Scrollbar'
import type { BGPSummarySnapshot } from '../../services/situational-awareness'
import {
getSituationalAwarenessGateway,
@@ -119,42 +118,46 @@ export function SituationalAlertsPanel() {
message="态势告警不是单一模块列表,而是把系统告警与 BGP 风险综合成一份值班研判入口。"
/>
<Row gutter={[12, 12]}>
<Col xs={24} sm={12} lg={6}>
<Card><Statistic title="活跃系统告警" value={summary.activeSystemAlerts} prefix={<WarningOutlined />} /></Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card><Statistic title="严重系统告警" value={summary.criticalSystemAlerts} valueStyle={{ color: '#ff4d4f' }} /></Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card><Statistic title="活跃 BGP 事件" value={summary.activeBGPIncidents} prefix={<DeploymentUnitOutlined />} /></Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card><Statistic title="严重 BGP 事件" value={summary.criticalBGPIncidents} valueStyle={{ color: '#fa8c16' }} /></Card>
</Col>
</Row>
<Scrollbar className="alerts-summary-scroll situational-alerts-page__summary-scroll">
<div className="alerts-summary-grid situational-alerts-page__summary-grid" style={{ gap: '12px' }}>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="活跃系统告警" value={summary.activeSystemAlerts} prefix={<WarningOutlined />} /></Card>
</div>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="严重系统告警" value={summary.criticalSystemAlerts} valueStyle={{ color: '#ff4d4f' }} /></Card>
</div>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="活跃 BGP 事件" value={summary.activeBGPIncidents} prefix={<DeploymentUnitOutlined />} /></Card>
</div>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="严重 BGP 事件" value={summary.criticalBGPIncidents} valueStyle={{ color: '#fa8c16' }} /></Card>
</div>
</div>
</Scrollbar>
<Row gutter={[12, 12]}>
<Col xs={24} lg={12}>
<Card title="系统告警侧">
<Descriptions size="small" column={1}>
<Descriptions.Item label="严重">{String(systemStats?.critical ?? '-')}</Descriptions.Item>
<Descriptions.Item label="警告">{String(systemStats?.warning ?? '-')}</Descriptions.Item>
<Descriptions.Item label="信息">{String(systemStats?.info ?? '-')}</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col xs={24} lg={12}>
<Card title="BGP 风险侧">
<Descriptions size="small" column={1}>
<Descriptions.Item label="活跃事件">{String(bgpSummary?.incidentSummary?.by_status?.active ?? '-')}</Descriptions.Item>
<Descriptions.Item label="严重事件">{String(bgpSummary?.incidentSummary?.by_severity?.critical ?? '-')}</Descriptions.Item>
<Descriptions.Item label="活跃观测站">{String(bgpSummary?.collectorSummary?.active_collectors ?? '-')}</Descriptions.Item>
<Descriptions.Item label="近24h事件">{String(bgpSummary?.collectorSummary?.recent_24h_events ?? '-')}</Descriptions.Item>
</Descriptions>
</Card>
</Col>
</Row>
<Scrollbar className="alerts-summary-scroll situational-alerts-page__panels-scroll">
<div className="alerts-summary-grid situational-alerts-page__panels-grid" style={{ gap: '12px' }}>
<div className="alerts-summary-cell" style={{ width: '360px' }}>
<Card title="系统告警侧">
<Descriptions size="small" column={1}>
<Descriptions.Item label="严重">{String(systemStats?.critical ?? '-')}</Descriptions.Item>
<Descriptions.Item label="警告">{String(systemStats?.warning ?? '-')}</Descriptions.Item>
<Descriptions.Item label="信息">{String(systemStats?.info ?? '-')}</Descriptions.Item>
</Descriptions>
</Card>
</div>
<div className="alerts-summary-cell" style={{ width: '360px' }}>
<Card title="BGP 风险侧">
<Descriptions size="small" column={1}>
<Descriptions.Item label="活跃事件">{String(bgpSummary?.incidentSummary?.by_status?.active ?? '-')}</Descriptions.Item>
<Descriptions.Item label="严重事件">{String(bgpSummary?.incidentSummary?.by_severity?.critical ?? '-')}</Descriptions.Item>
<Descriptions.Item label="活跃观测站">{String(bgpSummary?.collectorSummary?.active_collectors ?? '-')}</Descriptions.Item>
<Descriptions.Item label="近24h事件">{String(bgpSummary?.collectorSummary?.recent_24h_events ?? '-')}</Descriptions.Item>
</Descriptions>
</Card>
</div>
</div>
</Scrollbar>
</Space>
<Drawer title="态势告警 AI 简报" placement="right" width={560} onClose={() => setBriefOpen(false)} open={briefOpen}>

View File

@@ -5,11 +5,9 @@ import {
Alert,
Button,
Card,
Col,
Descriptions,
Drawer,
Modal,
Row,
Space,
Spin,
Statistic,
@@ -22,6 +20,8 @@ import {
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
import Scrollbar from '../../components/Scrollbar/Scrollbar'
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
import type { AlertBriefResponse, AlertRecord } from '../../services/situational-awareness'
import { formatDateTimeZhCN } from '../../utils/datetime'
@@ -216,20 +216,22 @@ export function SystemAlertsPanel() {
<Alert type="info" showIcon message="这里展示的是平台与采集链路告警,不等同于 BGP 态势风险本身。" />
<Row gutter={[12, 12]}>
<Col xs={24} sm={8}>
<Card><Statistic title="严重告警" value={stats.critical} valueStyle={{ color: '#ff4d4f' }} prefix={<AlertOutlined />} /></Card>
</Col>
<Col xs={24} sm={8}>
<Card><Statistic title="警告" value={stats.warning} valueStyle={{ color: '#faad14' }} prefix={<AlertOutlined />} /></Card>
</Col>
<Col xs={24} sm={8}>
<Card><Statistic title="信息" value={stats.info} valueStyle={{ color: '#1890ff' }} prefix={<InfoCircleOutlined />} /></Card>
</Col>
</Row>
<Scrollbar className="alerts-summary-scroll system-alerts-page__summary-scroll">
<div className="alerts-summary-grid" style={{ gap: '12px' }}>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="严重告警" value={stats.critical} valueStyle={{ color: '#ff4d4f' }} prefix={<AlertOutlined />} /></Card>
</div>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="警告" value={stats.warning} valueStyle={{ color: '#faad14' }} prefix={<AlertOutlined />} /></Card>
</div>
<div className="alerts-summary-cell" style={{ width: '220px' }}>
<Card><Statistic title="信息" value={stats.info} valueStyle={{ color: '#1890ff' }} prefix={<InfoCircleOutlined />} /></Card>
</div>
</div>
</Scrollbar>
<Card className="system-alerts-page__table-card" title="系统告警列表">
<div className="table-scroll-region system-alerts-page__table-region">
<TableScrollRegion className="system-alerts-page__table-region">
<Table<AlertRecord>
columns={columns}
dataSource={alerts}
@@ -239,7 +241,7 @@ export function SystemAlertsPanel() {
scroll={{ x: 1100, y: 480 }}
tableLayout="fixed"
/>
</div>
</TableScrollRegion>
</Card>
</Space>
@@ -260,7 +262,7 @@ export function SystemAlertsPanel() {
</Modal>
<Drawer title="系统告警 AI 简报" placement="right" width={520} onClose={() => setBriefOpen(false)} open={briefOpen}>
<div className="alerts-brief-drawer">
<Scrollbar className="alerts-brief-drawer">
{briefLoading ? (
<div className="alerts-brief-drawer__loading">
<Spin tip="正在汇总系统告警事实并生成简报..." />
@@ -293,7 +295,7 @@ export function SystemAlertsPanel() {
</Card>
</Space>
) : null}
</div>
</Scrollbar>
</Drawer>
</div>
</AppLayout>

View File

@@ -4,10 +4,8 @@ import {
Alert,
Button,
Card,
Col,
Descriptions,
Modal,
Row,
Select,
Space,
Spin,
@@ -22,6 +20,8 @@ import {
} from 'antd'
import AppLayout from '../../components/AppLayout/AppLayout'
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
import Scrollbar from '../../components/Scrollbar/Scrollbar'
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
import { formatDateTimeZhCN } from '../../utils/datetime'
import {
type BGPAnomaly,
@@ -708,37 +708,36 @@ function BGP() {
/>
<Card className="bgp-page__summary-card">
<Row
gutter={[compactViewport ? 8 : 12, compactViewport ? 8 : 12]}
className={`bgp-page__summary-grid${compactViewport ? ' bgp-page__summary-grid--compact' : ''}`}
wrap={!compactViewport}
>
{summaryItems.map((item) => (
<Col
key={item.label}
xs={24}
sm={12}
md={8}
flex={compactViewport ? '180px' : undefined}
>
<div className="bgp-page__summary-item">
<div className="bgp-page__summary-label">{item.label}</div>
<Statistic className="bgp-page__summary-stat" value={item.value} />
<Scrollbar className="bgp-page__summary-scroll">
<div
className="bgp-page__summary-grid"
style={{ gap: `${compactViewport ? 8 : 12}px` }}
>
{summaryItems.map((item) => (
<div
key={item.label}
className="bgp-page__summary-cell"
style={{ width: compactViewport ? '180px' : '220px' }}
>
<div className="bgp-page__summary-item">
<div className="bgp-page__summary-label">{item.label}</div>
<Statistic className="bgp-page__summary-stat" value={item.value} />
</div>
</div>
</Col>
))}
</Row>
))}
</div>
</Scrollbar>
</Card>
<Card className="bgp-page__table-card">
<div ref={tableRegionRef} className="table-scroll-region bgp-page__table-region">
<TableScrollRegion ref={tableRegionRef} className="bgp-page__table-region">
<Tabs
className="bgp-page__tabs"
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
/>
</div>
</TableScrollRegion>
</Card>
</Space>
</div>
@@ -755,7 +754,7 @@ function BGP() {
destroyOnHidden
>
{brief ? (
<div className="bgp-page__brief-modal-body">
<Scrollbar className="bgp-page__brief-modal-body">
{(brief.facts.length > 0 || Object.keys(brief.context || {}).length > 0) ? (
<div className="bgp-page__brief-evidence">
{brief.facts.length > 0 ? (
@@ -785,7 +784,7 @@ function BGP() {
</div>
) : null}
<MarkdownRenderer markdown={brief.content_markdown} />
</div>
</Scrollbar>
) : (
<Text type="secondary"></Text>
)}

View File

@@ -14,6 +14,7 @@ import { Link } from 'react-router-dom'
import axios from 'axios'
import { useAuthStore } from '../../stores/auth'
import AppLayout from '../../components/AppLayout/AppLayout'
import Scrollbar from '../../components/Scrollbar/Scrollbar'
import { useWebSocket } from '../../hooks/useWebSocket'
import { formatDateTimeZhCN } from '../../utils/datetime'
@@ -532,11 +533,11 @@ function Dashboard() {
<div className="dashboard-restart-section">
<Text className="dashboard-restart-section__label"></Text>
<div className="dashboard-restart-log">
<Scrollbar className="dashboard-restart-log">
{restartLogs.length > 0 ? restartLogs.map((line, index) => (
<div key={`${line}-${index}`}>{line}</div>
)) : <div></div>}
</div>
</Scrollbar>
</div>
</div>
</Modal>

View File

@@ -12,6 +12,7 @@ import {
} from '@ant-design/icons'
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
import { formatDateTimeZhCN, formatDateZhCN, parseBackendDate } from '../../utils/datetime'
const { Title, Text } = Typography
@@ -939,7 +940,7 @@ function DataList() {
/>
</div>
</div>
<div className="table-scroll-region data-list-table-region" style={{ padding: isCompact ? 10 : 12 }}>
<TableScrollRegion className="data-list-table-region" style={{ padding: isCompact ? 10 : 12 }}>
<Table
columns={columns}
dataSource={data}
@@ -960,7 +961,7 @@ function DataList() {
showTotal: (count) => `${count}`,
}}
/>
</div>
</TableScrollRegion>
</Card>
</div>
</div>

View File

@@ -116,18 +116,39 @@ function finalizeBulkProgressBatch(batch: BulkProgressBatch | null): BulkProgres
if (!batch || batch.sourceIds.length === 0) {
return null
}
return batch
}
const hasRunningItem = batch.sourceIds.some((sourceId) => batch.items[sourceId]?.is_running)
if (hasRunningItem) {
return batch
function resolveTerminalBatchItem(
sourceId: number,
batch: BulkProgressBatch,
builtInSources: BuiltInDataSource[],
taskProgress: Record<number, TaskTrackerState>,
): BulkProgressItem | null {
const currentItem = batch.items[sourceId]
const source = builtInSources.find((item) => item.id === sourceId)
const trackedTask = taskProgress[sourceId]
const isRunning = trackedTask?.is_running ?? source?.is_running ?? currentItem?.is_running ?? false
if (isRunning) {
return null
}
const allFinished = batch.sourceIds.every((sourceId) => {
const status = batch.items[sourceId]?.status
return Boolean(status && status !== 'running')
})
const status = trackedTask?.status ?? source?.last_status ?? currentItem?.status ?? null
if (!status || status === 'running') {
return null
}
return allFinished ? null : batch
return {
task_id: trackedTask?.task_id ?? currentItem?.task_id ?? source?.task_id ?? null,
progress:
status === 'success'
? 100
: trackedTask?.progress ?? source?.progress ?? currentItem?.progress ?? 0,
is_running: false,
phase: trackedTask?.phase ?? source?.phase ?? currentItem?.phase ?? null,
status,
}
}
interface WebSocketTaskMessage {
@@ -436,6 +457,41 @@ function DataSources() {
return () => clearInterval(interval)
}, [builtInSources, taskProgress, taskSocketConnected, fetchData])
useEffect(() => {
if (!bulkProgressBatch) return
let changed = false
const nextItems = { ...bulkProgressBatch.items }
for (const sourceId of bulkProgressBatch.sourceIds) {
const nextItem = resolveTerminalBatchItem(sourceId, bulkProgressBatch, builtInSources, taskProgress)
if (!nextItem) continue
const previousItem = bulkProgressBatch.items[sourceId]
if (
previousItem?.status === nextItem.status &&
previousItem?.is_running === nextItem.is_running &&
previousItem?.progress === nextItem.progress &&
previousItem?.phase === nextItem.phase
) {
continue
}
nextItems[sourceId] = nextItem
changed = true
}
if (!changed) return
setBulkProgressBatch((prev) => {
if (!prev) return prev
return finalizeBulkProgressBatch({
...prev,
items: nextItems,
})
})
}, [bulkProgressBatch, builtInSources, taskProgress])
const triggerDatasource = async (id: number, options?: { force?: boolean }) => {
const force = options?.force ?? false
const res = await axios.post(`/api/v1/datasources/${id}/trigger`, null, {

View File

@@ -22,6 +22,8 @@ import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
import Scrollbar from '../../components/Scrollbar/Scrollbar'
import ScrollbarOverlay from '../../components/Scrollbar/ScrollbarOverlay'
import { useAuthStore } from '../../stores/auth'
const { Title, Text, Paragraph } = Typography
@@ -239,6 +241,7 @@ function Playground() {
const [showScrollToBottom, setShowScrollToBottom] = useState(false)
const pollTimerRef = useRef<number | null>(null)
const messagesContainerRef = useRef<HTMLDivElement | null>(null)
const messagesShellRef = useRef<HTMLDivElement | null>(null)
const forceScrollToBottomRef = useRef(true)
const selectedPreset = useMemo(
@@ -575,7 +578,7 @@ function Playground() {
/>
)}
>
<div className="playground-card__scroll">
<Scrollbar className="playground-card__scroll">
<Spin spinning={statusLoading}>
{providerStatus ? (
<div className="playground-provider-panel">
@@ -617,7 +620,7 @@ function Playground() {
<Alert type="warning" showIcon message="尚未获取到 AI Provider 状态" />
)}
</Spin>
</div>
</Scrollbar>
</Card>
)
@@ -699,7 +702,7 @@ function Playground() {
</Space>
)}
>
<div className="playground-chat__messages-shell">
<div ref={messagesShellRef} className="playground-chat__messages-shell">
<div className="playground-chat__messages" ref={messagesContainerRef} onScroll={handleMessagesScroll}>
{messages.map((entry) => (
<div key={entry.id} className={`playground-message playground-message--${entry.role}`}>
@@ -760,8 +763,9 @@ function Playground() {
<Button size="small" type="primary" loading={editSaving} onClick={() => void handleSaveEditMessage(entry)}>
</Button>
</div>
</div>
</div>
<ScrollbarOverlay containerRef={messagesShellRef} targetSelector=".playground-chat__messages" />
</div>
</div>
) : (entry.role !== 'assistant' || entry.status === 'answering' || entry.status === 'done' || entry.status === 'stopped' || entry.status === 'error') && entry.markdown ? (
<div className="playground-message__markdown">
@@ -1000,42 +1004,42 @@ function Playground() {
/>
</div>
<div className="playground-result-modal__content">
<Scrollbar className="playground-result-modal__content">
<Space direction="vertical" size={16} className="playground-result__stack" style={{ width: '100%' }}>
{analysis.text_blocks.length ? (
<div className="playground-result__blocks">
<Text strong></Text>
<div className="playground-result__blocks-scroll">
<Scrollbar className="playground-result__blocks-scroll">
{analysis.text_blocks.map((block, index) => (
<Card key={`${index}-${block.slice(0, 12)}`} size="small">
<pre>{block}</pre>
</Card>
))}
</div>
</Scrollbar>
</div>
) : null}
{analysis.thinking_blocks.length ? (
<div className="playground-result__blocks">
<Text strong>Thinking Blocks</Text>
<div className="playground-result__blocks-scroll">
<Scrollbar className="playground-result__blocks-scroll">
{analysis.thinking_blocks.map((block, index) => (
<Card key={`${index}-${block.slice(0, 12)}`} size="small">
<pre>{block}</pre>
</Card>
))}
</div>
</Scrollbar>
</div>
) : null}
<div className="playground-result__blocks">
<Text strong>Raw Response</Text>
<div className="playground-result__blocks-scroll playground-result__blocks-scroll--raw">
<Scrollbar className="playground-result__blocks-scroll playground-result__blocks-scroll--raw">
<Card size="small">
<pre>{JSON.stringify(analysis.raw_response, null, 2)}</pre>
</Card>
</div>
</Scrollbar>
</div>
</Space>
</div>
</Scrollbar>
</div>
) : null}
</Modal>

View File

@@ -15,6 +15,8 @@ import {
} from 'antd'
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
import Scrollbar from '../../components/Scrollbar/Scrollbar'
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
import { formatDateTimeZhCN } from '../../utils/datetime'
const { Title, Text } = Typography
@@ -91,7 +93,7 @@ function SettingsPanel({
return (
<div className="settings-pane">
<Card className="settings-panel-card" loading={loading}>
<div className="settings-panel-scroll">{children}</div>
<Scrollbar className="settings-panel-scroll">{children}</Scrollbar>
</Card>
</div>
)
@@ -642,7 +644,7 @@ function Settings() {
</Button>
</div>
</div>
<div className="table-scroll-region data-source-table-region">
<TableScrollRegion className="data-source-table-region">
<Table
rowKey="id"
columns={tvSourceColumns}
@@ -652,7 +654,7 @@ function Settings() {
tableLayout="fixed"
size="small"
/>
</div>
</TableScrollRegion>
</div>
</Card>
</div>
@@ -668,7 +670,7 @@ function Settings() {
loading={loading}
styles={{ body: { padding: 0 } }}
>
<div ref={collectorTableRegionRef} className="table-scroll-region data-source-table-region">
<TableScrollRegion ref={collectorTableRegionRef} className="data-source-table-region">
<Table
rowKey="id"
columns={collectorColumns}
@@ -678,7 +680,7 @@ function Settings() {
tableLayout="fixed"
size="small"
/>
</div>
</TableScrollRegion>
</Card>
</div>
),

View File

@@ -3,6 +3,7 @@ import { Table, Tag, Card, Row, Col, Statistic, Button } from 'antd'
import { ReloadOutlined, CheckCircleOutlined, CloseCircleOutlined, SyncOutlined } from '@ant-design/icons'
import { useAuthStore } from '../../stores/auth'
import AppLayout from '../../components/AppLayout/AppLayout'
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
import { formatDateTimeZhCN } from '../../utils/datetime'
interface Task {
@@ -146,9 +147,9 @@ function Tasks() {
</Button>
}
>
<div className="table-scroll-region">
<TableScrollRegion>
<Table columns={columns} dataSource={tasks} rowKey="id" loading={loading} pagination={{ pageSize: 10 }} scroll={{ x: 'max-content', y: 'calc(100% - 360px)' }} tableLayout="fixed" />
</div>
</TableScrollRegion>
</Card>
</AppLayout>
)

View File

@@ -3,6 +3,7 @@ import { Table, Button, Tag, Space, message, Modal, Form, Input, Select } from '
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
import axios from 'axios'
import AppLayout from '../../components/AppLayout/AppLayout'
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
interface User {
id: number
@@ -144,7 +145,7 @@ function Users() {
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}></Button>
</div>
<div className="page-shell__body">
<div ref={tableRegionRef} className="table-scroll-region data-source-table-region users-table-region" style={{ height: '100%' }}>
<TableScrollRegion ref={tableRegionRef} className="data-source-table-region users-table-region" style={{ height: '100%' }}>
<Table
columns={columns}
dataSource={users}
@@ -153,7 +154,7 @@ function Users() {
scroll={{ x: 'max-content', y: tableHeight }}
tableLayout="fixed"
/>
</div>
</TableScrollRegion>
</div>
</div>
<Modal