Enhance user data handling in users page
Publish telemt-api gateway Docker image / test (push) Successful in 25s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m6s

- Added a new API call to fetch unique IPs alongside existing user data, improving the richness of information displayed.
- Implemented derived state to organize unique IPs by user, allowing for better data presentation in the UI.
- Updated the table to display IP-server pairs for each user, enhancing clarity and usability.
- Improved error handling during data fetching to ensure robust user experience.
This commit is contained in:
Denozordec
2026-03-30 13:36:30 +07:00
parent fe718b7e21
commit 40bcfbf993
+65 -8
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fetchAggUsers, ApiError } from '$lib/api/client.js'; import { fetchAggUsers, fetchAggUniqueIps, ApiError } from '$lib/api/client.js';
import type { components } from '$lib/api/aggregate.gen.js'; import type { components } from '$lib/api/aggregate.gen.js';
import { formatBytes, formatMiB } from '$lib/format.js'; import { formatBytes, formatMiB } from '$lib/format.js';
import { Button } from '$lib/components/ui/button/index.js'; import { Button } from '$lib/components/ui/button/index.js';
@@ -14,6 +14,7 @@
let loading = $state(true); let loading = $state(true);
let err = $state<string | null>(null); let err = $state<string | null>(null);
let rows = $state<components['schemas']['UsersRow'][]>([]); let rows = $state<components['schemas']['UsersRow'][]>([]);
let uniqueIps = $state<components['schemas']['UniqueIPsRow'][]>([]);
let partial = $state(false); let partial = $state(false);
let includeLinks = $state(false); let includeLinks = $state(false);
@@ -21,9 +22,13 @@
loading = true; loading = true;
err = null; err = null;
try { try {
const env = await fetchAggUsers({ include_links: includeLinks }); const [usersEnv, uniqueIpsEnv] = await Promise.all([
partial = !!env.partial; fetchAggUsers({ include_links: includeLinks }),
rows = env.data ?? []; fetchAggUniqueIps({ geo: false })
]);
partial = !!(usersEnv.partial || uniqueIpsEnv.partial);
rows = usersEnv.data ?? [];
uniqueIps = uniqueIpsEnv.data ?? [];
} catch (e) { } catch (e) {
err = e instanceof ApiError ? e.message : String(e); err = e instanceof ApiError ? e.message : String(e);
} finally { } finally {
@@ -33,6 +38,38 @@
onMount(load); onMount(load);
type IPServerPair = {
ip: string;
server: string;
};
let ipServerPairsByUser = $derived.by((): Record<string, IPServerPair[]> => {
const byUser: Record<string, IPServerPair[]> = {};
for (const ur of uniqueIps) {
const username = ur.username ?? '';
if (!username) continue;
// Дедупликация по ключу `${ip}|${server}`.
const dedupe: Record<string, IPServerPair> = {};
for (const ipa of ur.ips ?? []) {
const ip = ipa.ip;
if (!ip) continue;
for (const server of ipa.active_on_servers ?? []) {
if (!server) continue;
const key = `${ip}|${server}`;
dedupe[key] = { ip, server };
}
}
const pairs = Object.values(dedupe);
pairs.sort((a, b) => a.ip.localeCompare(b.ip) || a.server.localeCompare(b.server));
byUser[username] = pairs;
}
return byUser;
});
function copy(text: string) { function copy(text: string) {
void navigator.clipboard.writeText(text); void navigator.clipboard.writeText(text);
} }
@@ -80,13 +117,16 @@
<Table.Head>Имя</Table.Head> <Table.Head>Имя</Table.Head>
<Table.Head>Ссылки</Table.Head> <Table.Head>Ссылки</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 class="text-right">Квота</Table.Head> <Table.Head class="text-right">Квота</Table.Head>
<Table.Head>Истекает</Table.Head> <Table.Head>Истекает</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)}
{@const username = row.username ?? ''}
{@const pairs = ipServerPairsByUser[username] ?? []}
<Table.Row> <Table.Row>
<Table.Cell class="font-medium"> <Table.Cell class="font-medium">
<a href="/users/{encodeURIComponent(row.username ?? '')}" class="text-primary hover:underline"> <a href="/users/{encodeURIComponent(row.username ?? '')}" class="text-primary hover:underline">
@@ -111,9 +151,13 @@
</Table.Cell> </Table.Cell>
<Table.Cell class="text-right tabular-nums">{formatMiB(row.total_megabytes ?? 0)}</Table.Cell> <Table.Cell class="text-right tabular-nums">{formatMiB(row.total_megabytes ?? 0)}</Table.Cell>
<Table.Cell class="text-right text-sm tabular-nums"> <Table.Cell class="text-right text-sm tabular-nums">
{row.active_unique_ips ?? 0} {#if (row.active_unique_ips ?? 0) > 0}
{#if row.max_unique_ips != null} {row.active_unique_ips}
<span class="text-muted-foreground"> / {row.max_unique_ips}</span> {#if row.max_unique_ips != null}
<span class="text-muted-foreground"> / {row.max_unique_ips}</span>
{/if}
{:else}
{/if} {/if}
</Table.Cell> </Table.Cell>
<Table.Cell class="text-right text-sm"> <Table.Cell class="text-right text-sm">
@@ -125,6 +169,19 @@
: '—'} : '—'}
</Table.Cell> </Table.Cell>
</Table.Row> </Table.Row>
{#if pairs.length > 0}
{#each pairs as pair (pair.ip + '|' + pair.server)}
<Table.Row>
<Table.Cell>&nbsp;</Table.Cell>
<Table.Cell>&nbsp;</Table.Cell>
<Table.Cell>&nbsp;</Table.Cell>
<Table.Cell class="text-right text-sm font-mono">{pair.ip} - {pair.server}</Table.Cell>
<Table.Cell>&nbsp;</Table.Cell>
<Table.Cell>&nbsp;</Table.Cell>
</Table.Row>
{/each}
{/if}
{/each} {/each}
</Table.Body> </Table.Body>
</Table.Root> </Table.Root>