@@ -287,7 +294,7 @@ export default function AppSidebar() {
theme={currentTheme}
mode="inline"
selectedKeys={[selectedKey]}
- openKeys={collapsed ? undefined : openKeys}
+ openKeys={railCollapsed ? undefined : openKeys}
onOpenChange={(keys) => setOpenKeys(keys as string[])}
className="sider-nav"
items={toMenuItems(navItems)}
@@ -302,7 +309,7 @@ export default function AppSidebar() {
onClick={onMenuClick}
/>
-
+
diff --git a/frontend/src/models/status.ts b/frontend/src/models/status.ts
index 9bcfa6cf6..d8bfad792 100644
--- a/frontend/src/models/status.ts
+++ b/frontend/src/models/status.ts
@@ -1,5 +1,10 @@
import { NumberFormatter } from '@/utils';
+export const USAGE_WARN_PERCENT = 80;
+export const USAGE_CRIT_PERCENT = 90;
+export const USAGE_WARN_COLOR = '#faad14';
+export const USAGE_CRIT_COLOR = '#ff4d4f';
+
export class CurTotal {
current: number;
total: number;
@@ -16,9 +21,9 @@ export class CurTotal {
get color(): string {
const p = this.percent;
- if (p < 80) return '#1677ff';
- if (p < 90) return '#faad14';
- return '#ff4d4f';
+ if (p < USAGE_WARN_PERCENT) return '#1677ff';
+ if (p < USAGE_CRIT_PERCENT) return USAGE_WARN_COLOR;
+ return USAGE_CRIT_COLOR;
}
}
diff --git a/frontend/src/pages/index/ConnectionsCard.tsx b/frontend/src/pages/index/ConnectionsCard.tsx
new file mode 100644
index 000000000..424664b42
--- /dev/null
+++ b/frontend/src/pages/index/ConnectionsCard.tsx
@@ -0,0 +1,74 @@
+import { useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Card, theme } from 'antd';
+
+import { Sparkline } from '@/components/viz';
+import type { Status } from '@/models/status';
+
+interface ConnectionsCardProps {
+ status: Status;
+ tcp: number[];
+ udp: number[];
+ labels: string[];
+ isMobile: boolean;
+}
+
+export default function ConnectionsCard({ status, tcp, udp, labels, isMobile }: ConnectionsCardProps) {
+ const { t } = useTranslation();
+ const { token } = theme.useToken();
+ const accent = token.colorPrimary;
+ const udpColor = token.colorTextTertiary;
+
+ const referenceLines = useMemo(
+ () => [
+ { y: status.udpCount, color: udpColor, dash: '2 4' },
+ { y: status.tcpCount, color: accent, dash: '2 4' },
+ ],
+ [status.tcpCount, status.udpCount, accent, udpColor],
+ );
+
+ return (
+
+
+
{t('pages.index.connectionCount')}
+
+ {status.tcpCount + status.udpCount}
+ {t('pages.index.openSockets')}
+
+
+
+
+
+
+ TCP
+ {status.tcpCount.toLocaleString()}
+
+
+
+ UDP
+ {status.udpCount.toLocaleString()}
+
+
+
+
+ Math.round(v).toLocaleString()}
+ referenceLines={referenceLines}
+ />
+
+
+ );
+}
diff --git a/frontend/src/pages/index/IndexPage.css b/frontend/src/pages/index/IndexPage.css
index 3ca7c921e..ca3dd656c 100644
--- a/frontend/src/pages/index/IndexPage.css
+++ b/frontend/src/pages/index/IndexPage.css
@@ -1,51 +1,470 @@
+/* Overview page — trend-first layout. Every colour comes from the AntD theme
+ tokens so light / dark / ultra-dark keep working and the sidebar is untouched.
+ Set --ov-accent once here if you want a fixed accent instead of the primary. */
+
+.index-page {
+ --ov-accent: var(--ant-color-primary);
+ --ov-line: var(--ant-color-border);
+ --ov-label: var(--ant-color-text-secondary);
+ --ov-faint: var(--ant-color-text-tertiary);
+ --ov-gap: 12px;
+ --ov-pad: 20px;
+}
+
@media (max-width: 768px) {
.index-page .content-area {
padding: 12px;
padding-top: 64px;
}
+
+ .index-page {
+ --ov-gap: 8px;
+ --ov-pad: 14px;
+ }
}
-.index-page .action {
- cursor: pointer;
- justify-content: center;
- max-width: 100%;
- flex-wrap: nowrap;
+.ov-page {
+ display: flex;
+ flex-direction: column;
+ gap: var(--ov-gap);
}
-.index-page .action > span:not(.anticon) {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 0;
+/* — action bar — */
+
+.ov-bar {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
}
-.index-page .action-update {
- color: var(--ant-color-warning);
- font-weight: 600;
-}
-
-.index-page .action-update .anticon {
- color: var(--ant-color-warning);
-}
-
-.index-page .history-tag {
- cursor: pointer;
+.ov-state {
display: inline-flex;
align-items: center;
- gap: 4px;
+ gap: 8px;
+ padding: 4px 12px;
+ border: 1px solid var(--ov-line);
+ border-radius: 999px;
+ font-size: 13px;
+ color: var(--ant-color-text);
+}
+
+.ov-state-dot {
+ position: relative;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: currentColor;
+ flex: none;
+}
+
+.ov-state[data-state='running'] .ov-state-dot::after {
+ content: '';
+ position: absolute;
+ inset: -1px;
+ border-radius: 50%;
+ border: 1px solid currentColor;
+ animation: ovPulse 1.6s infinite ease-out;
+}
+
+@keyframes ovPulse {
+ 0% { transform: scale(0.9); opacity: 0.5; }
+ 100% { transform: scale(2.4); opacity: 0; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .ov-state[data-state='running'] .ov-state-dot::after {
+ animation: none;
+ }
+}
+
+.ov-state-version,
+.ov-panel-version {
+ padding: 0;
+ border: 0;
+ background: transparent;
+ font: inherit;
+ cursor: pointer;
+ color: var(--ov-label);
+ transition: color 0.2s;
+}
+
+.ov-state-version:hover,
+.ov-state-version:focus-visible,
+.ov-panel-version:hover,
+.ov-panel-version:focus-visible {
+ color: var(--ant-color-primary);
+}
+
+.ov-panel-version {
+ font-size: 12px;
+ color: var(--ov-faint);
+}
+
+.ov-update-tag {
+ cursor: pointer;
margin-inline-end: 0;
}
-.index-page .ip-toggle-icon {
- cursor: pointer;
- font-size: 16px;
+.ov-error-detail {
+ white-space: pre-wrap;
+ word-break: break-word;
}
-.index-page .ip-hidden .ant-statistic-content-value {
- filter: blur(6px);
+.ov-bar-actions {
+ margin-inline-start: auto;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-wrap: wrap;
+}
+
+@media (max-width: 768px) {
+ .ov-bar-actions {
+ margin-inline-start: 0;
+ width: 100%;
+ justify-content: space-between;
+ }
+}
+
+.ov-bar-sep {
+ width: 1px;
+ height: 20px;
+ background: var(--ov-line);
+ margin: 0 4px;
+}
+
+.ov-health {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12.5px;
+}
+
+.ov-health-mark {
+ width: 14px;
+ height: 1px;
+ background: currentColor;
+ flex: none;
+}
+
+.ov-rule {
+ height: 1px;
+ border: 0;
+ margin: 0;
+ background: linear-gradient(
+ to right,
+ transparent,
+ var(--ov-line) 48px,
+ var(--ov-line) calc(100% - 48px),
+ transparent
+ );
+}
+
+/* — shared type — */
+
+.ov-kicker {
+ font-size: 11px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--ov-label);
+}
+
+.ov-kicker-icon {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+}
+
+.ov-sub {
+ font-size: 12.5px;
+ margin-top: 4px;
+ color: var(--ov-faint);
+}
+
+.ov-mono {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+}
+
+/* — vitals tiles — */
+
+.ov-vitals {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: var(--ov-gap);
+}
+
+@media (max-width: 1100px) {
+ .ov-vitals { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+
+@media (max-width: 560px) {
+ .ov-vitals { grid-template-columns: minmax(0, 1fr); }
+}
+
+.ov-tile {
+ overflow: hidden;
+}
+
+.ov-tile-head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 14px var(--ov-pad) 0;
+ color: var(--ov-accent);
+}
+
+.ov-tile-icon {
+ display: inline-flex;
+ font-size: 15px;
+}
+
+.ov-tile-value {
+ display: flex;
+ align-items: baseline;
+ gap: 4px;
+ padding: 12px var(--ov-pad) 0;
+}
+
+.ov-tile-number {
+ font-size: 34px;
+ font-weight: 600;
+ line-height: 1;
+ letter-spacing: -0.02em;
+ color: var(--ant-color-text);
+ font-variant-numeric: tabular-nums;
+}
+
+.ov-tile-unit {
+ font-size: 14px;
+ color: var(--ov-label);
+}
+
+.ov-tile-detail {
+ padding: 5px var(--ov-pad) 0;
+ font-size: 12px;
+ color: var(--ov-label);
+}
+
+.ov-tile-foot {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 14px var(--ov-pad) 0;
+ font-size: 10px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: var(--ov-faint);
+}
+
+.ov-tile-chart {
+ margin-top: 6px;
+}
+
+/* — throughput + connections — */
+
+.ov-mid {
+ display: grid;
+ grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
+ gap: var(--ov-gap);
+}
+
+@media (max-width: 1100px) {
+ .ov-mid { grid-template-columns: minmax(0, 1fr); }
+}
+
+.ov-wide-head {
+ display: flex;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ gap: 16px;
+ padding: var(--ov-pad) var(--ov-pad) 0;
+}
+
+.ov-wide-head-stack {
+ flex-direction: column;
+ gap: 0;
+}
+
+.ov-wide-legend {
+ margin-inline-start: auto;
+ display: flex;
+ gap: 22px;
+ text-align: end;
+}
+
+.ov-legend-label {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 6px;
+ font-size: 11px;
+ color: var(--ov-label);
+}
+
+.ov-legend-num {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--ant-color-text);
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+}
+
+.ov-conn-total {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ margin-top: 12px;
+}
+
+.ov-conn-legend {
+ display: flex;
+ gap: 16px;
+ padding: 16px var(--ov-pad) 0;
+}
+
+.ov-conn-legend > div {
+ flex: 1 1 0;
+}
+
+.ov-conn-legend .ov-legend-label {
+ justify-content: flex-start;
+}
+
+.ov-swatch {
+ width: 14px;
+ height: 2px;
+ flex: none;
+}
+
+.ov-wide-chart {
+ padding: 12px 8px 0;
+}
+
+.ov-wide-foot {
+ display: flex;
+ gap: 16px;
+ margin: 12px var(--ov-pad) 0;
+ padding: 14px 0 var(--ov-pad);
+ border-top: 1px solid var(--ov-line);
+}
+
+.ov-wide-foot > div {
+ flex: 1 1 0;
+}
+
+.ov-foot-sep {
+ width: 1px;
+ background: var(--ov-line);
+}
+
+.ov-foot-value {
+ font-size: 18px;
+ font-weight: 600;
+ margin-top: 4px;
+ color: var(--ant-color-text);
+ font-variant-numeric: tabular-nums;
+}
+
+.ov-foot-part {
+ display: inline-block;
+ white-space: nowrap;
+}
+
+@media (max-width: 560px) {
+ .ov-wide-foot {
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .ov-wide-foot .ov-foot-sep {
+ display: none;
+ }
+}
+
+/* — system strip — */
+
+/* tracks follow SystemStrip's cell order:
+ uptime (xray | os) · panel (memory | threads) · ip addresses */
+.ov-strip-grid {
+ display: grid;
+ grid-template-columns:
+ minmax(max-content, 1.2fr) minmax(max-content, 1.2fr) minmax(0, 1.6fr);
+ gap: 16px;
+ padding: var(--ov-pad);
+}
+
+@media (max-width: 1439px) {
+ .ov-strip-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+}
+
+@media (max-width: 1100px) {
+ .ov-strip-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+
+@media (max-width: 560px) {
+ .ov-strip-grid { grid-template-columns: minmax(0, 1fr); }
+}
+
+@media (min-width: 1440px) {
+ .ov-strip-cell + .ov-strip-cell {
+ border-inline-start: 1px solid var(--ov-line);
+ padding-inline-start: 16px;
+ }
+}
+
+.ov-strip-value {
+ font-size: 19px;
+ font-weight: 600;
+ margin-top: 6px;
+ color: var(--ant-color-text);
+ font-variant-numeric: tabular-nums;
+}
+
+.ov-strip-split {
+ display: flex;
+ align-items: stretch;
+ gap: 14px;
+}
+
+.ov-strip-split-sep {
+ width: 1px;
+ background: var(--ov-line);
+ margin-top: 8px;
+}
+
+.ov-strip-sub {
+ font-size: 10px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ margin-top: 8px;
+ color: var(--ov-faint);
+}
+
+.ov-strip-sub + .ov-strip-value {
+ margin-top: 2px;
+}
+
+.ov-ip {
+ margin-top: 7px;
+ font-size: 13px;
+ overflow-wrap: anywhere;
transition: filter 0.2s ease;
}
-.index-page .ip-visible .ant-statistic-content-value {
- filter: none;
+.ov-ip-v6 {
+ margin-top: 3px;
+ color: var(--ov-label);
+}
+
+/* — preserved from the previous overview — */
+
+.index-page .ip-toggle-icon {
+ cursor: pointer;
+ font-size: 15px;
+ margin-inline-start: auto;
+}
+
+.index-page .ip-hidden {
+ filter: blur(6px);
}
diff --git a/frontend/src/pages/index/IndexPage.tsx b/frontend/src/pages/index/IndexPage.tsx
index 97b08b759..87441e91f 100644
--- a/frontend/src/pages/index/IndexPage.tsx
+++ b/frontend/src/pages/index/IndexPage.tsx
@@ -1,53 +1,29 @@
import { lazy, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
+import { Button, ConfigProvider, Layout, Modal, Result, Spin, message } from 'antd';
import {
- Button,
- Card,
- Col,
- ConfigProvider,
- Layout,
- message,
- Modal,
- Result,
- Row,
- Space,
- Spin,
- Statistic,
- Tag,
- Tooltip,
-} from 'antd';
-import {
- BarsOutlined,
- ControlOutlined,
- CloudServerOutlined,
- CloudDownloadOutlined,
- CloudUploadOutlined,
- ArrowUpOutlined,
- ArrowDownOutlined,
- AreaChartOutlined,
- GlobalOutlined,
- SwapOutlined,
- EyeOutlined,
- EyeInvisibleOutlined,
- ThunderboltOutlined,
- DesktopOutlined,
- DatabaseOutlined,
- ForkOutlined,
CopyOutlined,
- TelegramFilled,
+ CloudDownloadOutlined,
+ DashboardOutlined,
+ DatabaseOutlined,
+ HddOutlined,
+ SwapOutlined,
} from '@ant-design/icons';
-import { HttpUtil, SizeFormatter, TimeFormatter, ClipboardManager, FileManager } from '@/utils';
-import { formatPanelVersion } from '@/lib/panel-version';
-import { activateOnKey } from '@/utils/a11y';
+import { HttpUtil, CPUFormatter, SizeFormatter, ClipboardManager, FileManager } from '@/utils';
+import { USAGE_CRIT_COLOR, USAGE_CRIT_PERCENT, USAGE_WARN_COLOR, USAGE_WARN_PERCENT } from '@/models/status';
import { useTheme } from '@/hooks/useTheme';
import { useStatusQuery } from '@/api/queries/useStatusQuery';
import { useMediaQuery } from '@/hooks/useMediaQuery';
import AppSidebar from '@/layouts/AppSidebar';
import { LazyMount } from '@/components/utility';
import { setMessageInstance } from '@/utils/messageBus';
-import StatusCard from './StatusCard';
-import XrayStatusCard from './XrayStatusCard';
+import OverviewActionBar from './OverviewActionBar';
+import VitalTile from './VitalTile';
+import ThroughputCard from './ThroughputCard';
+import ConnectionsCard from './ConnectionsCard';
+import SystemStrip from './SystemStrip';
+import { mean, peak, useOverviewHistory } from './useOverviewHistory';
import type { PanelUpdateInfo } from './PanelUpdateModal';
const JsonEditor = lazy(() => import('@/components/form/JsonEditor'));
const PanelUpdateModal = lazy(() => import('./PanelUpdateModal'));
@@ -90,6 +66,8 @@ export default function IndexPage() {
const [loading, setLoading] = useState(false);
const [loadingTip, setLoadingTip] = useState(t('loading'));
+ const history = useOverviewHistory(status, fetched && !fetchError);
+
useEffect(() => {
HttpUtil.post<{ accessLogEnable?: boolean; devChannelEnable?: boolean }>(
'/panel/api/setting/defaultSettings',
@@ -127,10 +105,6 @@ export default function IndexPage() {
await refresh();
}, [refresh]);
- function openPanelVersion() {
- setPanelUpdateOpen(true);
- }
-
async function handleChannelChange(dev: boolean) {
const res = await HttpUtil.post('/panel/api/server/setUpdateChannel', { dev });
if (!res?.success) return;
@@ -139,10 +113,6 @@ export default function IndexPage() {
if (msg?.success && msg.obj) setPanelUpdateInfo(msg.obj);
}
- function openTelegram() {
- window.open('https://t.me/XrayUI', '_blank', 'noopener,noreferrer');
- }
-
async function openConfig() {
setLoading(true);
try {
@@ -165,6 +135,23 @@ export default function IndexPage() {
}
const pageClass = `index-page ${isDark ? 'is-dark' : ''} ${isUltra ? 'is-ultra' : ''}`.trim();
+ const totalDisk = status.disk.total;
+ const freeDisk = Math.max(0, totalDisk - status.disk.current);
+
+ const health = useMemo(() => {
+ const items = [
+ { name: t('pages.index.cpu'), value: status.cpu.percent },
+ { name: t('pages.index.memory'), value: status.mem.percent },
+ { name: t('pages.index.swap'), value: status.swap.percent },
+ { name: t('pages.index.storage'), value: status.disk.percent },
+ ];
+ const list = (xs: typeof items) => xs.map((i) => `${i.name} ${i.value.toFixed(0)}%`).join(', ');
+ const crit = items.filter((i) => i.value >= USAGE_CRIT_PERCENT);
+ if (crit.length) return { text: t('pages.index.healthCritical', { list: list(crit) }), color: USAGE_CRIT_COLOR };
+ const warm = items.filter((i) => i.value >= USAGE_WARN_PERCENT);
+ if (warm.length) return { text: t('pages.index.healthWarm', { list: list(warm) }), color: USAGE_WARN_COLOR };
+ return null;
+ }, [status, t]);
return (
@@ -190,277 +177,105 @@ export default function IndexPage() {
extra={}
/>
) : (
-
-
-
-
+
+ setLogsOpen(true)}
+ onOpenXrayLogs={() => setXrayLogsOpen(true)}
+ onOpenConfig={openConfig}
+ onOpenBackup={() => setBackupOpen(true)}
+ onOpenSystemHistory={() => setSysHistoryOpen(true)}
+ onOpenXrayMetrics={() => setXrayMetricsOpen(true)}
+ onOpenPanelUpdate={() => setPanelUpdateOpen(true)}
+ onOpenVersionSwitch={() => setVersionOpen(true)}
+ />
-
-
+
+ {health.text}
+
+ )}
+
+
+
+
+
}
+ label={t('pages.index.cpu')}
+ percent={status.cpu.percent}
+ statusColor={status.cpu.color}
+ detail={`${CPUFormatter.cpuCoreFormat(status.cpuCores)} / ${status.logicalPro}T · ${CPUFormatter.cpuSpeedFormat(status.cpuSpeedMhz)}`}
+ footLeft={`${t('pages.index.avg')} ${mean(history.series.cpu).toFixed(0)}%`}
+ footRight={`${t('pages.index.peak')} ${peak(history.series.cpu).toFixed(0)}%`}
+ data={history.series.cpu}
isMobile={isMobile}
- accessLogEnable={accessLogEnable}
- onStopXray={stopXray}
- onRestartXray={restartXray}
- onOpenXrayLogs={() => setXrayLogsOpen(true)}
- onOpenLogs={() => setLogsOpen(true)}
- onOpenVersionSwitch={() => setVersionOpen(true)}
/>
-
-
-
-
setLogsOpen(true)} onKeyDown={activateOnKey(() => setLogsOpen(true))}>
-
- {!isMobile && {t('pages.index.logs')}}
- ,
-
-
- {!isMobile && {t('pages.index.config')}}
- ,
- setBackupOpen(true)} onKeyDown={activateOnKey(() => setBackupOpen(true))}>
-
- {!isMobile && {t('pages.index.backupTitle')}}
- ,
- ]}
+ }
+ label={t('pages.index.memory')}
+ percent={status.mem.percent}
+ statusColor={status.mem.color}
+ detail={`${SizeFormatter.sizeFormat(status.mem.current)} / ${SizeFormatter.sizeFormat(status.mem.total)}`}
+ footLeft={`${t('pages.index.avg')} ${mean(history.series.mem).toFixed(0)}%`}
+ footRight={`${t('pages.index.peak')} ${peak(history.series.mem).toFixed(0)}%`}
+ data={history.series.mem}
+ isMobile={isMobile}
/>
-
-
-
-
- 3X-UI
- {isMobile && displayVersion && (
-
- {panelUpdateInfo.updateAvailable
- ? formatPanelVersion(panelUpdateInfo.latestVersion)
- : formatPanelVersion(displayVersion)}
-
- )}
-
- }
- hoverable
- actions={[
-
-
- {!isMobile && @XrayUI}
- ,
-
-
- {!isMobile && (
-
- {panelUpdateInfo.updateAvailable
- ? `${t('update')} ${formatPanelVersion(panelUpdateInfo.latestVersion)}`
- : formatPanelVersion(displayVersion)}
-
- )}
- ,
- ]}
+ }
+ label={t('pages.index.swap')}
+ percent={status.swap.percent}
+ statusColor={status.swap.color}
+ detail={`${SizeFormatter.sizeFormat(status.swap.current)} / ${SizeFormatter.sizeFormat(status.swap.total)}`}
+ footLeft={`${t('pages.index.avg')} ${mean(history.series.swap).toFixed(1)}%`}
+ footRight={`${t('pages.index.peak')} ${peak(history.series.swap).toFixed(0)}%`}
+ data={history.series.swap}
+ isMobile={isMobile}
/>
-
-
-
- setSysHistoryOpen(true)}
- onKeyDown={activateOnKey(() => setSysHistoryOpen(true))}
- >
-
- {!isMobile && {t('pages.index.systemHistoryTitle')}}
- ,
- setXrayMetricsOpen(true)}
- onKeyDown={activateOnKey(() => setXrayMetricsOpen(true))}
- >
-
- {!isMobile && {t('pages.index.xrayMetricsTitle')}}
- ,
- ]}
+ }
+ label={t('pages.index.storage')}
+ percent={status.disk.percent}
+ statusColor={status.disk.color}
+ detail={`${SizeFormatter.sizeFormat(status.disk.current)} / ${SizeFormatter.sizeFormat(totalDisk)}`}
+ footLeft={`${t('pages.index.free')} ${SizeFormatter.sizeFormat(freeDisk)}`}
+ footRight={`${t('pages.index.avg')} ${mean(history.series.diskUsage).toFixed(1)}%`}
+ data={history.series.diskUsage}
+ isMobile={isMobile}
/>
-
+
-
-
-
-
- }
- />
-
-
- }
- />
-
-
-
-
+
+
+
+
-
-
-
-
- }
- />
-
-
- }
- />
-
-
-
-
-
-
-
-
-
- }
- suffix="/s"
- />
-
-
- }
- suffix="/s"
- />
-
-
-
-
-
-
-
-
-
- }
- />
-
-
- }
- />
-
-
-
-
-
-
-
- {showIp ? (
- setShowIp(false)}
- onKeyDown={activateOnKey(() => setShowIp(false))}
- />
- ) : (
- setShowIp(true)}
- onKeyDown={activateOnKey(() => setShowIp(true))}
- />
- )}
-
- }
- >
-
-
- }
- />
-
-
- }
- />
-
-
-
-
-
-
-
-
-
- }
- />
-
-
- }
- />
-
-
-
-
-
+ setShowIp((v) => !v)}
+ />
+
)}
diff --git a/frontend/src/pages/index/OverviewActionBar.tsx b/frontend/src/pages/index/OverviewActionBar.tsx
new file mode 100644
index 000000000..e5020e9bd
--- /dev/null
+++ b/frontend/src/pages/index/OverviewActionBar.tsx
@@ -0,0 +1,163 @@
+import { Fragment } from 'react';
+import type { ReactNode } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Button, Tag, Tooltip } from 'antd';
+import {
+ ArrowUpOutlined,
+ AreaChartOutlined,
+ BarsOutlined,
+ CloudDownloadOutlined,
+ CloudServerOutlined,
+ ControlOutlined,
+ FileTextOutlined,
+ PoweroffOutlined,
+ ReloadOutlined,
+} from '@ant-design/icons';
+
+import { formatPanelVersion } from '@/lib/panel-version';
+import type { Status } from '@/models/status';
+
+interface OverviewActionBarProps {
+ status: Status;
+ isMobile: boolean;
+ accessLogEnable: boolean;
+ panelVersion: string;
+ latestVersion: string;
+ updateAvailable: boolean;
+ onStopXray: () => void;
+ onRestartXray: () => void;
+ onOpenLogs: () => void;
+ onOpenXrayLogs: () => void;
+ onOpenConfig: () => void;
+ onOpenBackup: () => void;
+ onOpenSystemHistory: () => void;
+ onOpenXrayMetrics: () => void;
+ onOpenPanelUpdate: () => void;
+ onOpenVersionSwitch: () => void;
+}
+
+interface BarAction {
+ key: string;
+ icon: ReactNode;
+ text: string;
+ onClick: () => void;
+ primary?: boolean;
+}
+
+const XRAY_STATE_KEYS: Record