feat: добавлены правила маршрутизации по устройствам и управление ими через API
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 19s

Refs: None
This commit is contained in:
2026-05-09 09:12:03 +03:00
parent b3fad00f80
commit 4bb8507e3f
7 changed files with 506 additions and 42 deletions

View File

@@ -18,6 +18,103 @@ const OUTBOUND_KIND = {
block: { kind: 'danger', label: 'block' },
};
function DevicesCard({ devices, onAdd, onUpdate, onRemove }) {
return (
<div className="card">
<div className="card-header">
<h2>Маршрутизация по устройствам</h2>
<button className="btn btn-primary sm" onClick={onAdd}>
+ Добавить устройство
</button>
</div>
<small className="muted" style={{ display: 'block', marginBottom: 8 }}>
Правила по source IP выполняются <strong>до</strong> правил маршрутизации.
Укажи IP устройства в сети и куда направлять весь его трафик.
</small>
{devices.length === 0 ? (
<div className="empty-state" style={{ padding: '16px 0' }}>
<p style={{ margin: 0 }}>Нет правил по устройствам все используют общую маршрутизацию.</p>
</div>
) : (
<div style={{ overflowX: 'auto' }}>
<table className="table">
<thead>
<tr>
<th style={{ width: 40 }}></th>
<th>Название</th>
<th>IP-адрес(а) устройства</th>
<th style={{ width: 130 }}>Маршрут</th>
<th style={{ width: 40 }}></th>
</tr>
</thead>
<tbody>
{devices.map((dev) => {
const ob = OUTBOUND_KIND[dev.outbound] || OUTBOUND_KIND.direct;
return (
<tr key={dev.id} className={dev.enabled !== false ? '' : 'disabled'}>
<td>
<input
type="checkbox"
checked={dev.enabled !== false}
onChange={(e) => onUpdate(dev.id, { enabled: e.target.checked })}
style={{ accentColor: 'var(--accent)' }}
/>
</td>
<td>
<input
className="input sm"
value={dev.name || ''}
onChange={(e) => onUpdate(dev.id, { name: e.target.value })}
placeholder="Название устройства"
style={{ width: '100%', minWidth: 120 }}
/>
</td>
<td>
<input
className="input sm"
value={(dev.sourceIps || []).join(', ')}
onChange={(e) =>
onUpdate(dev.id, {
sourceIps: e.target.value
.split(',')
.map((s) => s.trim())
.filter(Boolean),
})
}
placeholder="192.168.1.100"
style={{ width: '100%', minWidth: 160 }}
/>
</td>
<td>
<select
className="select sm"
value={dev.outbound || 'direct'}
onChange={(e) => onUpdate(dev.id, { outbound: e.target.value })}
>
<option value="direct">direct</option>
<option value="vpn">VPN</option>
<option value="block">block</option>
</select>
</td>
<td>
<button
className="btn btn-ghost sm"
onClick={() => {
if (confirm('Удалить устройство?')) onRemove(dev.id);
}}
>×</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
);
}
function summary(rule) {
const parts = [];
const totalDomains = (rule.domains?.length || 0) + (rule.domainSuffixes?.length || 0) + (rule.domainKeywords?.length || 0);
@@ -98,6 +195,7 @@ function TemplatesModal({ open, onClose, onAdd }) {
export function RoutingPage({
rules, saveStatus, busy,
onAdd, onAddTemplate, onUpdate, onRemove, onSaveNow, onReorder,
deviceRules = [], onAddDevice, onUpdateDevice, onRemoveDevice,
}) {
const [editingId, setEditingId] = useState(null);
const [showTemplates, setShowTemplates] = useState(false);
@@ -141,6 +239,13 @@ export function RoutingPage({
<div className="section-stack">
<RouteChecker />
<DevicesCard
devices={deviceRules}
onAdd={onAddDevice}
onUpdate={onUpdateDevice}
onRemove={onRemoveDevice}
/>
<div className="card">
<div className="card-header">
<h2>Правила маршрутизации</h2>