fix: model user assignment by service instead of port

This commit is contained in:
2026-04-01 23:33:47 +03:00
parent 15f8748be1
commit f1f5caea06
6 changed files with 119 additions and 21 deletions

View File

@@ -18,6 +18,14 @@ const tabs: Array<{ id: TabId; label: string }> = [
{ id: 'system', label: 'System' },
];
const assignableServices = dashboardSnapshot.system.services.filter(
(service) => service.enabled && service.assignable,
);
const servicesById = new Map(
dashboardSnapshot.system.services.map((service) => [service.id, service] as const),
);
function LoginGate({ onUnlock }: { onUnlock: () => void }) {
const [login, setLogin] = useState('');
const [password, setPassword] = useState('');
@@ -71,6 +79,8 @@ function LoginGate({ onUnlock }: { onUnlock: () => void }) {
}
function AddUserModal({ onClose }: { onClose: () => void }) {
const [serviceId, setServiceId] = useState(assignableServices[0]?.id ?? '');
useEffect(() => {
const handleKeyDown = (event: globalThis.KeyboardEvent) => {
if (event.key === 'Escape') {
@@ -87,6 +97,8 @@ function AddUserModal({ onClose }: { onClose: () => void }) {
event.stopPropagation();
};
const selectedService = servicesById.get(serviceId);
return (
<div className="modal-backdrop" role="presentation" onClick={onClose}>
<section
@@ -112,13 +124,31 @@ function AddUserModal({ onClose }: { onClose: () => void }) {
<input placeholder="generated secret" />
</label>
<label>
Port
<input placeholder="1080" />
Service
<select value={serviceId} onChange={(event) => setServiceId(event.target.value)}>
{assignableServices.map((service) => (
<option key={service.id} value={service.id}>
{service.name}
</option>
))}
</select>
</label>
<label>
Quota (MB)
<input placeholder="Optional" />
</label>
<div className="modal-preview">
<span>Endpoint</span>
<strong>
{selectedService
? `${dashboardSnapshot.system.publicHost}:${selectedService.port}`
: 'Unavailable'}
</strong>
</div>
<div className="modal-preview">
<span>Protocol</span>
<strong>{selectedService ? selectedService.protocol : 'Unavailable'}</strong>
</div>
<div className="modal-actions">
<button type="button" className="button-secondary" onClick={onClose}>
Cancel
@@ -269,7 +299,19 @@ function UsersTab() {
</thead>
<tbody>
{dashboardSnapshot.userRecords.map((user) => {
const proxyLink = buildProxyLink(user.username, user.password, user.host, user.port);
const service = servicesById.get(user.serviceId);
const endpoint = service
? `${dashboardSnapshot.system.publicHost}:${service.port}`
: 'service missing';
const proxyLink = service
? buildProxyLink(
user.username,
user.password,
dashboardSnapshot.system.publicHost,
service.port,
service.protocol,
)
: '';
const exhausted = isQuotaExceeded(user.usedBytes, user.quotaBytes);
return (
@@ -279,7 +321,12 @@ function UsersTab() {
<strong>{user.username}</strong>
</div>
</td>
<td>{`${user.host}:${user.port}`}</td>
<td>
<div className="endpoint-cell">
<strong>{service?.name ?? 'Unknown service'}</strong>
<span>{endpoint}</span>
</div>
</td>
<td>
<span className={`status-pill ${getServiceTone(user.status)}`}>{user.status}</span>
</td>
@@ -291,6 +338,7 @@ function UsersTab() {
type="button"
className={exhausted ? 'copy-button danger' : 'copy-button'}
onClick={() => handleCopy(user.id, proxyLink)}
disabled={!service}
>
{copiedId === user.id ? 'Copied' : 'Copy'}
</button>