feat: Integrate community suggestions in ASNs, Domains, and IPRanges managers for improved user input experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m14s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m14s
This commit is contained in:
@@ -35,6 +35,19 @@ function ASNsNewManager() {
|
||||
const editInputRef = useRef(null);
|
||||
const pageSize = 10;
|
||||
|
||||
// Справочник community для подсказок
|
||||
const [communities, setCommunities] = useState([]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await axios.get(`${API_URL}/communities`);
|
||||
setCommunities(Array.isArray(res.data) ? res.data : []);
|
||||
} catch (e) {
|
||||
// тихо игнорируем
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const isValidAsn = (value) => /^[0-9]+$/.test(String(value).trim());
|
||||
// Допускаем как числовые, так и строковые (AS:NNN) community
|
||||
const isValidCommunity = (value) => /^(\d+|\d+:\d+)$/.test(String(value).trim());
|
||||
@@ -262,6 +275,7 @@ function ASNsNewManager() {
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Community</label>
|
||||
<input
|
||||
list="community-options"
|
||||
type="text"
|
||||
className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
|
||||
placeholder="112"
|
||||
@@ -269,6 +283,19 @@ function ASNsNewManager() {
|
||||
onChange={(e) => setNewItem({ ...newItem, community: e.target.value })}
|
||||
/>
|
||||
{newInvalid.community && <div className="invalid-feedback">Только цифры</div>}
|
||||
{(() => {
|
||||
const match = communities.find(c => c.value === String(newItem.community).trim());
|
||||
if (!match) return null;
|
||||
return (
|
||||
<div className="form-text">
|
||||
{match.name && (<><strong>{match.name}</strong> — </>)}
|
||||
{match.description || ''}
|
||||
{match.tags && match.tags.length > 0 && (
|
||||
<> (<span className="text-muted">{match.tags.join(', ')}</span>)</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="form-footer">
|
||||
<button type="submit" className="btn btn-primary w-100">
|
||||
@@ -401,6 +428,7 @@ function ASNsNewManager() {
|
||||
{editingAsn === item.asn ? (
|
||||
<>
|
||||
<input
|
||||
list="community-options"
|
||||
type="text"
|
||||
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
|
||||
value={editingValue}
|
||||
@@ -513,6 +541,12 @@ function ASNsNewManager() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Общий список подсказок community */}
|
||||
<datalist id="community-options">
|
||||
{communities.map(c => (
|
||||
<option key={c.value} value={c.value}>{c.name ? `${c.name} — ${c.value}` : c.value}</option>
|
||||
))}
|
||||
</datalist>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,19 @@ function DomainsNewManager() {
|
||||
const editInputRef = useRef(null);
|
||||
const pageSize = 10;
|
||||
|
||||
// Справочник community для подсказок
|
||||
const [communities, setCommunities] = useState([]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await axios.get(`${API_URL}/communities`);
|
||||
setCommunities(Array.isArray(res.data) ? res.data : []);
|
||||
} catch (e) {
|
||||
// тихо игнорируем
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const isValidDomain = (value) => {
|
||||
const v = String(value).trim().toLowerCase();
|
||||
// Простой валидатор FQDN: sub.domain.tld
|
||||
@@ -268,6 +281,7 @@ function DomainsNewManager() {
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Community</label>
|
||||
<input
|
||||
list="community-options"
|
||||
type="text"
|
||||
className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
|
||||
placeholder="100"
|
||||
@@ -275,6 +289,19 @@ function DomainsNewManager() {
|
||||
onChange={(e) => setNewItem({ ...newItem, community: e.target.value })}
|
||||
/>
|
||||
{newInvalid.community && <div className="invalid-feedback">Только цифры</div>}
|
||||
{(() => {
|
||||
const match = communities.find(c => c.value === String(newItem.community).trim());
|
||||
if (!match) return null;
|
||||
return (
|
||||
<div className="form-text">
|
||||
{match.name && (<><strong>{match.name}</strong> — </>)}
|
||||
{match.description || ''}
|
||||
{match.tags && match.tags.length > 0 && (
|
||||
<> (<span className="text-muted">{match.tags.join(', ')}</span>)</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="form-footer">
|
||||
<button type="submit" className="btn btn-primary w-100">
|
||||
@@ -407,6 +434,7 @@ function DomainsNewManager() {
|
||||
{editingDomain === item.domain ? (
|
||||
<>
|
||||
<input
|
||||
list="community-options"
|
||||
type="text"
|
||||
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
|
||||
value={editingValue}
|
||||
@@ -519,6 +547,12 @@ function DomainsNewManager() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Общий список подсказок community */}
|
||||
<datalist id="community-options">
|
||||
{communities.map(c => (
|
||||
<option key={c.value} value={c.value}>{c.name ? `${c.name} — ${c.value}` : c.value}</option>
|
||||
))}
|
||||
</datalist>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,19 @@ function IPRangesManager() {
|
||||
const editInputRef = useRef(null);
|
||||
const pageSize = 10;
|
||||
|
||||
// Справочник community для подсказок
|
||||
const [communities, setCommunities] = useState([]);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await axios.get(`${API_URL}/communities`);
|
||||
setCommunities(Array.isArray(res.data) ? res.data : []);
|
||||
} catch (e) {
|
||||
// тихо игнорируем
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const isValidIPv4 = (ip) => {
|
||||
const octets = ip.split('.');
|
||||
if (octets.length !== 4) return false;
|
||||
@@ -277,6 +290,7 @@ function IPRangesManager() {
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Community</label>
|
||||
<input
|
||||
list="community-options"
|
||||
type="text"
|
||||
className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
|
||||
placeholder="65001:100"
|
||||
@@ -284,6 +298,19 @@ function IPRangesManager() {
|
||||
onChange={(e) => setNewItem({ ...newItem, community: e.target.value })}
|
||||
/>
|
||||
{newInvalid.community && <div className="invalid-feedback">Только цифры</div>}
|
||||
{(() => {
|
||||
const match = communities.find(c => c.value === String(newItem.community).trim());
|
||||
if (!match) return null;
|
||||
return (
|
||||
<div className="form-text">
|
||||
{match.name && (<><strong>{match.name}</strong> — </>)}
|
||||
{match.description || ''}
|
||||
{match.tags && match.tags.length > 0 && (
|
||||
<> (<span className="text-muted">{match.tags.join(', ')}</span>)</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="form-footer">
|
||||
<button type="submit" className="btn btn-primary w-100">
|
||||
@@ -416,6 +443,7 @@ function IPRangesManager() {
|
||||
{editingIp === item.ipRange ? (
|
||||
<>
|
||||
<input
|
||||
list="community-options"
|
||||
type="text"
|
||||
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
|
||||
value={editingValue}
|
||||
@@ -533,6 +561,12 @@ function IPRangesManager() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Общий список подсказок community */}
|
||||
<datalist id="community-options">
|
||||
{communities.map(c => (
|
||||
<option key={c.value} value={c.value}>{c.name ? `${c.name} — ${c.value}` : c.value}</option>
|
||||
))}
|
||||
</datalist>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user