Добавлены новые компоненты для отображения и управления конфигурацией, логами и правилами маршрутизации. Реализована логика для работы с API, включая запросы на получение и сохранение данных. Также добавлены шаблоны правил и утилиты для валидации. Refs: None
128 lines
5.0 KiB
JavaScript
128 lines
5.0 KiB
JavaScript
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>
|
|
);
|
|
}
|