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

This commit is contained in:
2025-08-10 17:31:13 +07:00
parent 76dee39843
commit e5f8544c2e
3 changed files with 102 additions and 0 deletions
+34
View File
@@ -35,6 +35,19 @@ function ASNsNewManager() {
const editInputRef = useRef(null); const editInputRef = useRef(null);
const pageSize = 10; 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()); const isValidAsn = (value) => /^[0-9]+$/.test(String(value).trim());
// Допускаем как числовые, так и строковые (AS:NNN) community // Допускаем как числовые, так и строковые (AS:NNN) community
const isValidCommunity = (value) => /^(\d+|\d+:\d+)$/.test(String(value).trim()); const isValidCommunity = (value) => /^(\d+|\d+:\d+)$/.test(String(value).trim());
@@ -262,6 +275,7 @@ function ASNsNewManager() {
<div className="mb-3"> <div className="mb-3">
<label className="form-label">Community</label> <label className="form-label">Community</label>
<input <input
list="community-options"
type="text" type="text"
className={`form-control${newInvalid.community ? ' is-invalid' : ''}`} className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
placeholder="112" placeholder="112"
@@ -269,6 +283,19 @@ function ASNsNewManager() {
onChange={(e) => setNewItem({ ...newItem, community: e.target.value })} onChange={(e) => setNewItem({ ...newItem, community: e.target.value })}
/> />
{newInvalid.community && <div className="invalid-feedback">Только цифры</div>} {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>
<div className="form-footer"> <div className="form-footer">
<button type="submit" className="btn btn-primary w-100"> <button type="submit" className="btn btn-primary w-100">
@@ -401,6 +428,7 @@ function ASNsNewManager() {
{editingAsn === item.asn ? ( {editingAsn === item.asn ? (
<> <>
<input <input
list="community-options"
type="text" type="text"
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
value={editingValue} value={editingValue}
@@ -513,6 +541,12 @@ function ASNsNewManager() {
</div> </div>
</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>
</> </>
); );
} }
+34
View File
@@ -35,6 +35,19 @@ function DomainsNewManager() {
const editInputRef = useRef(null); const editInputRef = useRef(null);
const pageSize = 10; 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 isValidDomain = (value) => {
const v = String(value).trim().toLowerCase(); const v = String(value).trim().toLowerCase();
// Простой валидатор FQDN: sub.domain.tld // Простой валидатор FQDN: sub.domain.tld
@@ -268,6 +281,7 @@ function DomainsNewManager() {
<div className="mb-3"> <div className="mb-3">
<label className="form-label">Community</label> <label className="form-label">Community</label>
<input <input
list="community-options"
type="text" type="text"
className={`form-control${newInvalid.community ? ' is-invalid' : ''}`} className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
placeholder="100" placeholder="100"
@@ -275,6 +289,19 @@ function DomainsNewManager() {
onChange={(e) => setNewItem({ ...newItem, community: e.target.value })} onChange={(e) => setNewItem({ ...newItem, community: e.target.value })}
/> />
{newInvalid.community && <div className="invalid-feedback">Только цифры</div>} {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>
<div className="form-footer"> <div className="form-footer">
<button type="submit" className="btn btn-primary w-100"> <button type="submit" className="btn btn-primary w-100">
@@ -407,6 +434,7 @@ function DomainsNewManager() {
{editingDomain === item.domain ? ( {editingDomain === item.domain ? (
<> <>
<input <input
list="community-options"
type="text" type="text"
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
value={editingValue} value={editingValue}
@@ -519,6 +547,12 @@ function DomainsNewManager() {
</div> </div>
</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>
</> </>
); );
} }
+34
View File
@@ -35,6 +35,19 @@ function IPRangesManager() {
const editInputRef = useRef(null); const editInputRef = useRef(null);
const pageSize = 10; 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 isValidIPv4 = (ip) => {
const octets = ip.split('.'); const octets = ip.split('.');
if (octets.length !== 4) return false; if (octets.length !== 4) return false;
@@ -277,6 +290,7 @@ function IPRangesManager() {
<div className="mb-3"> <div className="mb-3">
<label className="form-label">Community</label> <label className="form-label">Community</label>
<input <input
list="community-options"
type="text" type="text"
className={`form-control${newInvalid.community ? ' is-invalid' : ''}`} className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
placeholder="65001:100" placeholder="65001:100"
@@ -284,6 +298,19 @@ function IPRangesManager() {
onChange={(e) => setNewItem({ ...newItem, community: e.target.value })} onChange={(e) => setNewItem({ ...newItem, community: e.target.value })}
/> />
{newInvalid.community && <div className="invalid-feedback">Только цифры</div>} {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>
<div className="form-footer"> <div className="form-footer">
<button type="submit" className="btn btn-primary w-100"> <button type="submit" className="btn btn-primary w-100">
@@ -416,6 +443,7 @@ function IPRangesManager() {
{editingIp === item.ipRange ? ( {editingIp === item.ipRange ? (
<> <>
<input <input
list="community-options"
type="text" type="text"
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`} className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
value={editingValue} value={editingValue}
@@ -533,6 +561,12 @@ function IPRangesManager() {
</div> </div>
</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>
</> </>
); );
} }