feat: добавлены новые компоненты для управления правилами и серверами
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 25s
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 25s
- Создан компонент RuleEditorDrawer для редактирования правил с поддержкой JSON. - Добавлен компонент ServersPage для отображения и управления серверами. - Реализован компонент SettingsPage для управления подписками и конфигурациями. - Создан компонент Sidebar для навигации по приложению. - Добавлен компонент StatusPane для отображения статуса сервера. - Реализован компонент Toasts для отображения уведомлений. - Создан компонент Topbar для отображения информации о текущем состоянии. - Добавлен модуль country.js для определения страны по тегу сервера. Refs: None
This commit is contained in:
159
src/web/components/OverviewPage.jsx
Normal file
159
src/web/components/OverviewPage.jsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { formatRelative, formatBytes } from '../utils/format.js';
|
||||
import { flagFor } from '../utils/country.js';
|
||||
import { api } from '../api.js';
|
||||
|
||||
function StatusHero({ state, status }) {
|
||||
const text = {
|
||||
running: { title: '🟢 VPN-шлюз работает', kind: 'success' },
|
||||
applying: { title: '🟠 Применяем изменения…', kind: 'warning' },
|
||||
error: { title: '🔴 Ошибка', kind: 'danger' },
|
||||
stopped: { title: '⚫ Шлюз остановлен', kind: 'neutral' },
|
||||
no_config: { title: '⚪ Шлюз не настроен', kind: 'neutral' },
|
||||
}[status];
|
||||
|
||||
const userInfo = state?.userInfo;
|
||||
const traffic = userInfo
|
||||
? `${formatBytes((userInfo.upload || 0) + (userInfo.download || 0))} / ${userInfo.total ? formatBytes(userInfo.total) : 'без лимита'}`
|
||||
: 'нет данных';
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="flex-between">
|
||||
<div>
|
||||
<h2 style={{ marginBottom: 4 }}>{text.title}</h2>
|
||||
<small className="muted">
|
||||
{state?.appliedAt ? `Последнее применение: ${formatRelative(state.appliedAt)}` : 'Конфиг ещё не применялся'}
|
||||
</small>
|
||||
</div>
|
||||
<span className={`badge ${text.kind}`}>{state?.singboxRunning ? 'sing-box online' : 'sing-box offline'}</span>
|
||||
</div>
|
||||
|
||||
<div className="divider" />
|
||||
|
||||
<div className="grid-3">
|
||||
<div>
|
||||
<small className="muted">Активный сервер</small>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{state?.selectedTag ? (
|
||||
<>
|
||||
<strong>{flagFor({ tag: state.selectedTag })} {state.selectedTag}</strong>
|
||||
</>
|
||||
) : <span className="muted">Не выбран</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<small className="muted">Трафик</small>
|
||||
<div style={{ marginTop: 4 }}><strong>{traffic}</strong></div>
|
||||
</div>
|
||||
<div>
|
||||
<small className="muted">Правил маршрутизации</small>
|
||||
<div style={{ marginTop: 4 }}><strong>{(state?.customRules || []).filter(r => r.enabled).length} активных</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickActions({ state, busy, onRestart, onStop, onShowConfig, onNav }) {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3>Быстрые действия</h3>
|
||||
</div>
|
||||
<div className="btn-group">
|
||||
<button className="btn btn-primary" disabled={busy} onClick={() => onNav('servers')}>
|
||||
⋆ Сменить сервер
|
||||
</button>
|
||||
<button className="btn btn-secondary" disabled={busy || !state?.configExists} onClick={onRestart}>
|
||||
↻ Перезапустить
|
||||
</button>
|
||||
<button className="btn btn-secondary" disabled={busy || !state?.singboxRunning} onClick={onStop}>
|
||||
■ Остановить
|
||||
</button>
|
||||
<button className="btn btn-ghost" disabled={!state?.configExists} onClick={onShowConfig}>
|
||||
⌘ Показать config
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RecentEvents({ onNav }) {
|
||||
const [entries, setEntries] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetch('/api/logs')
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (cancelled) return;
|
||||
const list = (data.logs || []).slice(-15).reverse();
|
||||
setEntries(list);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3>Последние события</h3>
|
||||
<button className="btn btn-link" onClick={() => onNav('logs')}>Открыть логи →</button>
|
||||
</div>
|
||||
{entries.length === 0 ? (
|
||||
<small className="muted">Пока ничего нет.</small>
|
||||
) : (
|
||||
<div className="events-list">
|
||||
{entries.slice(0, 8).map((e, i) => {
|
||||
const dot = e.level === 'error' ? 'danger'
|
||||
: e.level === 'warning' ? 'warning'
|
||||
: 'success';
|
||||
const time = new Date(e.ts).toLocaleTimeString('ru-RU', { hour12: false });
|
||||
return (
|
||||
<div key={`${e.ts}-${i}`} className="event-row">
|
||||
<span className={`dot ${dot}`} />
|
||||
<span className="event-time">{time}</span>
|
||||
<span className="text-truncate" title={e.line}>{e.line}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RoutingSummary({ state, onNav }) {
|
||||
const rules = state?.customRules || [];
|
||||
const enabled = rules.filter((r) => r.enabled).length;
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3>Маршрутизация</h3>
|
||||
<button className="btn btn-link" onClick={() => onNav('routing')}>Открыть правила →</button>
|
||||
</div>
|
||||
<div className="kv-list">
|
||||
<div className="row"><span className="key">Private IP</span><span className="val text-success">→ direct</span></div>
|
||||
{state?.routingRuDirect && (
|
||||
<div className="row"><span className="key">RU (geoip/geosite)</span><span className="val text-success">→ direct</span></div>
|
||||
)}
|
||||
<div className="row"><span className="key">Custom правил</span><span className="val">{enabled} из {rules.length}</span></div>
|
||||
<div className="row"><span className="key">Остальное</span><span className="val text-warning">→ VPN</span></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OverviewPage({ state, status, busy, onRestart, onStop, onShowConfig, onNav }) {
|
||||
return (
|
||||
<div className="section-stack">
|
||||
<StatusHero state={state} status={status} />
|
||||
<div className="grid-2">
|
||||
<QuickActions state={state} busy={busy} onRestart={onRestart} onStop={onStop} onShowConfig={onShowConfig} onNav={onNav} />
|
||||
<RoutingSummary state={state} onNav={onNav} />
|
||||
</div>
|
||||
<RecentEvents onNav={onNav} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user