Refactor table structure and styling in user and IP pages
- Updated the table layout in the user traffic and IP detail pages to enhance readability and organization. - Improved the display of active connections and server information, ensuring a consistent user experience across different views. - Added responsive styling for table headers and cells, optimizing the interface for various screen sizes.
This commit is contained in:
+42
-42
@@ -449,53 +449,53 @@
|
|||||||
<Card.Description>С GeoIP при включённой базе на шлюзе</Card.Description>
|
<Card.Description>С GeoIP при включённой базе на шлюзе</Card.Description>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<Table.Root>
|
<div class="overflow-x-auto">
|
||||||
<Table.Header>
|
<Table.Root>
|
||||||
<Table.Row>
|
<Table.Header>
|
||||||
<Table.Head>IP со страной</Table.Head>
|
|
||||||
<Table.Head>Клиент</Table.Head>
|
|
||||||
<Table.Head>Серверы</Table.Head>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body>
|
|
||||||
{#if (activeIpServerRows?.length ?? 0) === 0}
|
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Cell colspan="3" class="p-4 text-center text-muted-foreground">
|
<Table.Head class="text-xs sm:text-sm">IP со страной</Table.Head>
|
||||||
Нет активных подключений
|
<Table.Head class="text-xs sm:text-sm">Клиент</Table.Head>
|
||||||
</Table.Cell>
|
<Table.Head class="text-xs sm:text-sm">Серверы</Table.Head>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
{:else}
|
</Table.Header>
|
||||||
{#each activeIpServerRows as r (r.username + '|' + r.ip)}
|
<Table.Body>
|
||||||
|
{#if (activeIpServerRows?.length ?? 0) === 0}
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Cell class="font-mono text-sm">
|
<Table.Cell colspan="3" class="p-4 text-center text-muted-foreground">
|
||||||
{r.activeLabel}
|
Нет активных подключений
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
<a
|
|
||||||
href="/users/{encodeURIComponent(r.username)}"
|
|
||||||
class="text-primary hover:underline"
|
|
||||||
>
|
|
||||||
{r.username}
|
|
||||||
</a>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
{#each r.servers as srv, idx (srv)}
|
|
||||||
<a
|
|
||||||
href="/servers/{encodeURIComponent(srv)}"
|
|
||||||
class="text-primary hover:underline"
|
|
||||||
>
|
|
||||||
{srv}
|
|
||||||
</a>
|
|
||||||
{#if idx < r.servers.length - 1}
|
|
||||||
<span class="text-muted-foreground">, </span>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
{/each}
|
{:else}
|
||||||
{/if}
|
{#each activeIpServerRows as r (r.username + '|' + r.ip)}
|
||||||
</Table.Body>
|
<Table.Row>
|
||||||
</Table.Root>
|
<Table.Cell class="font-mono text-xs sm:text-sm whitespace-normal break-all">
|
||||||
|
{r.activeLabel}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell class="text-xs sm:text-sm">
|
||||||
|
<a
|
||||||
|
href="/users/{encodeURIComponent(r.username)}"
|
||||||
|
class="text-primary hover:underline"
|
||||||
|
>
|
||||||
|
{r.username}
|
||||||
|
</a>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell class="text-xs sm:text-sm whitespace-normal">
|
||||||
|
<div class="flex flex-wrap items-center gap-1">
|
||||||
|
{#each r.servers as srv (srv)}
|
||||||
|
<a href="/servers/{encodeURIComponent(srv)}" class="inline-flex">
|
||||||
|
<Badge variant="secondary" class="font-normal px-2 py-0.5">
|
||||||
|
{srv}
|
||||||
|
</Badge>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
|
|||||||
@@ -84,17 +84,29 @@
|
|||||||
<Table.Header>
|
<Table.Header>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Head>Имя</Table.Head>
|
<Table.Head>Имя</Table.Head>
|
||||||
<Table.Head>Ссылки</Table.Head>
|
<Table.Head class="hidden sm:table-cell">Ссылки</Table.Head>
|
||||||
<Table.Head class="text-right">Трафик</Table.Head>
|
<Table.Head class="text-right">Трафик</Table.Head>
|
||||||
<Table.Head class="text-right">Активных IP</Table.Head>
|
<Table.Head class="text-right">Активных IP</Table.Head>
|
||||||
<Table.Head>Истекает</Table.Head>
|
<Table.Head class="hidden sm:table-cell">Истекает</Table.Head>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
{#each rows as row (row.username ?? '')}
|
{#each rows as row (row.username ?? '')}
|
||||||
<Table.Row class="cursor-pointer" onclick={() => openUser(row.username)}>
|
<Table.Row
|
||||||
|
class="cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
aria-label={`Открыть пользователя ${row.username ?? ''}`}
|
||||||
|
onclick={() => openUser(row.username)}
|
||||||
|
onkeydown={(e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
openUser(row.username);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Table.Cell class="font-medium">{row.username}</Table.Cell>
|
<Table.Cell class="font-medium">{row.username}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell class="hidden sm:table-cell">
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
{#each row.links?.tls ?? [] as link (link)}
|
{#each row.links?.tls ?? [] as link (link)}
|
||||||
<Button
|
<Button
|
||||||
@@ -127,7 +139,7 @@
|
|||||||
<span class="text-muted-foreground"> / {row.max_unique_ips}</span>
|
<span class="text-muted-foreground"> / {row.max_unique_ips}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell class="text-sm text-muted-foreground">
|
<Table.Cell class="hidden sm:table-cell text-sm text-muted-foreground">
|
||||||
{row.expiration_rfc3339 ? new Date(row.expiration_rfc3339).toLocaleString() : '—'}
|
{row.expiration_rfc3339 ? new Date(row.expiration_rfc3339).toLocaleString() : '—'}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
|
|||||||
@@ -167,26 +167,34 @@
|
|||||||
</Alert>
|
</Alert>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Table.Root>
|
<div class="overflow-x-auto">
|
||||||
<Table.Header>
|
<Table.Root>
|
||||||
<Table.Row>
|
<Table.Header>
|
||||||
<Table.Head>IP</Table.Head>
|
|
||||||
<Table.Head>Серверы</Table.Head>
|
|
||||||
<Table.Head>Geo / ASN</Table.Head>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body>
|
|
||||||
{#if ipsForUser.length === 0}
|
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Cell colspan="3" class="p-4 text-center text-muted-foreground">
|
<Table.Head class="text-xs sm:text-sm">IP</Table.Head>
|
||||||
Нет активных IP
|
<Table.Head class="text-xs sm:text-sm">Серверы</Table.Head>
|
||||||
</Table.Cell>
|
<Table.Head class="text-xs sm:text-sm">Geo / ASN</Table.Head>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
{:else}
|
</Table.Header>
|
||||||
{#each ipsForUser as ip (ip.ip ?? '')}
|
<Table.Body>
|
||||||
|
{#if ipsForUser.length === 0}
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Cell class="font-mono text-sm">{ip.ip}</Table.Cell>
|
<Table.Cell
|
||||||
<Table.Cell class="text-sm text-muted-foreground">
|
colspan="3"
|
||||||
|
class="p-4 text-center text-muted-foreground whitespace-normal"
|
||||||
|
>
|
||||||
|
Нет активных IP
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{:else}
|
||||||
|
{#each ipsForUser as ip (ip.ip ?? '')}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell
|
||||||
|
class="font-mono text-xs sm:text-sm whitespace-normal break-all"
|
||||||
|
>
|
||||||
|
{ip.ip}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell class="text-xs sm:text-sm text-muted-foreground whitespace-normal">
|
||||||
active: {((ip.active_on_servers ?? []).filter(Boolean) as string[]).join(', ') || '—'}
|
active: {((ip.active_on_servers ?? []).filter(Boolean) as string[]).join(', ') || '—'}
|
||||||
{#if (ip.recent_on_servers?.length ?? 0) > 0}
|
{#if (ip.recent_on_servers?.length ?? 0) > 0}
|
||||||
<br />
|
<br />
|
||||||
@@ -196,31 +204,32 @@
|
|||||||
<br />
|
<br />
|
||||||
primary: {ip.primary_server}
|
primary: {ip.primary_server}
|
||||||
{/if}
|
{/if}
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell class="text-sm">
|
<Table.Cell class="text-xs sm:text-sm whitespace-normal">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div>
|
<div>
|
||||||
<span class="mr-1">{flagEmoji(ip.country_code ?? undefined)}</span>
|
<span class="mr-1">{flagEmoji(ip.country_code ?? undefined)}</span>
|
||||||
{ip.country_code ?? '—'}
|
{ip.country_code ?? '—'}
|
||||||
{#if ip.city_name}
|
{#if ip.city_name}
|
||||||
<span class="text-muted-foreground"> · {ip.city_name}</span>
|
<span class="text-muted-foreground"> · {ip.city_name}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if ip.asn != null}
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<Badge variant="outline">AS{ip.asn}</Badge>
|
||||||
|
<span class="text-muted-foreground">{ip.as_organization ?? ''}</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="text-muted-foreground">ASN: —</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if ip.asn != null}
|
</Table.Cell>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
</Table.Row>
|
||||||
<Badge variant="outline">AS{ip.asn}</Badge>
|
{/each}
|
||||||
<span class="text-muted-foreground">{ip.as_organization ?? ''}</span>
|
{/if}
|
||||||
</div>
|
</Table.Body>
|
||||||
{:else}
|
</Table.Root>
|
||||||
<div class="text-muted-foreground">ASN: —</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</Table.Body>
|
|
||||||
</Table.Root>
|
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user