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:
61
src/web/components/ChipsInput.jsx
Normal file
61
src/web/components/ChipsInput.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
/**
|
||||
* Chip input. Items separated by Enter, comma, или space (для CIDR/портов).
|
||||
* Невалидные элементы помечаются красным.
|
||||
*/
|
||||
export function ChipsInput({ value = [], onChange, placeholder = '', validate, splitter = /[\s,]/ }) {
|
||||
const [draft, setDraft] = useState('');
|
||||
|
||||
function commit(text) {
|
||||
const parts = String(text).split(splitter).map((p) => p.trim()).filter(Boolean);
|
||||
if (!parts.length) return;
|
||||
const next = Array.from(new Set([...value, ...parts]));
|
||||
onChange(next);
|
||||
setDraft('');
|
||||
}
|
||||
|
||||
function remove(item) {
|
||||
onChange(value.filter((v) => v !== item));
|
||||
}
|
||||
|
||||
function onKeyDown(e) {
|
||||
if (e.key === 'Enter' || e.key === ',') {
|
||||
e.preventDefault();
|
||||
if (draft.trim()) commit(draft);
|
||||
} else if (e.key === 'Backspace' && !draft && value.length) {
|
||||
onChange(value.slice(0, -1));
|
||||
}
|
||||
}
|
||||
|
||||
function onPaste(e) {
|
||||
const text = e.clipboardData.getData('text');
|
||||
if (text && splitter.test(text)) {
|
||||
e.preventDefault();
|
||||
commit(text);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chips" onClick={(e) => e.currentTarget.querySelector('input')?.focus()}>
|
||||
{value.map((item) => {
|
||||
const invalid = validate ? !validate(item) : false;
|
||||
return (
|
||||
<span key={item} className={`chip ${invalid ? 'error' : ''}`}>
|
||||
{item}
|
||||
<button type="button" onClick={() => remove(item)} title="Убрать">×</button>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
<input
|
||||
className="chip-input"
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
onPaste={onPaste}
|
||||
onBlur={() => draft.trim() && commit(draft)}
|
||||
placeholder={value.length ? '' : placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user