CI / changes (push) Successful in 7s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 31s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 3m30s
- Added new utility functions for module state representation in Russian, improving localization. - Introduced derived states for counting enabled and disabled modules, enhancing user insights. - Updated module management components to display last updated timestamps and improved descriptions. - Refactored imports to utilize core UI components for better maintainability and consistency.
156 lines
4.3 KiB
Svelte
156 lines
4.3 KiB
Svelte
<script lang="ts">
|
|
import type { AsEntry, BgpCommunity, DohProfile, ModuleRow } from '$lib/api/types.js';
|
|
import { formatDateTime, moduleIntervalLabel } from '$lib/modules/display.js';
|
|
import { dohPolicyRu } from '$lib/ui-labels.js';
|
|
import {
|
|
communityLabel,
|
|
dohProfileLabel,
|
|
moduleDohProfileIds
|
|
} from '$lib/components/modules/module-helpers.js';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '$lib/ui/core/card/index.js';
|
|
import CardSkeleton from '$lib/ui/patterns/feedback/card-skeleton.svelte';
|
|
import { cn } from '$lib/utils.js';
|
|
import ArrowDownUp from '@lucide/svelte/icons/arrow-down-up';
|
|
import Timer from '@lucide/svelte/icons/timer';
|
|
import ShieldCheck from '@lucide/svelte/icons/shield-check';
|
|
import Network from '@lucide/svelte/icons/network';
|
|
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
|
|
|
type Props = {
|
|
mod: ModuleRow | null;
|
|
communities: BgpCommunity[];
|
|
dohProfiles: DohProfile[];
|
|
asEntries: AsEntry[];
|
|
loading?: boolean;
|
|
};
|
|
|
|
let { mod, communities, dohProfiles, asEntries, loading = false }: Props = $props();
|
|
|
|
const asPrefixTotal = $derived(
|
|
asEntries.reduce((acc, entry) => acc + (entry.prefix_count ?? 0), 0)
|
|
);
|
|
|
|
const statAccents = [
|
|
{
|
|
border: 'border-l-chart-3',
|
|
bg: 'bg-chart-3/5',
|
|
iconBg: 'bg-chart-3/15',
|
|
iconText: 'text-chart-3'
|
|
},
|
|
{
|
|
border: 'border-l-chart-5',
|
|
bg: 'bg-chart-5/5',
|
|
iconBg: 'bg-chart-5/15',
|
|
iconText: 'text-chart-5'
|
|
},
|
|
{
|
|
border: 'border-l-chart-4',
|
|
bg: 'bg-chart-4/5',
|
|
iconBg: 'bg-chart-4/15',
|
|
iconText: 'text-chart-4'
|
|
},
|
|
{
|
|
border: 'border-l-chart-2',
|
|
bg: 'bg-chart-2/5',
|
|
iconBg: 'bg-chart-2/15',
|
|
iconText: 'text-chart-2'
|
|
},
|
|
{
|
|
border: 'border-l-muted-foreground',
|
|
bg: 'bg-muted/30',
|
|
iconBg: 'bg-muted',
|
|
iconText: 'text-muted-foreground'
|
|
}
|
|
] as const;
|
|
|
|
const kpiCards = $derived.by(() => {
|
|
if (!mod) return [];
|
|
const dohIds = moduleDohProfileIds(mod);
|
|
return [
|
|
{
|
|
id: 'priority',
|
|
label: 'Приоритет',
|
|
value: String(mod.priority ?? 0),
|
|
description: 'порядок в сборке ревизии',
|
|
icon: ArrowDownUp,
|
|
accent: statAccents[0]
|
|
},
|
|
{
|
|
id: 'interval',
|
|
label: 'Интервал',
|
|
value: moduleIntervalLabel(mod),
|
|
description: 'refresh_interval_sec / cron',
|
|
icon: Timer,
|
|
accent: statAccents[1],
|
|
mono: true
|
|
},
|
|
{
|
|
id: 'doh',
|
|
label: 'DoH',
|
|
value: mod.type === 'DOMAINS' ? dohPolicyRu(mod.doh_resolver_policy) : '—',
|
|
description:
|
|
mod.type === 'DOMAINS'
|
|
? dohIds.length
|
|
? dohIds.map((id) => dohProfileLabel(id, dohProfiles)).join('; ')
|
|
: 'Системный DNS'
|
|
: 'не применимо',
|
|
icon: Network,
|
|
accent: statAccents[2]
|
|
},
|
|
{
|
|
id: 'community',
|
|
label: 'Community по умолч.',
|
|
value: communityLabel(mod.default_community_id, communities),
|
|
description: 'для записей без своего community',
|
|
icon: ShieldCheck,
|
|
accent: statAccents[3]
|
|
},
|
|
{
|
|
id: 'refreshed',
|
|
label: 'Последнее обновление',
|
|
value: formatDateTime(mod.last_refreshed_at),
|
|
description:
|
|
mod.type === 'AS_PREFIXES'
|
|
? `ASN: ${asEntries.length}, префиксов: ${asPrefixTotal}`
|
|
: 'время последнего refresh',
|
|
icon: RefreshCw,
|
|
accent: statAccents[4]
|
|
}
|
|
];
|
|
});
|
|
</script>
|
|
|
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-5">
|
|
{#if loading}
|
|
{#each Array(5) as _, i (i)}
|
|
<CardSkeleton />
|
|
{/each}
|
|
{:else}
|
|
{#each kpiCards as card (card.id)}
|
|
{@const Icon = card.icon}
|
|
{@const a = card.accent}
|
|
<Card class={cn('overflow-hidden border-l-4 shadow-sm', a.border, a.bg)}>
|
|
<CardHeader class="pb-2">
|
|
<p class="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
<span
|
|
class={cn('flex size-7 shrink-0 items-center justify-center rounded-md', a.iconBg)}
|
|
aria-hidden="true"
|
|
>
|
|
<Icon class={cn('size-3.5', a.iconText)} />
|
|
</span>
|
|
{card.label}
|
|
</p>
|
|
<CardTitle
|
|
class={cn('text-base font-semibold break-all', card.mono ? 'font-mono text-sm' : '')}
|
|
>
|
|
{card.value}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p class="line-clamp-2 text-xs text-muted-foreground">{card.description}</p>
|
|
</CardContent>
|
|
</Card>
|
|
{/each}
|
|
{/if}
|
|
</div>
|