Refactor server dashboard pages with reusable UI blocks.
Extract shared header, query-state, and JSON panel components, then apply them across server runtime/security/upstreams/update/config views for consistent Telemt panel UX. Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import type { KpiAccentKey } from '$lib/kpi-accents.js';
|
||||||
|
import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
|
||||||
|
import BracesIcon from '@lucide/svelte/icons/braces';
|
||||||
|
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
accent = 'info',
|
||||||
|
payload,
|
||||||
|
class: className,
|
||||||
|
preClass = 'max-h-[70vh]',
|
||||||
|
iconSnippet
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
accent?: KpiAccentKey;
|
||||||
|
payload: unknown;
|
||||||
|
class?: string;
|
||||||
|
preClass?: string;
|
||||||
|
iconSnippet?: Snippet;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<KpiPanelCard {accent} {title} {description} class={className} contentClass="p-0">
|
||||||
|
{#snippet icon()}
|
||||||
|
{#if iconSnippet}
|
||||||
|
{@render iconSnippet()}
|
||||||
|
{:else}
|
||||||
|
<BracesIcon class="size-5" aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
<pre
|
||||||
|
class={[
|
||||||
|
preClass,
|
||||||
|
'overflow-auto rounded-md border border-border bg-muted/40 p-4 text-xs leading-relaxed'
|
||||||
|
]}
|
||||||
|
>{JSON.stringify(payload, null, 2)}</pre>
|
||||||
|
</KpiPanelCard>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
|
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||||
|
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
loading = false,
|
||||||
|
showRefresh = true,
|
||||||
|
onRefresh,
|
||||||
|
actions
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
showRefresh?: boolean;
|
||||||
|
onRefresh?: () => void | Promise<void>;
|
||||||
|
actions?: Snippet;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h1 class="text-2xl font-semibold tracking-tight">{title}</h1>
|
||||||
|
{#if subtitle}
|
||||||
|
<p class="text-sm text-muted-foreground">{subtitle}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
{@render actions?.()}
|
||||||
|
{#if showRefresh}
|
||||||
|
<Button variant="outline" size="sm" onclick={onRefresh} disabled={loading || !onRefresh}>
|
||||||
|
<RefreshCwIcon class={['mr-1 size-4', loading && 'animate-spin']} />
|
||||||
|
Обновить
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
|
||||||
|
import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
|
||||||
|
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle';
|
||||||
|
import InboxIcon from '@lucide/svelte/icons/inbox';
|
||||||
|
|
||||||
|
let {
|
||||||
|
loading = false,
|
||||||
|
err = null,
|
||||||
|
isEmpty = false,
|
||||||
|
loadingTitle = 'Загрузка данных',
|
||||||
|
loadingDescription = 'Пожалуйста, подождите...',
|
||||||
|
emptyTitle = 'Нет данных',
|
||||||
|
emptyDescription = 'По этому endpoint пока нет содержимого.',
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
loading?: boolean;
|
||||||
|
err?: string | null;
|
||||||
|
isEmpty?: boolean;
|
||||||
|
loadingTitle?: string;
|
||||||
|
loadingDescription?: string;
|
||||||
|
emptyTitle?: string;
|
||||||
|
emptyDescription?: string;
|
||||||
|
children?: Snippet;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if err}
|
||||||
|
<Alert variant="destructive" class="mb-4">
|
||||||
|
<AlertTitle>Ошибка</AlertTitle>
|
||||||
|
<AlertDescription>{err}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if loading}
|
||||||
|
<KpiPanelCard
|
||||||
|
accent="neutral"
|
||||||
|
title={loadingTitle}
|
||||||
|
description={loadingDescription}
|
||||||
|
contentClass="pt-0 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
{#snippet icon()}
|
||||||
|
<LoaderCircleIcon class="size-5 animate-spin" aria-hidden="true" />
|
||||||
|
{/snippet}
|
||||||
|
Идет запрос к ноде, обновляем показатели dashboard.
|
||||||
|
</KpiPanelCard>
|
||||||
|
{:else if !err && isEmpty}
|
||||||
|
<KpiPanelCard
|
||||||
|
accent="neutral"
|
||||||
|
title={emptyTitle}
|
||||||
|
description={emptyDescription}
|
||||||
|
contentClass="pt-0 text-sm text-muted-foreground"
|
||||||
|
>
|
||||||
|
{#snippet icon()}
|
||||||
|
<InboxIcon class="size-5" aria-hidden="true" />
|
||||||
|
{/snippet}
|
||||||
|
Проверьте параметры запроса или состояние сервера.
|
||||||
|
</KpiPanelCard>
|
||||||
|
{:else if !loading && !isEmpty}
|
||||||
|
{@render children?.()}
|
||||||
|
{/if}
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
|
import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
|
||||||
|
import ServerPageHeader from '$lib/components/server-page-header.svelte';
|
||||||
import SettingsIcon from '@lucide/svelte/icons/settings';
|
import SettingsIcon from '@lucide/svelte/icons/settings';
|
||||||
|
|
||||||
const alias = $derived(page.params.alias ?? '');
|
const alias = $derived(page.params.alias ?? '');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-6">
|
<ServerPageHeader
|
||||||
<h1 class="mb-2 text-2xl font-semibold tracking-tight">Configuration</h1>
|
title="Configuration"
|
||||||
<p class="text-sm text-muted-foreground">
|
subtitle="Конфигурация этой ноды не отдается через Control API"
|
||||||
Конфигурация Telemt на ноде <span class="font-mono text-foreground">{alias}</span> не доступна через API
|
showRefresh={false}
|
||||||
— правьте <code class="rounded bg-muted px-1">config.toml</code> на сервере.
|
/>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<KpiPanelCard accent="neutral" title="Конфигурация" description="Настройка на сервере" contentClass="text-sm text-muted-foreground">
|
<KpiPanelCard accent="neutral" title="Конфигурация" description="Настройка на сервере" contentClass="text-sm text-muted-foreground">
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
<SettingsIcon class="size-5" aria-hidden="true" />
|
<SettingsIcon class="size-5" aria-hidden="true" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
<p class="mb-2">
|
||||||
|
Конфигурация Telemt на ноде <span class="font-mono text-foreground">{alias}</span> не доступна через API.
|
||||||
|
Правьте <code class="rounded bg-muted px-1">config.toml</code> на сервере.
|
||||||
|
</p>
|
||||||
См. <a
|
См. <a
|
||||||
class="text-primary underline"
|
class="text-primary underline"
|
||||||
href="https://github.com/telemt/telemt/blob/main/docs/API.md"
|
href="https://github.com/telemt/telemt/blob/main/docs/API.md"
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
import { ApiError, fetchTelemt } from '$lib/api/client.js';
|
import { ApiError, fetchTelemt } from '$lib/api/client.js';
|
||||||
import type { DcStatusData, RuntimeGatesData } from '$lib/api/telemt-v1.js';
|
import type { DcStatusData, RuntimeGatesData } from '$lib/api/telemt-v1.js';
|
||||||
import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
|
import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
|
||||||
|
import ServerPageHeader from '$lib/components/server-page-header.svelte';
|
||||||
|
import ServerQueryState from '$lib/components/server-query-state.svelte';
|
||||||
|
import ServerJsonCard from '$lib/components/server-json-card.svelte';
|
||||||
import BuildingIcon from '@lucide/svelte/icons/building-2';
|
import BuildingIcon from '@lucide/svelte/icons/building-2';
|
||||||
import BracesIcon from '@lucide/svelte/icons/braces';
|
import BracesIcon from '@lucide/svelte/icons/braces';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
|
import { Alert, AlertDescription } from '$lib/components/ui/alert/index.js';
|
||||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
|
||||||
import * as Table from '$lib/components/ui/table/index.js';
|
import * as Table from '$lib/components/ui/table/index.js';
|
||||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
|
||||||
|
|
||||||
const alias = $derived(page.params.alias ?? '');
|
const alias = $derived(page.params.alias ?? '');
|
||||||
|
|
||||||
@@ -56,22 +57,10 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<ServerPageHeader title="Runtime" {loading} onRefresh={load} />
|
||||||
<h1 class="text-2xl font-semibold tracking-tight">Runtime</h1>
|
|
||||||
<Button variant="outline" size="sm" onclick={load} disabled={loading}>
|
|
||||||
<RefreshCwIcon class="mr-1 size-4 {loading ? 'animate-spin' : ''}" />
|
|
||||||
Обновить
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if err}
|
<ServerQueryState {loading} {err} isEmpty={false}>
|
||||||
<Alert variant="destructive">
|
{#snippet children()}
|
||||||
<AlertTitle>Ошибка</AlertTitle>
|
|
||||||
<AlertDescription>{err}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
{:else if loading}
|
|
||||||
<p class="text-muted-foreground">Загрузка…</p>
|
|
||||||
{:else}
|
|
||||||
<div class="mb-4 flex flex-wrap gap-2">
|
<div class="mb-4 flex flex-wrap gap-2">
|
||||||
{#if gates}
|
{#if gates}
|
||||||
<Badge variant="secondary" class="px-3 py-1">accepting: {onOff(gates.accepting_new_connections)}</Badge>
|
<Badge variant="secondary" class="px-3 py-1">accepting: {onOff(gates.accepting_new_connections)}</Badge>
|
||||||
@@ -118,14 +107,10 @@
|
|||||||
</Alert>
|
</Alert>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<KpiPanelCard accent="violet" title="ME pool state" description="Сырой JSON" contentClass="">
|
<ServerJsonCard accent="violet" title="ME pool state" description="Сырой JSON" payload={pool} preClass="max-h-[480px]">
|
||||||
{#snippet icon()}
|
{#snippet iconSnippet()}
|
||||||
<BracesIcon class="size-5" aria-hidden="true" />
|
<BracesIcon class="size-5" aria-hidden="true" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<pre class="max-h-[480px] overflow-auto rounded-md border border-border bg-muted/40 p-3 text-xs">{JSON.stringify(
|
</ServerJsonCard>
|
||||||
pool,
|
{/snippet}
|
||||||
null,
|
</ServerQueryState>
|
||||||
2
|
|
||||||
)}</pre>
|
|
||||||
</KpiPanelCard>
|
|
||||||
{/if}
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { ApiError, fetchTelemt } from '$lib/api/client.js';
|
import { ApiError, fetchTelemt } from '$lib/api/client.js';
|
||||||
import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
|
import ServerPageHeader from '$lib/components/server-page-header.svelte';
|
||||||
|
import ServerQueryState from '$lib/components/server-query-state.svelte';
|
||||||
|
import ServerJsonCard from '$lib/components/server-json-card.svelte';
|
||||||
import ShieldIcon from '@lucide/svelte/icons/shield';
|
import ShieldIcon from '@lucide/svelte/icons/shield';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
|
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
|
||||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
|
||||||
|
|
||||||
const alias = $derived(page.params.alias ?? '');
|
const alias = $derived(page.params.alias ?? '');
|
||||||
|
|
||||||
@@ -39,30 +38,16 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<ServerPageHeader title="Security" {loading} onRefresh={load} />
|
||||||
<h1 class="text-2xl font-semibold tracking-tight">Security</h1>
|
|
||||||
<Button variant="outline" size="sm" onclick={load} disabled={loading}>
|
|
||||||
<RefreshCwIcon class="mr-1 size-4 {loading ? 'animate-spin' : ''}" />
|
|
||||||
Обновить
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if err}
|
<ServerQueryState {loading} {err} isEmpty={!data} emptyDescription="Endpoint вернул пустой ответ.">
|
||||||
<Alert variant="destructive">
|
{#snippet children()}
|
||||||
<AlertTitle>Ошибка</AlertTitle>
|
{#if data}
|
||||||
<AlertDescription>{err}</AlertDescription>
|
<ServerJsonCard accent="danger" title="Security posture" description="POST /security/posture" payload={data}>
|
||||||
</Alert>
|
{#snippet iconSnippet()}
|
||||||
{:else if loading}
|
|
||||||
<p class="text-muted-foreground">Загрузка…</p>
|
|
||||||
{:else if data}
|
|
||||||
<KpiPanelCard accent="danger" title="Security posture" description="POST /security/posture" contentClass="p-0">
|
|
||||||
{#snippet icon()}
|
|
||||||
<ShieldIcon class="size-5" aria-hidden="true" />
|
<ShieldIcon class="size-5" aria-hidden="true" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<pre class="overflow-auto rounded-md border border-border bg-muted/40 p-4 text-xs">{JSON.stringify(
|
</ServerJsonCard>
|
||||||
data,
|
{/if}
|
||||||
null,
|
{/snippet}
|
||||||
2
|
</ServerQueryState>
|
||||||
)}</pre>
|
|
||||||
</KpiPanelCard>
|
|
||||||
{/if}
|
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
import { ApiError, fetchTelemt } from '$lib/api/client.js';
|
import { ApiError, fetchTelemt } from '$lib/api/client.js';
|
||||||
import type { SystemInfoData } from '$lib/api/telemt-v1.js';
|
import type { SystemInfoData } from '$lib/api/telemt-v1.js';
|
||||||
import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
|
import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
|
||||||
|
import ServerPageHeader from '$lib/components/server-page-header.svelte';
|
||||||
|
import ServerQueryState from '$lib/components/server-query-state.svelte';
|
||||||
import PackageIcon from '@lucide/svelte/icons/package';
|
import PackageIcon from '@lucide/svelte/icons/package';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
|
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
|
||||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
|
||||||
|
|
||||||
const alias = $derived(page.params.alias ?? '');
|
const alias = $derived(page.params.alias ?? '');
|
||||||
|
|
||||||
@@ -40,22 +39,11 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<ServerPageHeader title="Update" {loading} onRefresh={load} />
|
||||||
<h1 class="text-2xl font-semibold tracking-tight">Update</h1>
|
|
||||||
<Button variant="outline" size="sm" onclick={load} disabled={loading}>
|
|
||||||
<RefreshCwIcon class="mr-1 size-4 {loading ? 'animate-spin' : ''}" />
|
|
||||||
Обновить
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if err}
|
<ServerQueryState {loading} {err} isEmpty={!sys} emptyDescription="Информация о бинарнике не получена.">
|
||||||
<Alert variant="destructive">
|
{#snippet children()}
|
||||||
<AlertTitle>Ошибка</AlertTitle>
|
{#if sys}
|
||||||
<AlertDescription>{err}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
{:else if loading}
|
|
||||||
<p class="text-muted-foreground">Загрузка…</p>
|
|
||||||
{:else if sys}
|
|
||||||
<KpiPanelCard accent="teal" title="Версия бинарника" description="system/info" contentClass="space-y-2 text-sm">
|
<KpiPanelCard accent="teal" title="Версия бинарника" description="system/info" contentClass="space-y-2 text-sm">
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
<PackageIcon class="size-5" aria-hidden="true" />
|
<PackageIcon class="size-5" aria-hidden="true" />
|
||||||
@@ -65,4 +53,6 @@
|
|||||||
<p><span class="text-muted-foreground">build</span> {sys.build_time_utc ?? '—'}</p>
|
<p><span class="text-muted-foreground">build</span> {sys.build_time_utc ?? '—'}</p>
|
||||||
<p><span class="text-muted-foreground">rustc</span> {sys.rustc_version ?? '—'}</p>
|
<p><span class="text-muted-foreground">rustc</span> {sys.rustc_version ?? '—'}</p>
|
||||||
</KpiPanelCard>
|
</KpiPanelCard>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</ServerQueryState>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { ApiError, fetchTelemt } from '$lib/api/client.js';
|
import { ApiError, fetchTelemt } from '$lib/api/client.js';
|
||||||
import KpiPanelCard from '$lib/components/kpi-panel-card.svelte';
|
import ServerPageHeader from '$lib/components/server-page-header.svelte';
|
||||||
|
import ServerQueryState from '$lib/components/server-query-state.svelte';
|
||||||
|
import ServerJsonCard from '$lib/components/server-json-card.svelte';
|
||||||
import Share2Icon from '@lucide/svelte/icons/share-2';
|
import Share2Icon from '@lucide/svelte/icons/share-2';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
|
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
|
||||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
|
||||||
|
|
||||||
const alias = $derived(page.params.alias ?? '');
|
const alias = $derived(page.params.alias ?? '');
|
||||||
|
|
||||||
@@ -39,30 +38,22 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<ServerPageHeader title="Upstreams & DCs" {loading} onRefresh={load} />
|
||||||
<h1 class="text-2xl font-semibold tracking-tight">Upstreams & DCs</h1>
|
|
||||||
<Button variant="outline" size="sm" onclick={load} disabled={loading}>
|
|
||||||
<RefreshCwIcon class="mr-1 size-4 {loading ? 'animate-spin' : ''}" />
|
|
||||||
Обновить
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if err}
|
<ServerQueryState {loading} {err} isEmpty={!data} emptyDescription="Статистика upstream-ов пока не доступна.">
|
||||||
<Alert variant="destructive">
|
{#snippet children()}
|
||||||
<AlertTitle>Ошибка</AlertTitle>
|
{#if data}
|
||||||
<AlertDescription>{err}</AlertDescription>
|
<ServerJsonCard
|
||||||
</Alert>
|
accent="info"
|
||||||
{:else if loading}
|
title="Upstreams & DCs"
|
||||||
<p class="text-muted-foreground">Загрузка…</p>
|
description="stats/upstreams"
|
||||||
{:else if data}
|
payload={data}
|
||||||
<KpiPanelCard accent="info" title="Upstreams & DCs" description="stats/upstreams" contentClass="p-0">
|
preClass="max-h-[70vh]"
|
||||||
{#snippet icon()}
|
>
|
||||||
|
{#snippet iconSnippet()}
|
||||||
<Share2Icon class="size-5" aria-hidden="true" />
|
<Share2Icon class="size-5" aria-hidden="true" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<pre class="max-h-[70vh] overflow-auto rounded-md border border-border bg-muted/40 p-4 text-xs">{JSON.stringify(
|
</ServerJsonCard>
|
||||||
data,
|
{/if}
|
||||||
null,
|
{/snippet}
|
||||||
2
|
</ServerQueryState>
|
||||||
)}</pre>
|
|
||||||
</KpiPanelCard>
|
|
||||||
{/if}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user