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:
171
src/web/components/SettingsPage.jsx
Normal file
171
src/web/components/SettingsPage.jsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { api } from '../api.js';
|
||||
import { formatRelative } from '../utils/format.js';
|
||||
|
||||
function SubscriptionCard({ state, subscriptionUrl, setSubscriptionUrl, busy, onFetch, onForget, pushToast }) {
|
||||
const [editing, setEditing] = useState(!state?.hasSubscription);
|
||||
|
||||
useEffect(() => { if (!state?.hasSubscription) setEditing(true); }, [state?.hasSubscription]);
|
||||
|
||||
const masked = state?.hasSubscription && !editing;
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h2>Подписка</h2>
|
||||
{state?.hasSubscription && (
|
||||
<span className="badge success">● активна</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{masked ? (
|
||||
<div className="kv-list">
|
||||
<div className="row">
|
||||
<span className="key">URL</span>
|
||||
<span className="val text-mono">{state.subscriptionHost}</span>
|
||||
</div>
|
||||
<div className="row">
|
||||
<span className="key">Серверов</span>
|
||||
<span className="val">{state.servers?.length || 0}</span>
|
||||
</div>
|
||||
<div className="row">
|
||||
<span className="key">Загружено</span>
|
||||
<span className="val">{state.fetchedAt ? formatRelative(state.fetchedAt) : '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="field">
|
||||
<span className="field-label">Subscription URL</span>
|
||||
<div className="subscription-input">
|
||||
<input
|
||||
className="input"
|
||||
value={subscriptionUrl}
|
||||
onChange={(e) => setSubscriptionUrl(e.target.value)}
|
||||
placeholder="https://provider.example/sub/..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="btn-group" style={{ marginTop: 16 }}>
|
||||
{masked ? (
|
||||
<>
|
||||
<button className="btn btn-secondary" onClick={() => setEditing(true)} disabled={busy}>Изменить URL</button>
|
||||
<button className="btn btn-secondary" onClick={onFetch} disabled={busy}>↻ Обновить серверы</button>
|
||||
<button className="btn btn-danger" onClick={onForget} disabled={busy}>Удалить подписку</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={async () => { await onFetch(); setEditing(false); }}
|
||||
disabled={busy || !subscriptionUrl}
|
||||
>
|
||||
{busy ? 'Загрузка…' : 'Загрузить серверы'}
|
||||
</button>
|
||||
{state?.hasSubscription && (
|
||||
<button className="btn btn-ghost" onClick={() => setEditing(false)}>Отмена</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConfigCard({ state, busy, onShowConfig, onClearConfig, pushToast }) {
|
||||
const [validation, setValidation] = useState(null);
|
||||
const [validating, setValidating] = useState(false);
|
||||
|
||||
async function validate() {
|
||||
setValidating(true);
|
||||
try {
|
||||
const data = await api.configValidate();
|
||||
setValidation(data);
|
||||
pushToast({
|
||||
kind: data.valid ? 'success' : 'danger',
|
||||
title: data.valid ? 'Config валиден' : 'Config невалиден',
|
||||
message: data.error || data.note,
|
||||
});
|
||||
} catch (err) {
|
||||
pushToast({ kind: 'danger', title: 'Ошибка проверки', message: err.message });
|
||||
} finally {
|
||||
setValidating(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h2>sing-box config</h2>
|
||||
{validation && (
|
||||
<span className={`badge ${validation.valid ? 'success' : 'danger'}`}>
|
||||
{validation.valid ? '✓ валиден' : '✗ ошибка'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="kv-list">
|
||||
<div className="row"><span className="key">Файл</span><span className="val">{state?.configExists ? 'есть' : 'нет'}</span></div>
|
||||
<div className="row"><span className="key">Применено</span><span className="val">{state?.appliedAt ? formatRelative(state.appliedAt) : '—'}</span></div>
|
||||
</div>
|
||||
<div className="btn-group" style={{ marginTop: 16 }}>
|
||||
<button className="btn btn-secondary" disabled={!state?.configExists} onClick={onShowConfig}>Показать config</button>
|
||||
<button className="btn btn-secondary" disabled={validating || !state?.configExists} onClick={validate}>
|
||||
{validating ? 'Проверяем…' : '✓ Валидировать'}
|
||||
</button>
|
||||
<button className="btn btn-danger" disabled={busy || !state?.configExists} onClick={onClearConfig}>
|
||||
Сбросить config
|
||||
</button>
|
||||
</div>
|
||||
{validation && !validation.valid && validation.error && (
|
||||
<div className="conflict-banner danger" style={{ marginTop: 12 }}>
|
||||
<span>✗</span><div>{validation.error}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PortsCard({ state }) {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header"><h2>Порты и маршруты</h2></div>
|
||||
<div className="kv-list">
|
||||
<div className="row"><span className="key">UI</span><span className="val text-mono">:{state?.port || 3456}</span></div>
|
||||
<div className="row"><span className="key">Mixed proxy (http+socks5)</span><span className="val text-mono">:{state?.proxyPort || 8080}</span></div>
|
||||
<div className="row"><span className="key">TProxy</span><span className="val text-mono">:{state?.tproxyPort || 7895}</span></div>
|
||||
<div className="row"><span className="key">RU direct (geoip-ru)</span><span className="val">{state?.routingRuDirect ? 'включено' : 'выключено'}</span></div>
|
||||
</div>
|
||||
<small className="muted" style={{ display: 'block', marginTop: 10 }}>
|
||||
Эти параметры задаются в config.js на сервере.
|
||||
</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsPage({
|
||||
state, subscriptionUrl, setSubscriptionUrl, busy,
|
||||
onFetchSubscription, onForgetSubscription, onShowConfig, onClearConfig, pushToast,
|
||||
}) {
|
||||
return (
|
||||
<div className="section-stack">
|
||||
<SubscriptionCard
|
||||
state={state}
|
||||
subscriptionUrl={subscriptionUrl}
|
||||
setSubscriptionUrl={setSubscriptionUrl}
|
||||
busy={busy}
|
||||
onFetch={onFetchSubscription}
|
||||
onForget={onForgetSubscription}
|
||||
pushToast={pushToast}
|
||||
/>
|
||||
<ConfigCard
|
||||
state={state}
|
||||
busy={busy}
|
||||
onShowConfig={onShowConfig}
|
||||
onClearConfig={onClearConfig}
|
||||
pushToast={pushToast}
|
||||
/>
|
||||
<PortsCard state={state} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user