feat(GatewayAutocomplete): enhance UI layout and improve gateway card display for better user interaction
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 2m19s

This commit is contained in:
2026-02-10 18:22:34 +07:00
parent 5e0397f667
commit 5dfe76f1df
+29 -36
View File
@@ -431,7 +431,7 @@ function GatewayAutocomplete({ label, value, onChange, gateways = [], required =
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>
<div className="position-relative"> <div className="position-relative mb-2">
<div className="input-icon"> <div className="input-icon">
<span className="input-icon-addon"> <span className="input-icon-addon">
<IconServer size={16} /> <IconServer size={16} />
@@ -443,24 +443,17 @@ function GatewayAutocomplete({ label, value, onChange, gateways = [], required =
value={inputValue} value={inputValue}
onChange={handleInputChange} onChange={handleInputChange}
onFocus={() => setIsOpen(true)} onFocus={() => setIsOpen(true)}
onBlur={() => setIsOpen(false)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={placeholder} placeholder={placeholder}
required={required && !value} required={required && !value}
autoComplete="off" autoComplete="off"
/> />
<span
className="input-icon-addon cursor-pointer"
style={{ right: 0, left: 'auto' }}
onClick={() => setIsOpen(!isOpen)}
>
<IconChevronDown size={16} className={isOpen ? 'rotate-180' : ''} style={{ transition: 'transform 0.2s' }} />
</span>
</div> </div>
</div> </div>
{/* Карточки gateway вместо выпадающего списка */} {/* Карточки gateway под полем, ВСЕГДА видимы */}
{isOpen && ( <div className="mt-1">
<div className="mt-3">
{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" />
@@ -471,7 +464,7 @@ function GatewayAutocomplete({ label, value, onChange, gateways = [], required =
Ничего не найдено. Уточните запрос по IP, имени или описанию. Ничего не найдено. Уточните запрос по IP, имени или описанию.
</div> </div>
) : ( ) : (
<div className="row row-cols-1 row-cols-md-2 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;
@@ -486,35 +479,35 @@ function GatewayAutocomplete({ label, value, onChange, gateways = [], required =
<div <div
className={ className={
'card h-100 gateway-card ' + 'card h-100 gateway-card ' +
(isSelected ? 'border-primary shadow-sm ' : '') + (isSelected ? 'bg-primary text-white border-primary shadow-sm ' : 'bg-white ') +
(isHighlighted ? 'border-2 ' : '') (!isSelected && isHighlighted ? 'border-primary ' : '')
} }
style={{ borderRadius: '10px', transition: 'all 0.12s ease' }} style={{ borderRadius: '10px', transition: 'all 0.12s ease' }}
> >
<div className="card-body py-2 px-3 d-flex align-items-start gap-2"> <div className="card-body py-2 px-3">
<span className="avatar avatar-sm bg-primary-lt"> <div className="d-flex align-items-start gap-2 mb-1">
{gw.country ? countryToFlag(gw.country) : <IconServer size={16} />} <span className={`avatar avatar-sm ${isSelected ? 'bg-white text-primary' : 'bg-primary-lt'}`}>
</span> {gw.country ? countryToFlag(gw.country) : <IconServer size={16} />}
<div className="flex-grow-1 min-w-0"> </span>
<div className="d-flex align-items-center gap-2 mb-1"> <div className="flex-grow-1 min-w-0">
<code className="fw-bold">{gw.name}</code> <div className="fw-bold text-truncate">
{gw.comment && ( {gw.ip || gw.name}
<span className="text-muted small text-truncate" style={{ maxWidth: '180px' }}> </div>
{gw.comment} <div className={`small text-truncate ${isSelected ? 'text-white-75' : 'text-muted'}`}>
</span> {gw.comment || gw.name}
</div>
{gw.serverDns && (
<div className={`small text-truncate ${isSelected ? 'text-white-75' : 'text-muted'}`}>
{gw.serverDns}
</div>
)} )}
</div> </div>
<div className="text-muted small"> {gw.primary && (
{gw.ip && <span>IP: {gw.ip}</span>} <span className={`badge ${isSelected ? 'bg-white text-primary' : 'bg-green-lt text-green'}`}>
{gw.ip && gw.serverDns && <span className="mx-1"></span>} Основной
{gw.serverDns && <span>{gw.serverDns}</span>} </span>
</div> )}
</div> </div>
{gw.primary && (
<span className="badge bg-green-lt text-green align-self-start">
Основной
</span>
)}
</div> </div>
</div> </div>
</button> </button>
@@ -524,7 +517,7 @@ function GatewayAutocomplete({ label, value, onChange, gateways = [], required =
</div> </div>
)} )}
</div> </div>
)} </div>
{/* Показываем выбранный gateway отдельной карточкой под полем */} {/* Показываем выбранный gateway отдельной карточкой под полем */}
{value && selectedGateway && !isOpen && ( {value && selectedGateway && !isOpen && (