feat: добавлены компоненты для управления конфигурацией и логами

Добавлены новые компоненты для отображения и управления конфигурацией, логами и правилами маршрутизации. Реализована логика для работы с API, включая запросы на получение и сохранение данных. Также добавлены шаблоны правил и утилиты для валидации.

Refs: None
This commit is contained in:
2026-05-08 18:23:29 +03:00
parent 7d41dd86e7
commit 8789496ae6
24 changed files with 2987 additions and 364 deletions

View File

@@ -0,0 +1,127 @@
import React from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { ruleErrors, hasErrors } from '../utils/validation.js';
function listToText(value) {
return Array.isArray(value) ? value.join('\n') : '';
}
function textToList(value) {
return value
.split(/\r?\n|,/)
.map((item) => item.trim())
.filter(Boolean);
}
export function RuleCard({ rule, index, total, onUpdate, onRemove }) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: rule.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.6 : 1,
};
const errors = ruleErrors(rule);
const errored = hasErrors(errors);
return (
<article ref={setNodeRef} style={style} className={errored ? 'rule-card invalid' : 'rule-card'}>
<div className="rule-top">
<span className="drag-handle" {...attributes} {...listeners} title="Перетащить">
#{index + 1}/{total}
</span>
<input
value={rule.name}
onChange={(event) => onUpdate(rule.id, { name: event.target.value })}
placeholder="Название списка"
/>
<label className="checkbox-label">
<input
type="checkbox"
checked={rule.enabled}
onChange={(event) => onUpdate(rule.id, { enabled: event.target.checked })}
/>
включено
</label>
</div>
<label className="field">
<span>Outbound</span>
<select value={rule.outbound} onChange={(event) => onUpdate(rule.id, { outbound: event.target.value })}>
<option value="direct">direct (напрямую)</option>
<option value="vpn">vpn (через выбранный сервер)</option>
<option value="block">block (заблокировать)</option>
</select>
</label>
<div className="rule-fields">
<label className={errors.domains.length ? 'field has-error' : 'field'}>
<span>Домены (точное совпадение)</span>
<textarea
value={listToText(rule.domains)}
onChange={(event) => onUpdate(rule.id, { domains: textToList(event.target.value) })}
placeholder="riotgames.com"
/>
{errors.domains.length > 0 && <small className="error">Невалидно: {errors.domains.join(', ')}</small>}
</label>
<label className={errors.domainSuffixes.length ? 'field has-error' : 'field'}>
<span>Суффиксы доменов</span>
<textarea
value={listToText(rule.domainSuffixes)}
onChange={(event) => onUpdate(rule.id, { domainSuffixes: textToList(event.target.value) })}
placeholder={'leagueoflegends.com\nriotcdn.net'}
/>
{errors.domainSuffixes.length > 0 && <small className="error">Невалидно: {errors.domainSuffixes.join(', ')}</small>}
</label>
<label className={errors.ipCidrs.length ? 'field has-error' : 'field'}>
<span>IP CIDR</span>
<textarea
value={listToText(rule.ipCidrs)}
onChange={(event) => onUpdate(rule.id, { ipCidrs: textToList(event.target.value) })}
placeholder="104.160.128.0/19"
/>
{errors.ipCidrs.length > 0 && <small className="error">Невалидно: {errors.ipCidrs.join(', ')}</small>}
</label>
<label className={errors.ports.length ? 'field has-error' : 'field'}>
<span>Порты</span>
<textarea
value={listToText(rule.ports)}
onChange={(event) => onUpdate(rule.id, { ports: textToList(event.target.value) })}
placeholder={'5000\n5223'}
/>
{errors.ports.length > 0 && <small className="error">Невалидно: {errors.ports.join(', ')}</small>}
</label>
</div>
<div className="rule-footer">
<label className="checkbox-label">
<input
type="checkbox"
checked={(rule.networks || []).includes('tcp')}
onChange={(event) => {
const set = new Set(rule.networks || []);
event.target.checked ? set.add('tcp') : set.delete('tcp');
onUpdate(rule.id, { networks: Array.from(set) });
}}
/>
tcp
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={(rule.networks || []).includes('udp')}
onChange={(event) => {
const set = new Set(rule.networks || []);
event.target.checked ? set.add('udp') : set.delete('udp');
onUpdate(rule.id, { networks: Array.from(set) });
}}
/>
udp
</label>
<button className="danger-button" type="button" onClick={() => onRemove(rule.id)}>
Удалить
</button>
</div>
</article>
);
}