Translate UI text to Russian for improved localization across various components and pages, including sidebar, pagination, and server details. Update labels, tooltips, and headers to enhance user experience for Russian-speaking users.
This commit is contained in:
@@ -4,12 +4,12 @@ const SEG_LABELS: Record<string, string> = {
|
|||||||
users: 'Пользователи',
|
users: 'Пользователи',
|
||||||
ips: 'Уникальные IP',
|
ips: 'Уникальные IP',
|
||||||
incidents: 'Инциденты',
|
incidents: 'Инциденты',
|
||||||
live: 'Live',
|
live: 'Поток',
|
||||||
mihomo: 'Mihomo',
|
mihomo: 'Mihomo',
|
||||||
servers: 'Серверы',
|
servers: 'Серверы',
|
||||||
runtime: 'Runtime',
|
runtime: 'Состояние',
|
||||||
security: 'Security',
|
security: 'Безопасность',
|
||||||
upstreams: 'Upstreams',
|
upstreams: 'Апстримы и ДЦ',
|
||||||
config: 'Конфигурация',
|
config: 'Конфигурация',
|
||||||
update: 'Обновление'
|
update: 'Обновление'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
</Sidebar.MenuButton>
|
</Sidebar.MenuButton>
|
||||||
</Sidebar.MenuItem>
|
</Sidebar.MenuItem>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton isActive={active('/incidents')} tooltipContent="Incidents">
|
<Sidebar.MenuButton isActive={active('/incidents')} tooltipContent="Инциденты">
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href="/incidents" {...props}>
|
<a href="/incidents" {...props}>
|
||||||
<SirenIcon />
|
<SirenIcon />
|
||||||
@@ -143,11 +143,11 @@
|
|||||||
<Sidebar.GroupLabel>Нода</Sidebar.GroupLabel>
|
<Sidebar.GroupLabel>Нода</Sidebar.GroupLabel>
|
||||||
<Sidebar.Menu>
|
<Sidebar.Menu>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton isActive={activeExact(serverPath)} tooltipContent="Dashboard">
|
<Sidebar.MenuButton isActive={activeExact(serverPath)} tooltipContent="Обзор">
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href={serverPath} {...props}>
|
<a href={serverPath} {...props}>
|
||||||
<LayoutDashboardIcon />
|
<LayoutDashboardIcon />
|
||||||
<span>Dashboard</span>
|
<span>Обзор</span>
|
||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Sidebar.MenuButton>
|
</Sidebar.MenuButton>
|
||||||
@@ -163,61 +163,61 @@
|
|||||||
</Sidebar.MenuButton>
|
</Sidebar.MenuButton>
|
||||||
</Sidebar.MenuItem>
|
</Sidebar.MenuItem>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton isActive={active(serverPath + '/runtime')} tooltipContent="Runtime">
|
<Sidebar.MenuButton isActive={active(serverPath + '/runtime')} tooltipContent="Состояние">
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href="{serverPath}/runtime" {...props}>
|
<a href="{serverPath}/runtime" {...props}>
|
||||||
<ActivityIcon />
|
<ActivityIcon />
|
||||||
<span>Runtime</span>
|
<span>Состояние</span>
|
||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Sidebar.MenuButton>
|
</Sidebar.MenuButton>
|
||||||
</Sidebar.MenuItem>
|
</Sidebar.MenuItem>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton isActive={active(serverPath + '/users')} tooltipContent="Users">
|
<Sidebar.MenuButton isActive={active(serverPath + '/users')} tooltipContent="Пользователи">
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href="{serverPath}/users" {...props}>
|
<a href="{serverPath}/users" {...props}>
|
||||||
<UsersIcon />
|
<UsersIcon />
|
||||||
<span>Users</span>
|
<span>Пользователи</span>
|
||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Sidebar.MenuButton>
|
</Sidebar.MenuButton>
|
||||||
</Sidebar.MenuItem>
|
</Sidebar.MenuItem>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton isActive={active(serverPath + '/security')} tooltipContent="Security">
|
<Sidebar.MenuButton isActive={active(serverPath + '/security')} tooltipContent="Безопасность">
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href="{serverPath}/security" {...props}>
|
<a href="{serverPath}/security" {...props}>
|
||||||
<ShieldIcon />
|
<ShieldIcon />
|
||||||
<span>Security</span>
|
<span>Безопасность</span>
|
||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Sidebar.MenuButton>
|
</Sidebar.MenuButton>
|
||||||
</Sidebar.MenuItem>
|
</Sidebar.MenuItem>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton isActive={active(serverPath + '/upstreams')} tooltipContent="Upstreams">
|
<Sidebar.MenuButton isActive={active(serverPath + '/upstreams')} tooltipContent="Апстримы и ДЦ">
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href="{serverPath}/upstreams" {...props}>
|
<a href="{serverPath}/upstreams" {...props}>
|
||||||
<NetworkIcon />
|
<NetworkIcon />
|
||||||
<span>Upstreams & DCs</span>
|
<span>Апстримы и ДЦ</span>
|
||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Sidebar.MenuButton>
|
</Sidebar.MenuButton>
|
||||||
</Sidebar.MenuItem>
|
</Sidebar.MenuItem>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton isActive={active(serverPath + '/config')} tooltipContent="Configuration">
|
<Sidebar.MenuButton isActive={active(serverPath + '/config')} tooltipContent="Конфигурация">
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href="{serverPath}/config" {...props}>
|
<a href="{serverPath}/config" {...props}>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
<span>Configuration</span>
|
<span>Конфигурация</span>
|
||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Sidebar.MenuButton>
|
</Sidebar.MenuButton>
|
||||||
</Sidebar.MenuItem>
|
</Sidebar.MenuItem>
|
||||||
<Sidebar.MenuItem>
|
<Sidebar.MenuItem>
|
||||||
<Sidebar.MenuButton isActive={active(serverPath + '/update')} tooltipContent="Update">
|
<Sidebar.MenuButton isActive={active(serverPath + '/update')} tooltipContent="Обновление">
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<a href="{serverPath}/update" {...props}>
|
<a href="{serverPath}/update" {...props}>
|
||||||
<RefreshCwIcon />
|
<RefreshCwIcon />
|
||||||
<span>Update</span>
|
<span>Обновление</span>
|
||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Sidebar.MenuButton>
|
</Sidebar.MenuButton>
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet Fallback()}
|
{#snippet Fallback()}
|
||||||
<span>Next</span>
|
<span>Следующая</span>
|
||||||
<ChevronRightIcon class="size-4" />
|
<ChevronRightIcon class="size-4" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<PaginationPrimitive.NextButton
|
<PaginationPrimitive.NextButton
|
||||||
bind:ref
|
bind:ref
|
||||||
aria-label="Go to next page"
|
aria-label="Перейти на следующую страницу"
|
||||||
class={cn(buttonVariants({ variant: "ghost" }), "pr-1.5!", className)}
|
class={cn(buttonVariants({ variant: "ghost" }), "pr-1.5!", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to next page"
|
aria-label="Перейти на следующую страницу"
|
||||||
size="default"
|
size="default"
|
||||||
class={cn("pr-1.5!", className)}
|
class={cn("pr-1.5!", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<span class="cn-pagination-next-text hidden sm:block">Next</span>
|
<span class="cn-pagination-next-text hidden sm:block">Следующая</span>
|
||||||
<ChevronRightIcon data-icon="inline-end" />
|
<ChevronRightIcon data-icon="inline-end" />
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
|
|||||||
@@ -14,12 +14,12 @@
|
|||||||
|
|
||||||
{#snippet Fallback()}
|
{#snippet Fallback()}
|
||||||
<ChevronLeftIcon class="size-4" />
|
<ChevronLeftIcon class="size-4" />
|
||||||
<span>Previous</span>
|
<span>Предыдущая</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<PaginationPrimitive.PrevButton
|
<PaginationPrimitive.PrevButton
|
||||||
bind:ref
|
bind:ref
|
||||||
aria-label="Go to previous page"
|
aria-label="Перейти на предыдущую страницу"
|
||||||
class={cn(buttonVariants({ variant: "ghost" }), "pl-1.5!", className)}
|
class={cn(buttonVariants({ variant: "ghost" }), "pl-1.5!", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to previous page"
|
aria-label="Перейти на предыдущую страницу"
|
||||||
size="default"
|
size="default"
|
||||||
class={cn("pl-1.5!", className)}
|
class={cn("pl-1.5!", className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon data-icon="inline-start" />
|
<ChevronLeftIcon data-icon="inline-start" />
|
||||||
<span class="cn-pagination-previous-text hidden sm:block">Previous</span>
|
<span class="cn-pagination-previous-text hidden sm:block">Предыдущая</span>
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
bind:ref
|
bind:ref
|
||||||
bind:page
|
bind:page
|
||||||
role="navigation"
|
role="navigation"
|
||||||
aria-label="pagination"
|
aria-label="Пагинация"
|
||||||
data-slot="pagination"
|
data-slot="pagination"
|
||||||
{count}
|
{count}
|
||||||
{perPage}
|
{perPage}
|
||||||
|
|||||||
@@ -51,8 +51,8 @@
|
|||||||
{side}
|
{side}
|
||||||
>
|
>
|
||||||
<Sheet.Header class="sr-only">
|
<Sheet.Header class="sr-only">
|
||||||
<Sheet.Title>Sidebar</Sheet.Title>
|
<Sheet.Title>Боковая панель</Sheet.Title>
|
||||||
<Sheet.Description>Displays the mobile sidebar.</Sheet.Description>
|
<Sheet.Description>Отображает мобильную боковую панель.</Sheet.Description>
|
||||||
</Sheet.Header>
|
</Sheet.Header>
|
||||||
<div class="flex h-full w-full flex-col">
|
<div class="flex h-full w-full flex-col">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ const SEG_LABELS: Record<string, string> = {
|
|||||||
users: 'Пользователи',
|
users: 'Пользователи',
|
||||||
ips: 'Уникальные IP',
|
ips: 'Уникальные IP',
|
||||||
incidents: 'Инциденты',
|
incidents: 'Инциденты',
|
||||||
live: 'Live',
|
live: 'Поток',
|
||||||
mihomo: 'Mihomo',
|
mihomo: 'Mihomo',
|
||||||
servers: 'Серверы',
|
servers: 'Серверы',
|
||||||
runtime: 'Runtime',
|
runtime: 'Состояние',
|
||||||
security: 'Security',
|
security: 'Безопасность',
|
||||||
upstreams: 'Upstreams и DC',
|
upstreams: 'Апстримы и ДЦ',
|
||||||
config: 'Конфигурация',
|
config: 'Конфигурация',
|
||||||
update: 'Обновление'
|
update: 'Обновление'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import './layout.css';
|
import './layout.css';
|
||||||
import { page } from '$app/state';
|
import { page, navigating } from '$app/state';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { ModeWatcher } from 'mode-watcher';
|
import { ModeWatcher } from 'mode-watcher';
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
const docTitle = $derived(`${panelTitleFromPath(page.url.pathname)} · Telemt Panel`);
|
const docTitle = $derived(`${panelTitleFromPath(page.url.pathname)} · Telemt Panel`);
|
||||||
|
const isNavigating = $derived(navigating !== null);
|
||||||
|
|
||||||
let serverAliases = $state<string[]>([]);
|
let serverAliases = $state<string[]>([]);
|
||||||
|
|
||||||
@@ -54,6 +55,11 @@
|
|||||||
<Sidebar.Provider>
|
<Sidebar.Provider>
|
||||||
<AppSidebar {serverAliases} {serverPath} />
|
<AppSidebar {serverAliases} {serverPath} />
|
||||||
<Sidebar.Inset>
|
<Sidebar.Inset>
|
||||||
|
{#if isNavigating}
|
||||||
|
<div class="h-0.5 w-full overflow-hidden bg-transparent">
|
||||||
|
<div class="h-full w-1/3 animate-pulse rounded-full bg-primary"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<header class="flex h-14 w-full shrink-0 items-center gap-2 border-b border-border px-4">
|
<header class="flex h-14 w-full shrink-0 items-center gap-2 border-b border-border px-4">
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -572,7 +572,7 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
{summary.fleet_total_connections}
|
{summary.fleet_total_connections}
|
||||||
</KpiStatCard>
|
</KpiStatCard>
|
||||||
<KpiStatCard accent="danger" label="Bad connections" footer="сумма по нодам" valueClass="text-xl">
|
<KpiStatCard accent="danger" label="Плохие соединения" footer="сумма по нодам" valueClass="text-xl">
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
<ShieldAlertIcon class="size-5" aria-hidden="true" />
|
<ShieldAlertIcon class="size-5" aria-hidden="true" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -124,6 +124,12 @@
|
|||||||
return 'outline';
|
return 'outline';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function severityLabel(sev: IncidentItem['severity']): string {
|
||||||
|
if (sev === 'critical') return 'критично';
|
||||||
|
if (sev === 'warning') return 'предупреждение';
|
||||||
|
return 'инфо';
|
||||||
|
}
|
||||||
|
|
||||||
function restartPolling() {
|
function restartPolling() {
|
||||||
if (pollTimer) {
|
if (pollTimer) {
|
||||||
clearInterval(pollTimer);
|
clearInterval(pollTimer);
|
||||||
@@ -283,7 +289,7 @@
|
|||||||
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
|
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-semibold tracking-tight">Инциденты</h1>
|
<h1 class="text-2xl font-semibold tracking-tight">Инциденты</h1>
|
||||||
<p class="text-sm text-muted-foreground">Сводка и triage по всему флоту.</p>
|
<p class="text-sm text-muted-foreground">Сводка и разбор инцидентов по всему флоту.</p>
|
||||||
{#if generatedAt}
|
{#if generatedAt}
|
||||||
<p class="mt-1 text-xs text-muted-foreground">Снимок: {new Date(generatedAt).toLocaleString()}</p>
|
<p class="mt-1 text-xs text-muted-foreground">Снимок: {new Date(generatedAt).toLocaleString()}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -331,38 +337,38 @@
|
|||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
<div class="mb-4 grid gap-4 sm:grid-cols-3">
|
<div class="mb-4 grid gap-4 sm:grid-cols-3">
|
||||||
<KpiTrendCard
|
<KpiTrendCard
|
||||||
label="Critical"
|
label="Критичные"
|
||||||
valueDisplay={String(data?.critical_total ?? 0)}
|
valueDisplay={String(data?.critical_total ?? 0)}
|
||||||
prevValue={kpiPrev?.critical ?? null}
|
prevValue={kpiPrev?.critical ?? null}
|
||||||
currentValue={data != null ? (data.critical_total ?? 0) : null}
|
currentValue={data != null ? (data.critical_total ?? 0) : null}
|
||||||
invertTrend={true}
|
invertTrend={true}
|
||||||
insight="Открытые critical; меньше — лучше. К прошлому опросу."
|
insight="Открытые критичные; меньше — лучше. К прошлому опросу."
|
||||||
/>
|
/>
|
||||||
<KpiTrendCard
|
<KpiTrendCard
|
||||||
label="Warning"
|
label="Предупреждения"
|
||||||
valueDisplay={String(data?.warning_total ?? 0)}
|
valueDisplay={String(data?.warning_total ?? 0)}
|
||||||
prevValue={kpiPrev?.warning ?? null}
|
prevValue={kpiPrev?.warning ?? null}
|
||||||
currentValue={data != null ? (data.warning_total ?? 0) : null}
|
currentValue={data != null ? (data.warning_total ?? 0) : null}
|
||||||
invertTrend={true}
|
invertTrend={true}
|
||||||
insight="Открытые warning; меньше — лучше."
|
insight="Открытые предупреждения; меньше — лучше."
|
||||||
/>
|
/>
|
||||||
<KpiTrendCard
|
<KpiTrendCard
|
||||||
label="Info"
|
label="Инфо"
|
||||||
valueDisplay={String(data?.info_total ?? 0)}
|
valueDisplay={String(data?.info_total ?? 0)}
|
||||||
prevValue={kpiPrev?.info ?? null}
|
prevValue={kpiPrev?.info ?? null}
|
||||||
currentValue={data != null ? (data.info_total ?? 0) : null}
|
currentValue={data != null ? (data.info_total ?? 0) : null}
|
||||||
insight="Открытые info. К прошлому опросу."
|
insight="Открытые инфо. К прошлому опросу."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTableCard title="Инциденты" description="Список и triage">
|
<DataTableCard title="Инциденты" description="Список и разбор">
|
||||||
{#snippet toolbar()}
|
{#snippet toolbar()}
|
||||||
<DataTableToolbar>
|
<DataTableToolbar>
|
||||||
<div class="relative max-w-md flex-1">
|
<div class="relative max-w-md flex-1">
|
||||||
<SearchIcon
|
<SearchIcon
|
||||||
class="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground"
|
class="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<Input class="h-8 ps-8" placeholder="Поиск по заголовку, summary, severity…" bind:value={incSearch} />
|
<Input class="h-8 ps-8" placeholder="Поиск по заголовку, описанию, критичности…" bind:value={incSearch} />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -384,7 +390,7 @@
|
|||||||
active={incSortKey === 'sev'}
|
active={incSortKey === 'sev'}
|
||||||
dir={incSortDir}
|
dir={incSortDir}
|
||||||
onToggle={() => toggleIncSort('sev')}
|
onToggle={() => toggleIncSort('sev')}
|
||||||
>Severity</SortableTh>
|
>Критичность</SortableTh>
|
||||||
<SortableTh
|
<SortableTh
|
||||||
active={incSortKey === 'title'}
|
active={incSortKey === 'title'}
|
||||||
dir={incSortDir}
|
dir={incSortDir}
|
||||||
@@ -406,8 +412,8 @@
|
|||||||
</Tooltip.Root>
|
</Tooltip.Root>
|
||||||
</div>
|
</div>
|
||||||
</Table.Head>
|
</Table.Head>
|
||||||
<Table.Head>Runbook</Table.Head>
|
<Table.Head>Инструкции</Table.Head>
|
||||||
<Table.Head>Triage</Table.Head>
|
<Table.Head>Разбор</Table.Head>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
@@ -427,7 +433,7 @@
|
|||||||
{#each filteredIncidents as item (item.id)}
|
{#each filteredIncidents as item (item.id)}
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Badge variant={severityVariant(item.severity)}>{item.severity}</Badge>
|
<Badge variant={severityVariant(item.severity)}>{severityLabel(item.severity)}</Badge>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell class="max-w-[360px]">
|
<Table.Cell class="max-w-[360px]">
|
||||||
<div class="font-medium">{item.title}</div>
|
<div class="font-medium">{item.title}</div>
|
||||||
@@ -458,21 +464,21 @@
|
|||||||
variant={triage.ack ? 'default' : 'outline'}
|
variant={triage.ack ? 'default' : 'outline'}
|
||||||
onclick={() => toggleAck(item.id)}
|
onclick={() => toggleAck(item.id)}
|
||||||
>
|
>
|
||||||
{triage.ack ? 'unack' : 'ack'}
|
{triage.ack ? 'Снять подтверждение' : 'Подтвердить'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="xs"
|
size="xs"
|
||||||
variant={triage.resolved ? 'default' : 'outline'}
|
variant={triage.resolved ? 'default' : 'outline'}
|
||||||
onclick={() => toggleResolved(item.id)}
|
onclick={() => toggleResolved(item.id)}
|
||||||
>
|
>
|
||||||
{triage.resolved ? 'unresolve' : 'resolve'}
|
{triage.resolved ? 'Вернуть в работу' : 'Решить'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
class="h-8 w-full text-xs"
|
class="h-8 w-full text-xs"
|
||||||
placeholder="owner"
|
placeholder="Ответственный"
|
||||||
value={triage.owner ?? ''}
|
value={triage.owner ?? ''}
|
||||||
oninput={(e) =>
|
oninput={(e) =>
|
||||||
patchTriage(item.id, {
|
patchTriage(item.id, {
|
||||||
@@ -484,7 +490,7 @@
|
|||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
class="h-8 w-full text-xs"
|
class="h-8 w-full text-xs"
|
||||||
placeholder="note"
|
placeholder="Примечание"
|
||||||
value={triage.note ?? ''}
|
value={triage.note ?? ''}
|
||||||
oninput={(e) =>
|
oninput={(e) =>
|
||||||
patchTriage(item.id, {
|
patchTriage(item.id, {
|
||||||
|
|||||||
@@ -106,12 +106,24 @@
|
|||||||
return 'outline';
|
return 'outline';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function severityLabel(sev: IncidentSeverity): string {
|
||||||
|
if (sev === 'critical') return 'критично';
|
||||||
|
if (sev === 'warning') return 'предупреждение';
|
||||||
|
return 'инфо';
|
||||||
|
}
|
||||||
|
|
||||||
function statusVariant(st: LiveStatus | undefined): 'default' | 'secondary' | 'destructive' {
|
function statusVariant(st: LiveStatus | undefined): 'default' | 'secondary' | 'destructive' {
|
||||||
if (st === 'critical') return 'destructive';
|
if (st === 'critical') return 'destructive';
|
||||||
if (st === 'degraded') return 'secondary';
|
if (st === 'degraded') return 'secondary';
|
||||||
return 'default';
|
return 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function statusLabel(st: LiveStatus | undefined): string {
|
||||||
|
if (st === 'critical') return 'критично';
|
||||||
|
if (st === 'degraded') return 'деградация';
|
||||||
|
return 'норма';
|
||||||
|
}
|
||||||
|
|
||||||
function parseAliasFilter(raw: string | null): string {
|
function parseAliasFilter(raw: string | null): string {
|
||||||
const value = raw?.trim() ?? '';
|
const value = raw?.trim() ?? '';
|
||||||
return !value || value.includes(',') ? 'all' : value;
|
return !value || value.includes(',') ? 'all' : value;
|
||||||
@@ -312,12 +324,12 @@
|
|||||||
|
|
||||||
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
|
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-semibold tracking-tight">Live</h1>
|
<h1 class="text-2xl font-semibold tracking-tight">Поток</h1>
|
||||||
<p class="text-sm text-muted-foreground">SSE поток инцидентов и статуса флота.</p>
|
<p class="text-sm text-muted-foreground">SSE поток инцидентов и статуса флота.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant={connected ? 'default' : 'secondary'}>
|
<Badge variant={connected ? 'default' : 'secondary'}>
|
||||||
{connected ? 'connected' : 'disconnected'}
|
{connected ? 'подключено' : 'отключено'}
|
||||||
</Badge>
|
</Badge>
|
||||||
<FleetLiveStaleBadge
|
<FleetLiveStaleBadge
|
||||||
{isStale}
|
{isStale}
|
||||||
@@ -329,7 +341,7 @@
|
|||||||
/>
|
/>
|
||||||
<Button variant="outline" size="sm" onclick={() => connectStream()}>
|
<Button variant="outline" size="sm" onclick={() => connectStream()}>
|
||||||
<RefreshCwIcon class="mr-1 size-4" />
|
<RefreshCwIcon class="mr-1 size-4" />
|
||||||
Reconnect
|
Переподключить
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -340,11 +352,11 @@
|
|||||||
|
|
||||||
{#if err}
|
{#if err}
|
||||||
<Alert class="mb-4">
|
<Alert class="mb-4">
|
||||||
<AlertTitle>Live stream</AlertTitle>
|
<AlertTitle>Поток событий</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{err}
|
{err}
|
||||||
{#if reconnectInSec != null}
|
{#if reconnectInSec != null}
|
||||||
Переподключение через {reconnectInSec}s.
|
Переподключение через {reconnectInSec} с.
|
||||||
{/if}
|
{/if}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
@@ -355,19 +367,17 @@
|
|||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
<ActivityIcon class="size-5" aria-hidden="true" />
|
<ActivityIcon class="size-5" aria-hidden="true" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<Badge variant={statusVariant(snapshot?.status)}>
|
<Badge variant={statusVariant(snapshot?.status)}>{statusLabel(snapshot?.status)}</Badge>
|
||||||
{snapshot?.status ?? 'healthy'}
|
|
||||||
</Badge>
|
|
||||||
</KpiStatCard>
|
</KpiStatCard>
|
||||||
<KpiStatCard accent="warning" label="Partial" footer="неполный снимок">
|
<KpiStatCard accent="warning" label="Частичность" footer="неполный снимок">
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
<LayersIcon class="size-5" aria-hidden="true" />
|
<LayersIcon class="size-5" aria-hidden="true" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{snapshot?.partial ? 'yes' : 'no'}
|
{snapshot?.partial ? 'да' : 'нет'}
|
||||||
</KpiStatCard>
|
</KpiStatCard>
|
||||||
<KpiStatCard
|
<KpiStatCard
|
||||||
accent="info"
|
accent="info"
|
||||||
label="Timestamp"
|
label="Временная метка"
|
||||||
footer="время snapshot"
|
footer="время snapshot"
|
||||||
valueClass="text-base font-normal tabular-nums leading-tight"
|
valueClass="text-base font-normal tabular-nums leading-tight"
|
||||||
>
|
>
|
||||||
@@ -379,7 +389,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTableCard
|
<DataTableCard
|
||||||
title="Последние incidents из snapshot"
|
title="Последние инциденты из снимка"
|
||||||
description="Всего: {snapshot?.counts?.total ?? snapshot?.incidents?.length ?? 0}"
|
description="Всего: {snapshot?.counts?.total ?? snapshot?.incidents?.length ?? 0}"
|
||||||
>
|
>
|
||||||
{#snippet toolbar()}
|
{#snippet toolbar()}
|
||||||
@@ -388,7 +398,7 @@
|
|||||||
<SearchIcon
|
<SearchIcon
|
||||||
class="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground"
|
class="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<Input class="h-8 ps-8" placeholder="Поиск по severity, title, summary…" bind:value={liveIncSearch} />
|
<Input class="h-8 ps-8" placeholder="Поиск по критичности, заголовку, описанию…" bind:value={liveIncSearch} />
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -410,13 +420,13 @@
|
|||||||
active={liveSortKey === 'sev'}
|
active={liveSortKey === 'sev'}
|
||||||
dir={liveSortDir}
|
dir={liveSortDir}
|
||||||
onToggle={() => toggleLiveSort('sev')}
|
onToggle={() => toggleLiveSort('sev')}
|
||||||
>Severity</SortableTh>
|
>Критичность</SortableTh>
|
||||||
<SortableTh
|
<SortableTh
|
||||||
active={liveSortKey === 'title'}
|
active={liveSortKey === 'title'}
|
||||||
dir={liveSortDir}
|
dir={liveSortDir}
|
||||||
onToggle={() => toggleLiveSort('title')}
|
onToggle={() => toggleLiveSort('title')}
|
||||||
>Title</SortableTh>
|
>Заголовок</SortableTh>
|
||||||
<Table.Head>Summary</Table.Head>
|
<Table.Head>Описание</Table.Head>
|
||||||
<Table.Head>
|
<Table.Head>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
Серверы
|
Серверы
|
||||||
@@ -452,7 +462,7 @@
|
|||||||
{#each filteredLiveIncidents as item (item.id)}
|
{#each filteredLiveIncidents as item (item.id)}
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Badge variant={severityVariant(item.severity)}>{item.severity}</Badge>
|
<Badge variant={severityVariant(item.severity)}>{severityLabel(item.severity)}</Badge>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
<Table.Cell class="font-medium">{item.title}</Table.Cell>
|
<Table.Cell class="font-medium">{item.title}</Table.Cell>
|
||||||
<Table.Cell class="max-w-[380px] text-sm text-muted-foreground">{item.summary}</Table.Cell>
|
<Table.Cell class="max-w-[380px] text-sm text-muted-foreground">{item.summary}</Table.Cell>
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
</KpiStatCard>
|
</KpiStatCard>
|
||||||
<KpiStatCard
|
<KpiStatCard
|
||||||
accent="danger"
|
accent="danger"
|
||||||
label="Bad connections"
|
label="Плохие соединения"
|
||||||
valueClass="text-xl"
|
valueClass="text-xl"
|
||||||
>
|
>
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
@@ -146,7 +146,7 @@
|
|||||||
</KpiStatCard>
|
</KpiStatCard>
|
||||||
<KpiStatCard
|
<KpiStatCard
|
||||||
accent="warning"
|
accent="warning"
|
||||||
label="Handshake timeouts"
|
label="Таймауты рукопожатия"
|
||||||
valueClass="text-xl"
|
valueClass="text-xl"
|
||||||
>
|
>
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ServerPageHeader
|
<ServerPageHeader
|
||||||
title="Configuration"
|
title="Конфигурация"
|
||||||
subtitle="Конфигурация этой ноды не отдается через Control API"
|
subtitle="Конфигурация этой ноды не отдается через Control API"
|
||||||
showRefresh={false}
|
showRefresh={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -57,22 +57,22 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ServerPageHeader title="Runtime" {loading} onRefresh={load} />
|
<ServerPageHeader title="Состояние" {loading} onRefresh={load} />
|
||||||
|
|
||||||
<ServerQueryState {loading} {err} isEmpty={false}>
|
<ServerQueryState {loading} {err} isEmpty={false}>
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
<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">Приём новых: {onOff(gates.accepting_new_connections)}</Badge>
|
||||||
<Badge variant="secondary" class="px-3 py-1">conditional cast: {onOff(gates.conditional_cast_enabled)}</Badge>
|
<Badge variant="secondary" class="px-3 py-1">Условный cast: {onOff(gates.conditional_cast_enabled)}</Badge>
|
||||||
<Badge variant="secondary" class="px-3 py-1">me2dc fallback: {onOff(gates.me2dc_fallback_enabled)}</Badge>
|
<Badge variant="secondary" class="px-3 py-1">Резерв me2dc: {onOff(gates.me2dc_fallback_enabled)}</Badge>
|
||||||
<Badge variant="secondary" class="px-3 py-1">ME ready: {onOff(gates.me_runtime_ready)}</Badge>
|
<Badge variant="secondary" class="px-3 py-1">ME готов: {onOff(gates.me_runtime_ready)}</Badge>
|
||||||
<Badge variant="outline" class="px-3 py-1">middle proxy: {onOff(gates.use_middle_proxy)}</Badge>
|
<Badge variant="outline" class="px-3 py-1">Промежуточный прокси: {onOff(gates.use_middle_proxy)}</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if dcs && dcs.middle_proxy_enabled && dcs.dcs}
|
{#if dcs && dcs.middle_proxy_enabled && dcs.dcs}
|
||||||
<KpiPanelCard class="mb-6" accent="info" title="Datacenter status" contentClass="p-0">
|
<KpiPanelCard class="mb-6" accent="info" title="Статус дата-центров" contentClass="p-0">
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
<BuildingIcon class="size-5" aria-hidden="true" />
|
<BuildingIcon class="size-5" aria-hidden="true" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -81,8 +81,8 @@
|
|||||||
<Table.Row class="border-b border-border/80 hover:bg-transparent">
|
<Table.Row class="border-b border-border/80 hover:bg-transparent">
|
||||||
<Table.Head>DC</Table.Head>
|
<Table.Head>DC</Table.Head>
|
||||||
<Table.Head class="text-right">RTT</Table.Head>
|
<Table.Head class="text-right">RTT</Table.Head>
|
||||||
<Table.Head class="text-right">Writers</Table.Head>
|
<Table.Head class="text-right">Писатели</Table.Head>
|
||||||
<Table.Head class="text-right">Coverage</Table.Head>
|
<Table.Head class="text-right">Покрытие</Table.Head>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Header>
|
</Table.Header>
|
||||||
<Table.Body>
|
<Table.Body>
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
</KpiPanelCard>
|
</KpiPanelCard>
|
||||||
{:else if dcs?.reason}
|
{:else if dcs?.reason}
|
||||||
<Alert class="mb-4">
|
<Alert class="mb-4">
|
||||||
<AlertDescription>DC status: {dcs.reason}</AlertDescription>
|
<AlertDescription>Статус ДЦ: {dcs.reason}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -38,12 +38,12 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ServerPageHeader title="Security" {loading} onRefresh={load} />
|
<ServerPageHeader title="Безопасность" {loading} onRefresh={load} />
|
||||||
|
|
||||||
<ServerQueryState {loading} {err} isEmpty={!data} emptyDescription="Endpoint вернул пустой ответ.">
|
<ServerQueryState {loading} {err} isEmpty={!data} emptyDescription="Endpoint вернул пустой ответ.">
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
{#if data}
|
{#if data}
|
||||||
<ServerJsonCard accent="danger" title="Security posture" description="POST /security/posture" payload={data}>
|
<ServerJsonCard accent="danger" title="Профиль безопасности" description="POST /security/posture" payload={data}>
|
||||||
{#snippet iconSnippet()}
|
{#snippet iconSnippet()}
|
||||||
<ShieldIcon class="size-5" aria-hidden="true" />
|
<ShieldIcon class="size-5" aria-hidden="true" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ServerPageHeader title="Update" {loading} onRefresh={load} />
|
<ServerPageHeader title="Обновление" {loading} onRefresh={load} />
|
||||||
|
|
||||||
<ServerQueryState {loading} {err} isEmpty={!sys} emptyDescription="Информация о бинарнике не получена.">
|
<ServerQueryState {loading} {err} isEmpty={!sys} emptyDescription="Информация о бинарнике не получена.">
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
|
|||||||
@@ -38,14 +38,14 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ServerPageHeader title="Upstreams & DCs" {loading} onRefresh={load} />
|
<ServerPageHeader title="Апстримы и ДЦ" {loading} onRefresh={load} />
|
||||||
|
|
||||||
<ServerQueryState {loading} {err} isEmpty={!data} emptyDescription="Статистика upstream-ов пока не доступна.">
|
<ServerQueryState {loading} {err} isEmpty={!data} emptyDescription="Статистика upstream-ов пока не доступна.">
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
{#if data}
|
{#if data}
|
||||||
<ServerJsonCard
|
<ServerJsonCard
|
||||||
accent="info"
|
accent="info"
|
||||||
title="Upstreams & DCs"
|
title="Апстримы и ДЦ"
|
||||||
description="stats/upstreams"
|
description="stats/upstreams"
|
||||||
payload={data}
|
payload={data}
|
||||||
preClass="max-h-[70vh]"
|
preClass="max-h-[70vh]"
|
||||||
|
|||||||
@@ -199,7 +199,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
|
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
|
||||||
<h1 class="text-2xl font-semibold tracking-tight">Users</h1>
|
<h1 class="text-2xl font-semibold tracking-tight">Пользователи</h1>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button onclick={() => (createOpen = true)} disabled={readOnly}>
|
<Button onclick={() => (createOpen = true)} disabled={readOnly}>
|
||||||
<PlusIcon class="mr-1 size-4" />
|
<PlusIcon class="mr-1 size-4" />
|
||||||
@@ -236,7 +236,7 @@
|
|||||||
<Table.Row class="border-b border-border/80 hover:bg-transparent">
|
<Table.Row class="border-b border-border/80 hover:bg-transparent">
|
||||||
<Table.Head>Имя</Table.Head>
|
<Table.Head>Имя</Table.Head>
|
||||||
<Table.Head>Ссылки</Table.Head>
|
<Table.Head>Ссылки</Table.Head>
|
||||||
<Table.Head class="text-right">Conn</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 class="w-[100px]"></Table.Head>
|
<Table.Head class="w-[100px]"></Table.Head>
|
||||||
@@ -248,9 +248,9 @@
|
|||||||
<Table.Cell class="font-medium">{u.username}</Table.Cell>
|
<Table.Cell class="font-medium">{u.username}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="flex flex-wrap gap-1">
|
||||||
{#each u.links?.tls ?? [] as link (link)}
|
{#each u.links?.tls ?? [] as link, idx (`${link}:${idx}`)}
|
||||||
<Button variant="outline" size="xs" class="h-7" onclick={() => copy(link)}>
|
<Button variant="outline" size="xs" class="h-7" onclick={() => copy(link)}>
|
||||||
TLS <CopyIcon class="size-3" />
|
TLS {idx + 1} <CopyIcon class="size-3" />
|
||||||
</Button>
|
</Button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -269,6 +269,8 @@
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
onclick={() => openEdit(u)}
|
onclick={() => openEdit(u)}
|
||||||
|
aria-label="Редактировать пользователя"
|
||||||
|
title="Редактировать пользователя"
|
||||||
>
|
>
|
||||||
<PencilIcon class="size-4" />
|
<PencilIcon class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -277,6 +279,8 @@
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
onclick={() => (deleteUserRow = u)}
|
onclick={() => (deleteUserRow = u)}
|
||||||
|
aria-label="Удалить пользователя"
|
||||||
|
title="Удалить пользователя"
|
||||||
>
|
>
|
||||||
<TrashIcon class="size-4 text-destructive" />
|
<TrashIcon class="size-4 text-destructive" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -325,15 +329,15 @@
|
|||||||
<Input id="eq" bind:value={editQuota} type="number" min="0" />
|
<Input id="eq" bind:value={editQuota} type="number" min="0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="em">max_unique_ips</Label>
|
<Label for="em">Макс. уникальных IP (max_unique_ips)</Label>
|
||||||
<Input id="em" bind:value={editMaxIp} type="number" min="0" />
|
<Input id="em" bind:value={editMaxIp} type="number" min="0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="et">max_tcp_conns</Label>
|
<Label for="et">Макс. TCP-соединений (max_tcp_conns)</Label>
|
||||||
<Input id="et" bind:value={editMaxTcp} type="number" min="0" />
|
<Input id="et" bind:value={editMaxTcp} type="number" min="0" />
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="ex">expiry (RFC3339)</Label>
|
<Label for="ex">Срок действия (RFC3339)</Label>
|
||||||
<Input id="ex" bind:value={editExp} class="font-mono text-sm" />
|
<Input id="ex" bind:value={editExp} class="font-mono text-sm" />
|
||||||
</div>
|
</div>
|
||||||
{#if formErr}
|
{#if formErr}
|
||||||
|
|||||||
Reference in New Issue
Block a user