feat: добавлены правила маршрутизации по устройствам и управление ими через API
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 19s
All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 19s
Refs: None
This commit is contained in:
@@ -27,6 +27,7 @@ function App() {
|
||||
const [subscriptionUrl, setSubscriptionUrl] = useState('');
|
||||
const [servers, setServers] = useState([]);
|
||||
const [customRules, setCustomRules] = useState([]);
|
||||
const [deviceRules, setDeviceRules] = useState([]);
|
||||
const [selectedTag, setSelectedTag] = useState('');
|
||||
const [pendingTag, setPendingTag] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -67,6 +68,7 @@ function App() {
|
||||
setState(data);
|
||||
setServers(data.servers || []);
|
||||
if (!rulesDirtyRef.current) setCustomRules(data.customRules || []);
|
||||
setDeviceRules(data.deviceRules || []);
|
||||
setSelectedTag((prev) => prev || data.selectedTag || '');
|
||||
setPendingTag((prev) => prev || data.selectedTag || '');
|
||||
}
|
||||
@@ -193,6 +195,37 @@ function App() {
|
||||
});
|
||||
}
|
||||
|
||||
// === Device Rules ===
|
||||
async function saveDeviceRules(rules) {
|
||||
try {
|
||||
const data = await api.deviceRules.save(rules);
|
||||
setDeviceRules(data.deviceRules || rules);
|
||||
} catch (err) {
|
||||
pushToast({ kind: 'danger', title: 'Не удалось сохранить устройства', message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
function addDevice() {
|
||||
const next = [
|
||||
...deviceRules,
|
||||
{ id: `dev-${Date.now()}`, name: 'Новое устройство', enabled: true, sourceIps: [], outbound: 'direct' },
|
||||
];
|
||||
setDeviceRules(next);
|
||||
saveDeviceRules(next);
|
||||
}
|
||||
|
||||
function updateDevice(id, patch) {
|
||||
const next = deviceRules.map((d) => (d.id === id ? { ...d, ...patch } : d));
|
||||
setDeviceRules(next);
|
||||
saveDeviceRules(next);
|
||||
}
|
||||
|
||||
function removeDevice(id) {
|
||||
const next = deviceRules.filter((d) => d.id !== id);
|
||||
setDeviceRules(next);
|
||||
saveDeviceRules(next);
|
||||
}
|
||||
|
||||
// === Rules CRUD ===
|
||||
function emptyRule() {
|
||||
return {
|
||||
@@ -358,6 +391,10 @@ function App() {
|
||||
onRemove={removeRule}
|
||||
onSaveNow={saveRulesNow}
|
||||
onReorder={reorderRules}
|
||||
deviceRules={deviceRules}
|
||||
onAddDevice={addDevice}
|
||||
onUpdateDevice={updateDevice}
|
||||
onRemoveDevice={removeDevice}
|
||||
/>
|
||||
)}
|
||||
{page === 'logs' && <LogsPage />}
|
||||
|
||||
@@ -26,6 +26,15 @@ export const api = {
|
||||
conflicts: () => request("/api/rules/conflicts"),
|
||||
},
|
||||
|
||||
deviceRules: {
|
||||
get: () => request("/api/device-rules"),
|
||||
save: (deviceRules) =>
|
||||
request("/api/device-rules", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ deviceRules }),
|
||||
}),
|
||||
},
|
||||
|
||||
ruleSets: {
|
||||
get: () => request("/api/rule-sets"),
|
||||
save: (ruleSets) =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user