feat(GatewayAutocomplete): add selected gateway info and free gateway warning to enhance user feedback
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 3m32s

This commit is contained in:
2026-02-10 18:33:53 +07:00
parent 4e2f4ed7a5
commit 56c181f697
+89 -85
View File
@@ -428,6 +428,37 @@ function GatewayAutocomplete({ label, value, onChange, gateways = [], required =
const selectedGateway = gateways.find(g => g.name === value); const selectedGateway = gateways.find(g => g.name === value);
const selectedGatewayInfo =
value && selectedGateway && !isOpen ? (
<div className="mt-2">
<div className="d-flex align-items-center gap-2 p-2 bg-light rounded">
<span className="avatar avatar-xs bg-primary-lt">
{selectedGateway.country ? countryToFlag(selectedGateway.country) : <IconServer size={14} />}
</span>
<div className="flex-grow-1">
<div className="fw-bold">
<code>{selectedGateway.name}</code>
{selectedGateway.comment && <span className="text-muted ms-2">({selectedGateway.comment})</span>}
</div>
<div className="text-muted small">
{[selectedGateway.serverDns].filter(Boolean).join(' · ')}
</div>
</div>
{selectedGateway.primary && <span className="badge bg-green-lt text-green">Основной</span>}
</div>
</div>
) : null;
const freeGatewayWarning =
value && !selectedGateway && !isOpen ? (
<div className="mt-2">
<div className="d-flex align-items-center gap-2 p-2 bg-warning-lt rounded">
<IconAlertTriangle size={16} className="text-warning" />
<div className="text-muted small">Gateway "{value}" — свободный ввод</div>
</div>
</div>
) : null;
return ( return (
<div className="mb-3" ref={containerRef}> <div className="mb-3" ref={containerRef}>
<label className={`form-label ${required ? 'required' : ''}`}>{label}</label> <label className={`form-label ${required ? 'required' : ''}`}>{label}</label>
@@ -454,99 +485,72 @@ function GatewayAutocomplete({ label, value, onChange, gateways = [], required =
{/* Карточки gateway под полем, ВСЕГДА видимы */} {/* Карточки gateway под полем, ВСЕГДА видимы */}
<div className="mt-1"> <div className="mt-1">
{gateways.length === 0 ? ( {gateways.length === 0 ? (
<div className="text-muted small d-flex align-items-center"> <div className="text-muted small d-flex align-items-center">
<IconAlertTriangle size={14} className="me-2 text-warning" /> <IconAlertTriangle size={14} className="me-2 text-warning" />
<span>Нет доступных gateway для выбранного сервера</span> <span>Нет доступных gateway для выбранного сервера</span>
</div> </div>
) : filteredGateways.length === 0 ? ( ) : filteredGateways.length === 0 ? (
<div className="text-muted small"> <div className="text-muted small">
Ничего не найдено. Уточните запрос по IP, имени или описанию. Ничего не найдено. Уточните запрос по IP, имени или описанию.
</div> </div>
) : ( ) : (
<div className="row row-cols-1 row-cols-md-3 g-2"> <div className="row row-cols-1 row-cols-md-3 g-2">
{filteredGateways.map((gw, idx) => { {filteredGateways.map((gw, idx) => {
const isSelected = gw.name === value; const isSelected = gw.name === value;
const isHighlighted = highlightedIndex === idx; const isHighlighted = highlightedIndex === idx;
return ( return (
<div className="col" key={gw.name || idx}> <div className="col" key={gw.name || idx}>
<button <button
type="button" type="button"
className="w-100 border-0 bg-transparent p-0 text-start" className="w-100 border-0 bg-transparent p-0 text-start"
onClick={() => handleSelect(gw)} onClick={() => handleSelect(gw)}
onMouseEnter={() => setHighlightedIndex(idx)} onMouseEnter={() => setHighlightedIndex(idx)}
>
<div
className={
'card h-100 gateway-card ' +
(isSelected ? 'bg-primary text-white border-primary shadow-sm ' : 'bg-white ') +
(!isSelected && isHighlighted ? 'border-primary ' : '')
}
style={{ borderRadius: '10px', transition: 'all 0.12s ease' }}
> >
<div <div className="card-body py-2 px-3">
className={ <div className="d-flex align-items-start gap-2 mb-1">
'card h-100 gateway-card ' + <span className={`avatar avatar-sm ${isSelected ? 'bg-white text-primary' : 'bg-primary-lt'}`}>
(isSelected ? 'bg-primary text-white border-primary shadow-sm ' : 'bg-white ') + {gw.country ? countryToFlag(gw.country) : <IconServer size={16} />}
(!isSelected && isHighlighted ? 'border-primary ' : '') </span>
} <div className="flex-grow-1 min-w-0">
style={{ borderRadius: '10px', transition: 'all 0.12s ease' }} <div className="fw-bold text-truncate">
> {gw.ip || gw.name}
<div className="card-body py-2 px-3">
<div className="d-flex align-items-start gap-2 mb-1">
<span className={`avatar avatar-sm ${isSelected ? 'bg-white text-primary' : 'bg-primary-lt'}`}>
{gw.country ? countryToFlag(gw.country) : <IconServer size={16} />}
</span>
<div className="flex-grow-1 min-w-0">
<div className="fw-bold text-truncate">
{gw.ip || gw.name}
</div>
<div className={`small text-truncate ${isSelected ? 'text-white-75' : 'text-muted'}`}>
{gw.comment || gw.name}
</div>
{gw.serverDns && (
<div className={`small text-truncate ${isSelected ? 'text-white-75' : 'text-muted'}`}>
{gw.serverDns}
</div>
)}
</div> </div>
{gw.primary && ( <div className={`small text-truncate ${isSelected ? 'text-white-75' : 'text-muted'}`}>
<span className={`badge ${isSelected ? 'bg-white text-primary' : 'bg-green-lt text-green'}`}> {gw.comment || gw.name}
Основной </div>
</span> {gw.serverDns && (
<div className={`small text-truncate ${isSelected ? 'text-white-75' : 'text-muted'}`}>
{gw.serverDns}
</div>
)} )}
</div> </div>
{gw.primary && (
<span className={`badge ${isSelected ? 'bg-white text-primary' : 'bg-green-lt text-green'}`}>
Основной
</span>
)}
</div> </div>
</div> </div>
</button> </div>
</div> </button>
); </div>
})} );
</div> })}
)} </div>
</div> )}
</div> </div>
{value && selectedGateway && !isOpen && (
<div className="mt-2">
<div className="d-flex align-items-center gap-2 p-2 bg-light rounded">
<span className="avatar avatar-xs bg-primary-lt">
{selectedGateway.country ? countryToFlag(selectedGateway.country) : <IconServer size={14} />}
</span>
<div className="flex-grow-1">
<div className="fw-bold">
<code>{selectedGateway.name}</code>
{selectedGateway.comment && <span className="text-muted ms-2">({selectedGateway.comment})</span>}
</div>
<div className="text-muted small">
{[selectedGateway.serverDns].filter(Boolean).join(' · ')}
</div>
</div>
{selectedGateway.primary && <span className="badge bg-green-lt text-green">Основной</span>}
</div>
</div>
)}
{/* Предупреждение если gateway не в списке */} {selectedGatewayInfo}
{value && !selectedGateway && !isOpen && ( {freeGatewayWarning}
<div className="mt-2">
<div className="d-flex align-items-center gap-2 p-2 bg-warning-lt rounded">
<IconAlertTriangle size={16} className="text-warning" />
<div className="text-muted small">Gateway "{value}" — свободный ввод</div>
</div>
</div>
)}
</div> </div>
); );
} }