Enhance user data handling in users page
- 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:
@@ -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> </Table.Cell>
|
||||||
|
<Table.Cell> </Table.Cell>
|
||||||
|
<Table.Cell> </Table.Cell>
|
||||||
|
<Table.Cell class="text-right text-sm font-mono">{pair.ip} - {pair.server}</Table.Cell>
|
||||||
|
<Table.Cell> </Table.Cell>
|
||||||
|
<Table.Cell> </Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</Table.Body>
|
</Table.Body>
|
||||||
</Table.Root>
|
</Table.Root>
|
||||||
|
|||||||
Reference in New Issue
Block a user