Update package dependencies and refactor state management in Svelte components
- Added `mode-watcher` and `svelte-sonner` as dependencies to enhance application functionality. - Refactored state management in various Svelte components to use `$app/state` instead of `$app/stores`, improving consistency across the application. - Updated UI components to utilize new features from the added dependencies, enhancing user experience and responsiveness. - Improved loading states and error handling in data-fetching components, ensuring better feedback during asynchronous operations.
This commit is contained in:
Generated
+99
@@ -22,9 +22,11 @@
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"openapi-typescript": "^7.13.0",
|
||||
"svelte": "^5.54.0",
|
||||
"svelte-check": "^4.4.2",
|
||||
"svelte-sonner": "^1.1.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
@@ -2084,6 +2086,73 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mode-watcher": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz",
|
||||
"integrity": "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"runed": "^0.25.0",
|
||||
"svelte-toolbelt": "^0.7.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.27.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mode-watcher/node_modules/runed": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/runed/-/runed-0.25.0.tgz",
|
||||
"integrity": "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/huntabyte",
|
||||
"https://github.com/sponsors/tglide"
|
||||
],
|
||||
"dependencies": {
|
||||
"esm-env": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mode-watcher/node_modules/svelte-toolbelt": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz",
|
||||
"integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/huntabyte"
|
||||
],
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
"runed": "^0.23.2",
|
||||
"style-to-object": "^1.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"pnpm": ">=8.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mode-watcher/node_modules/svelte-toolbelt/node_modules/runed": {
|
||||
"version": "0.23.4",
|
||||
"resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz",
|
||||
"integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/huntabyte",
|
||||
"https://github.com/sponsors/tglide"
|
||||
],
|
||||
"dependencies": {
|
||||
"esm-env": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@@ -2450,6 +2519,36 @@
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-sonner": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.1.0.tgz",
|
||||
"integrity": "sha512-3lYM6ZIqWe+p9vwwWHGWP/ZdvHiUtzURsud2quIxivrX4rvpXh6i+geBGn0m3JS6KwW6W8VgbOl3xQMcDuh6gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"runed": "^0.28.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-sonner/node_modules/runed": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/runed/-/runed-0.28.0.tgz",
|
||||
"integrity": "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/huntabyte",
|
||||
"https://github.com/sponsors/tglide"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esm-env": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-toolbelt": {
|
||||
"version": "0.10.6",
|
||||
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz",
|
||||
|
||||
@@ -19,9 +19,11 @@
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"openapi-typescript": "^7.13.0",
|
||||
"svelte": "^5.54.0",
|
||||
"svelte-check": "^4.4.2",
|
||||
"svelte-sonner": "^1.1.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import LayoutDashboardIcon from '@lucide/svelte/icons/layout-dashboard';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import GlobeIcon from '@lucide/svelte/icons/globe';
|
||||
@@ -22,7 +22,7 @@
|
||||
} = $props();
|
||||
|
||||
function active(href: string): boolean {
|
||||
const p = $page.url.pathname;
|
||||
const p = page.url.pathname;
|
||||
if (href === '/') return p === '/';
|
||||
return p === href || p.startsWith(href + '/');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
type PartialVariant = 'default' | 'destructive';
|
||||
|
||||
let {
|
||||
partial = false,
|
||||
partialVariant = 'default' as PartialVariant,
|
||||
partialTitle = 'Частичные данные',
|
||||
partialDescription = 'Не все upstream ответили успешно.',
|
||||
partialSnippet,
|
||||
err = null as string | null,
|
||||
hasStaleData = false,
|
||||
showSkeleton = false,
|
||||
isEmpty = false,
|
||||
children,
|
||||
skeleton,
|
||||
empty
|
||||
}: {
|
||||
partial?: boolean;
|
||||
partialVariant?: PartialVariant;
|
||||
partialTitle?: string;
|
||||
partialDescription?: string;
|
||||
partialSnippet?: Snippet;
|
||||
err?: string | null;
|
||||
hasStaleData?: boolean;
|
||||
showSkeleton?: boolean;
|
||||
isEmpty?: boolean;
|
||||
children?: Snippet;
|
||||
skeleton?: Snippet;
|
||||
empty?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#if partial}
|
||||
{#if partialSnippet}
|
||||
{@render partialSnippet()}
|
||||
{:else}
|
||||
<Alert variant={partialVariant} class="mb-4">
|
||||
<AlertTitle>{partialTitle}</AlertTitle>
|
||||
<AlertDescription>{partialDescription}</AlertDescription>
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if err}
|
||||
<Alert variant="destructive" class="mb-4">
|
||||
<AlertTitle>{hasStaleData ? 'Ошибка обновления' : 'Ошибка'}</AlertTitle>
|
||||
<AlertDescription>{err}</AlertDescription>
|
||||
{#if hasStaleData}
|
||||
<p class="mt-2 text-xs text-muted-foreground">Показаны последние успешные данные.</p>
|
||||
{/if}
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{#if showSkeleton}
|
||||
{@render skeleton?.()}
|
||||
{:else if err && !hasStaleData}
|
||||
<!-- блокируем контент при первичной ошибке -->
|
||||
{:else if isEmpty}
|
||||
{@render empty?.()}
|
||||
{:else}
|
||||
<div class={cn(err && hasStaleData && 'opacity-90')}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
|
||||
let {
|
||||
isStale,
|
||||
staleSeconds,
|
||||
refreshSeconds,
|
||||
staleMultiplier = 2,
|
||||
tooltipOverride
|
||||
}: {
|
||||
isStale: boolean;
|
||||
staleSeconds: number | null;
|
||||
refreshSeconds: number;
|
||||
staleMultiplier?: number;
|
||||
/** Полный текст тултипа (например для Live/SSE с фиксированным порогом). */
|
||||
tooltipOverride?: string;
|
||||
} = $props();
|
||||
|
||||
const thresholdSec = $derived(refreshSeconds > 0 ? refreshSeconds * staleMultiplier : 60);
|
||||
const tip = $derived(
|
||||
tooltipOverride ??
|
||||
(isStale
|
||||
? `Статус stale: нет успешного обновления дольше ${thresholdSec} с (порог ≈ ${staleMultiplier}× интервал автообновления).`
|
||||
: `Свежие данные: последний успешный снимок ${staleSeconds ?? 0} с назад.`)
|
||||
);
|
||||
</script>
|
||||
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger class="inline-flex">
|
||||
<Badge variant={isStale ? 'secondary' : 'default'}>
|
||||
{isStale ? 'stale' : 'live'}{#if staleSeconds != null} · {staleSeconds}s{/if}
|
||||
</Badge>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="bottom" class="max-w-xs">
|
||||
<p class="text-xs">{tip}</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
|
||||
let {
|
||||
id = 'fleet-refresh-sec',
|
||||
value = $bindable('30'),
|
||||
onValueChange
|
||||
}: {
|
||||
id?: string;
|
||||
value?: string;
|
||||
onValueChange?: () => void;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="grid w-fit gap-1.5">
|
||||
<Label for={id} class="text-xs text-muted-foreground">Refresh (sec)</Label>
|
||||
<Input
|
||||
{id}
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
class="h-9 w-28"
|
||||
bind:value
|
||||
onchange={onValueChange}
|
||||
/>
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import {
|
||||
buttonVariants,
|
||||
type ButtonVariant,
|
||||
type ButtonSize,
|
||||
} from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.ActionProps & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Action
|
||||
bind:ref
|
||||
data-slot="alert-dialog-action"
|
||||
class={cn(buttonVariants({ variant, size }), "cn-alert-dialog-action", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import {
|
||||
buttonVariants,
|
||||
type ButtonVariant,
|
||||
type ButtonSize,
|
||||
} from "$lib/components/ui/button/index.js";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
variant = "outline",
|
||||
size = "default",
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.CancelProps & {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Cancel
|
||||
bind:ref
|
||||
data-slot="alert-dialog-cancel"
|
||||
class={cn(buttonVariants({ variant, size }), "cn-alert-dialog-cancel", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import AlertDialogPortal from "./alert-dialog-portal.svelte";
|
||||
import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
|
||||
import { cn, type WithoutChild, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
size = "default",
|
||||
portalProps,
|
||||
...restProps
|
||||
}: WithoutChild<AlertDialogPrimitive.ContentProps> & {
|
||||
size?: "default" | "sm";
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof AlertDialogPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPortal {...portalProps}>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="alert-dialog-content"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 bg-background ring-foreground/10 gap-4 rounded-xl p-4 ring-1 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Description
|
||||
bind:ref
|
||||
data-slot="alert-dialog-description"
|
||||
class={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-dialog-footer"
|
||||
class={cn(
|
||||
"bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-dialog-header"
|
||||
class={cn("grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-dialog-media"
|
||||
class={cn("bg-muted mb-2 inline-flex size-10 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.OverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Overlay
|
||||
bind:ref
|
||||
data-slot="alert-dialog-overlay"
|
||||
class={cn("data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: AlertDialogPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: AlertDialogPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Title
|
||||
bind:ref
|
||||
data-slot="alert-dialog-title"
|
||||
class={cn("text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: AlertDialogPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Trigger bind:ref data-slot="alert-dialog-trigger" {...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||
|
||||
let { open = $bindable(false), ...restProps }: AlertDialogPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<AlertDialogPrimitive.Root bind:open {...restProps} />
|
||||
@@ -0,0 +1,40 @@
|
||||
import Root from "./alert-dialog.svelte";
|
||||
import Portal from "./alert-dialog-portal.svelte";
|
||||
import Trigger from "./alert-dialog-trigger.svelte";
|
||||
import Title from "./alert-dialog-title.svelte";
|
||||
import Action from "./alert-dialog-action.svelte";
|
||||
import Cancel from "./alert-dialog-cancel.svelte";
|
||||
import Footer from "./alert-dialog-footer.svelte";
|
||||
import Header from "./alert-dialog-header.svelte";
|
||||
import Overlay from "./alert-dialog-overlay.svelte";
|
||||
import Content from "./alert-dialog-content.svelte";
|
||||
import Description from "./alert-dialog-description.svelte";
|
||||
import Media from "./alert-dialog-media.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Title,
|
||||
Action,
|
||||
Cancel,
|
||||
Portal,
|
||||
Footer,
|
||||
Header,
|
||||
Trigger,
|
||||
Overlay,
|
||||
Content,
|
||||
Description,
|
||||
Media,
|
||||
//
|
||||
Root as AlertDialog,
|
||||
Title as AlertDialogTitle,
|
||||
Action as AlertDialogAction,
|
||||
Cancel as AlertDialogCancel,
|
||||
Portal as AlertDialogPortal,
|
||||
Footer as AlertDialogFooter,
|
||||
Header as AlertDialogHeader,
|
||||
Trigger as AlertDialogTrigger,
|
||||
Overlay as AlertDialogOverlay,
|
||||
Content as AlertDialogContent,
|
||||
Description as AlertDialogDescription,
|
||||
Media as AlertDialogMedia,
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from "bits-ui";
|
||||
import type { Snippet } from "svelte";
|
||||
import Command from "./command.svelte";
|
||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
showCloseButton = false,
|
||||
portalProps,
|
||||
children,
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<DialogPrimitive.RootProps> &
|
||||
WithoutChildrenOrChild<CommandPrimitive.RootProps> & {
|
||||
portalProps?: DialogPrimitive.PortalProps;
|
||||
children: Snippet;
|
||||
title?: string;
|
||||
description?: string;
|
||||
showCloseButton?: boolean;
|
||||
class?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open {...restProps}>
|
||||
<Dialog.Header class="sr-only">
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Description>{description}</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Content
|
||||
class={cn("rounded-xl! top-1/3 translate-y-0 overflow-hidden p-0", className)}
|
||||
{showCloseButton}
|
||||
{portalProps}
|
||||
>
|
||||
<Command {...restProps} bind:value bind:ref {children} />
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.EmptyProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Empty
|
||||
bind:ref
|
||||
data-slot="command-empty"
|
||||
class={cn("py-6 text-center text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive, useId } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
heading,
|
||||
value,
|
||||
...restProps
|
||||
}: CommandPrimitive.GroupProps & {
|
||||
heading?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Group
|
||||
bind:ref
|
||||
data-slot="command-group"
|
||||
class={cn("text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium", className)}
|
||||
value={value ?? heading ?? `----${useId()}`}
|
||||
{...restProps}
|
||||
>
|
||||
{#if heading}
|
||||
<CommandPrimitive.GroupHeading
|
||||
class="text-muted-foreground px-2 py-1.5 text-xs font-medium"
|
||||
>
|
||||
{heading}
|
||||
</CommandPrimitive.GroupHeading>
|
||||
{/if}
|
||||
<CommandPrimitive.GroupItems {children} />
|
||||
</CommandPrimitive.Group>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import * as InputGroup from "$lib/components/ui/input-group/index.js";
|
||||
import SearchIcon from '@lucide/svelte/icons/search';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
value = $bindable(""),
|
||||
...restProps
|
||||
}: CommandPrimitive.InputProps = $props();
|
||||
</script>
|
||||
|
||||
<div data-slot="command-input-wrapper" class="p-1 pb-0">
|
||||
<InputGroup.Root class="bg-input/30 border-input/30 h-8! rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!">
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
class={cn(
|
||||
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
bind:ref
|
||||
{...restProps}
|
||||
bind:value
|
||||
/>
|
||||
<InputGroup.Addon>
|
||||
<SearchIcon class="size-4 shrink-0 opacity-50" />
|
||||
</InputGroup.Addon>
|
||||
</InputGroup.Root>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: CommandPrimitive.ItemProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="command-item"
|
||||
class={cn(
|
||||
"group/command-item data-selected:bg-muted data-selected:text-foreground data-selected:*:[svg]:text-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<CheckIcon class="cn-command-item-indicator ml-auto opacity-0 group-has-[[data-slot=command-shortcut]]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
|
||||
</CommandPrimitive.Item>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.LinkItemProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.LinkItem
|
||||
bind:ref
|
||||
data-slot="command-item"
|
||||
class={cn(
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.ListProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.List
|
||||
bind:ref
|
||||
data-slot="command-list"
|
||||
class={cn("no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: CommandPrimitive.LoadingProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Loading bind:ref {...restProps} />
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.SeparatorProps = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Separator
|
||||
bind:ref
|
||||
data-slot="command-separator"
|
||||
class={cn("bg-border -mx-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
data-slot="command-shortcut"
|
||||
class={cn("text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Command as CommandPrimitive } from "bits-ui";
|
||||
|
||||
export type CommandRootApi = CommandPrimitive.Root;
|
||||
|
||||
let {
|
||||
api = $bindable(null),
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
class: className,
|
||||
...restProps
|
||||
}: CommandPrimitive.RootProps & {
|
||||
api?: CommandRootApi | null;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<CommandPrimitive.Root
|
||||
bind:this={api}
|
||||
bind:value
|
||||
bind:ref
|
||||
data-slot="command"
|
||||
class={cn("bg-popover text-popover-foreground rounded-xl! p-1 flex size-full flex-col overflow-hidden", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,37 @@
|
||||
import Root from "./command.svelte";
|
||||
import Loading from "./command-loading.svelte";
|
||||
import Dialog from "./command-dialog.svelte";
|
||||
import Empty from "./command-empty.svelte";
|
||||
import Group from "./command-group.svelte";
|
||||
import Item from "./command-item.svelte";
|
||||
import Input from "./command-input.svelte";
|
||||
import List from "./command-list.svelte";
|
||||
import Separator from "./command-separator.svelte";
|
||||
import Shortcut from "./command-shortcut.svelte";
|
||||
import LinkItem from "./command-link-item.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Dialog,
|
||||
Empty,
|
||||
Group,
|
||||
Item,
|
||||
LinkItem,
|
||||
Input,
|
||||
List,
|
||||
Separator,
|
||||
Shortcut,
|
||||
Loading,
|
||||
//
|
||||
Root as Command,
|
||||
Dialog as CommandDialog,
|
||||
Empty as CommandEmpty,
|
||||
Group as CommandGroup,
|
||||
Item as CommandItem,
|
||||
LinkItem as CommandLinkItem,
|
||||
Input as CommandInput,
|
||||
List as CommandList,
|
||||
Separator as CommandSeparator,
|
||||
Shortcut as CommandShortcut,
|
||||
Loading as CommandLoading,
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import Root from "./input-group.svelte";
|
||||
import Addon from "./input-group-addon.svelte";
|
||||
import Button from "./input-group-button.svelte";
|
||||
import Input from "./input-group-input.svelte";
|
||||
import Text from "./input-group-text.svelte";
|
||||
import Textarea from "./input-group-textarea.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Addon,
|
||||
Button,
|
||||
Input,
|
||||
Text,
|
||||
Textarea,
|
||||
//
|
||||
Root as InputGroup,
|
||||
Addon as InputGroupAddon,
|
||||
Button as InputGroupButton,
|
||||
Input as InputGroupInput,
|
||||
Text as InputGroupText,
|
||||
Textarea as InputGroupTextarea,
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
export const inputGroupAddonVariants = tv({
|
||||
base: "text-muted-foreground h-auto gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4 flex cursor-text items-center justify-center select-none",
|
||||
variants: {
|
||||
align: {
|
||||
"inline-start": "pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem] order-first",
|
||||
"inline-end": "pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem] order-last",
|
||||
"block-start":
|
||||
"px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start",
|
||||
"block-end": "px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
align: "inline-start",
|
||||
},
|
||||
});
|
||||
|
||||
export type InputGroupAddonAlign = VariantProps<typeof inputGroupAddonVariants>["align"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
align = "inline-start",
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
align?: InputGroupAddonAlign;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
role="group"
|
||||
data-slot="input-group-addon"
|
||||
data-align={align}
|
||||
class={cn(inputGroupAddonVariants({ align }), className)}
|
||||
onclick={(e) => {
|
||||
if ((e.target as HTMLElement).closest("button")) {
|
||||
return;
|
||||
}
|
||||
e.currentTarget.parentElement?.querySelector("input")?.focus();
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
|
||||
const inputGroupButtonVariants = tv({
|
||||
base: "gap-2 text-sm flex items-center shadow-none",
|
||||
variants: {
|
||||
size: {
|
||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
|
||||
sm: "cn-input-group-button-size-sm",
|
||||
"icon-xs": "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
|
||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "xs",
|
||||
},
|
||||
});
|
||||
|
||||
export type InputGroupButtonSize = VariantProps<typeof inputGroupButtonVariants>["size"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
type = "button",
|
||||
variant = "ghost",
|
||||
size = "xs",
|
||||
...restProps
|
||||
}: Omit<ComponentProps<typeof Button>, "href" | "size"> & {
|
||||
size?: InputGroupButtonSize;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Button
|
||||
bind:ref
|
||||
{type}
|
||||
data-size={size}
|
||||
{variant}
|
||||
class={cn(inputGroupButtonVariants({ size }), className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</Button>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import { Input } from "$lib/components/ui/input/index.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
...props
|
||||
}: ComponentProps<typeof Input> = $props();
|
||||
</script>
|
||||
|
||||
<Input
|
||||
bind:ref
|
||||
data-slot="input-group-control"
|
||||
class={cn("rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent flex-1", className)}
|
||||
bind:value
|
||||
{...props}
|
||||
/>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
class={cn("text-muted-foreground gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Textarea } from "$lib/components/ui/textarea/index.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
...props
|
||||
}: ComponentProps<typeof Textarea> = $props();
|
||||
</script>
|
||||
|
||||
<Textarea
|
||||
bind:ref
|
||||
data-slot="input-group-control"
|
||||
class={cn("rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent flex-1 resize-none", className)}
|
||||
bind:value
|
||||
{...props}
|
||||
/>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...props
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
class={cn(
|
||||
"group/input-group border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-disabled:bg-input/50 dark:has-disabled:bg-input/80 h-8 rounded-lg border transition-colors in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,31 @@
|
||||
import Root from "./pagination.svelte";
|
||||
import Content from "./pagination-content.svelte";
|
||||
import Item from "./pagination-item.svelte";
|
||||
import Link from "./pagination-link.svelte";
|
||||
import PrevButton from "./pagination-prev-button.svelte";
|
||||
import NextButton from "./pagination-next-button.svelte";
|
||||
import Ellipsis from "./pagination-ellipsis.svelte";
|
||||
import Previous from "./pagination-previous.svelte";
|
||||
import Next from "./pagination-next.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Item,
|
||||
Link,
|
||||
PrevButton, // old
|
||||
NextButton, // old
|
||||
Ellipsis,
|
||||
Previous,
|
||||
Next,
|
||||
//
|
||||
Root as Pagination,
|
||||
Content as PaginationContent,
|
||||
Item as PaginationItem,
|
||||
Link as PaginationLink,
|
||||
PrevButton as PaginationPrevButton, // old
|
||||
NextButton as PaginationNextButton, // old
|
||||
Ellipsis as PaginationEllipsis,
|
||||
Previous as PaginationPrevious,
|
||||
Next as PaginationNext,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLUListElement>> = $props();
|
||||
</script>
|
||||
|
||||
<ul
|
||||
bind:this={ref}
|
||||
data-slot="pagination-content"
|
||||
class={cn("gap-0.5 flex items-center", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</ul>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||
import MoreHorizontalIcon from '@lucide/svelte/icons/more-horizontal';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLSpanElement>>> = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
bind:this={ref}
|
||||
aria-hidden="true"
|
||||
data-slot="pagination-ellipsis"
|
||||
class={cn("size-8 items-center justify-center [&_svg:not([class*='size-'])]:size-4 flex items-center justify-center", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
<span class="sr-only">More pages</span>
|
||||
</span>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLLiAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLLiAttributes> = $props();
|
||||
</script>
|
||||
|
||||
<li bind:this={ref} data-slot="pagination-item" {...restProps}>
|
||||
{@render children?.()}
|
||||
</li>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { buttonVariants, type ButtonSize } from "$lib/components/ui/button/index.js";
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
size = "icon",
|
||||
isActive,
|
||||
page,
|
||||
children,
|
||||
...restProps
|
||||
}: PaginationPrimitive.PageProps & {
|
||||
size?: ButtonSize;
|
||||
isActive: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
{page.value}
|
||||
{/snippet}
|
||||
|
||||
<PaginationPrimitive.Page
|
||||
bind:ref
|
||||
{page}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
data-size={size}
|
||||
class={cn(
|
||||
buttonVariants({ size, variant: isActive ? "outline" : "ghost" }),
|
||||
"cn-pagination-link",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
{@render Fallback()}
|
||||
{/if}
|
||||
</PaginationPrimitive.Page>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from "bits-ui";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { buttonVariants } from "../button/index.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: PaginationPrimitive.NextButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<span>Next</span>
|
||||
<ChevronRightIcon class="size-4" />
|
||||
{/snippet}
|
||||
|
||||
<PaginationPrimitive.NextButton
|
||||
bind:ref
|
||||
aria-label="Go to next page"
|
||||
class={cn(buttonVariants({ variant: "ghost" }), "pr-1.5!", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
{@render Fallback()}
|
||||
{/if}
|
||||
</PaginationPrimitive.NextButton>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { PaginationLink } from "./index.js";
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
|
||||
type PaginationNextProps = ComponentProps<typeof PaginationLink>;
|
||||
|
||||
let { class: className, ...restProps }: PaginationNextProps = $props();
|
||||
</script>
|
||||
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
class={cn("pr-1.5!", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<span class="cn-pagination-next-text hidden sm:block">Next</span>
|
||||
<ChevronRightIcon data-icon="inline-end" />
|
||||
</PaginationLink>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from "bits-ui";
|
||||
import ChevronLeftIcon from "@lucide/svelte/icons/chevron-left";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { buttonVariants } from "../button/index.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: PaginationPrimitive.PrevButtonProps = $props();
|
||||
</script>
|
||||
|
||||
{#snippet Fallback()}
|
||||
<ChevronLeftIcon class="size-4" />
|
||||
<span>Previous</span>
|
||||
{/snippet}
|
||||
|
||||
<PaginationPrimitive.PrevButton
|
||||
bind:ref
|
||||
aria-label="Go to previous page"
|
||||
class={cn(buttonVariants({ variant: "ghost" }), "pl-1.5!", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
{@render Fallback()}
|
||||
{/if}
|
||||
</PaginationPrimitive.PrevButton>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { ComponentProps } from "svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { PaginationLink } from "./index.js";
|
||||
import ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';
|
||||
|
||||
type PaginationPreviousProps = ComponentProps<typeof PaginationLink>;
|
||||
|
||||
let { class: className, ...restProps }: PaginationPreviousProps = $props();
|
||||
</script>
|
||||
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
class={cn("pl-1.5!", className)}
|
||||
{...restProps}
|
||||
>
|
||||
<ChevronLeftIcon data-icon="inline-start" />
|
||||
<span class="cn-pagination-previous-text hidden sm:block">Previous</span>
|
||||
</PaginationLink>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { Pagination as PaginationPrimitive } from "bits-ui";
|
||||
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
count = 0,
|
||||
perPage = 10,
|
||||
page = $bindable(1),
|
||||
siblingCount = 1,
|
||||
...restProps
|
||||
}: PaginationPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<PaginationPrimitive.Root
|
||||
bind:ref
|
||||
bind:page
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
{count}
|
||||
{perPage}
|
||||
{siblingCount}
|
||||
class={cn("cn-pagination mx-auto flex w-full justify-center", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,28 @@
|
||||
import Root from "./popover.svelte";
|
||||
import Close from "./popover-close.svelte";
|
||||
import Content from "./popover-content.svelte";
|
||||
import Description from "./popover-description.svelte";
|
||||
import Header from "./popover-header.svelte";
|
||||
import Title from "./popover-title.svelte";
|
||||
import Trigger from "./popover-trigger.svelte";
|
||||
import Portal from "./popover-portal.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Description,
|
||||
Header,
|
||||
Title,
|
||||
Trigger,
|
||||
Close,
|
||||
Portal,
|
||||
//
|
||||
Root as Popover,
|
||||
Content as PopoverContent,
|
||||
Description as PopoverDescription,
|
||||
Header as PopoverHeader,
|
||||
Title as PopoverTitle,
|
||||
Trigger as PopoverTrigger,
|
||||
Close as PopoverClose,
|
||||
Portal as PopoverPortal,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
import PopoverPortal from "./popover-portal.svelte";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
portalProps,
|
||||
...restProps
|
||||
}: PopoverPrimitive.ContentProps & {
|
||||
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPortal {...portalProps}>
|
||||
<PopoverPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="popover-content"
|
||||
{sideOffset}
|
||||
{align}
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 flex flex-col gap-2.5 rounded-lg p-2.5 text-sm shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 w-72 origin-(--transform-origin) outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</PopoverPortal>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="popover-description"
|
||||
class={cn("text-muted-foreground", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="popover-header"
|
||||
class={cn("flex flex-col gap-0.5 text-sm", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Portal {...restProps} />
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="popover-title"
|
||||
class={cn("font-medium", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: PopoverPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Trigger
|
||||
bind:ref
|
||||
data-slot="popover-trigger"
|
||||
class={cn("", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from "bits-ui";
|
||||
|
||||
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<PopoverPrimitive.Root bind:open {...restProps} />
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Toaster } from "./sonner.svelte";
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
|
||||
import { mode } from "mode-watcher";
|
||||
import Loader2Icon from '@lucide/svelte/icons/loader-2';
|
||||
import CircleCheckIcon from '@lucide/svelte/icons/circle-check';
|
||||
import OctagonXIcon from '@lucide/svelte/icons/octagon-x';
|
||||
import InfoIcon from '@lucide/svelte/icons/info';
|
||||
import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
|
||||
|
||||
let { ...restProps }: SonnerProps = $props();
|
||||
</script>
|
||||
|
||||
<Sonner
|
||||
theme={mode.current}
|
||||
class="toaster group"
|
||||
style="--normal-bg: var(--color-popover); --normal-text: var(--color-popover-foreground); --normal-border: var(--color-border);"
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet loadingIcon()}
|
||||
<Loader2Icon class="size-4 animate-spin" />
|
||||
{/snippet}
|
||||
{#snippet successIcon()}
|
||||
<CircleCheckIcon class="size-4" />
|
||||
{/snippet}
|
||||
{#snippet errorIcon()}
|
||||
<OctagonXIcon class="size-4" />
|
||||
{/snippet}
|
||||
{#snippet infoIcon()}
|
||||
<InfoIcon class="size-4" />
|
||||
{/snippet}
|
||||
{#snippet warningIcon()}
|
||||
<TriangleAlertIcon class="size-4" />
|
||||
{/snippet}
|
||||
</Sonner>
|
||||
@@ -0,0 +1,7 @@
|
||||
import Root from "./switch.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Switch,
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { Switch as SwitchPrimitive } from "bits-ui";
|
||||
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
checked = $bindable(false),
|
||||
size = "default",
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> & {
|
||||
size?: "sm" | "default";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<SwitchPrimitive.Root
|
||||
bind:ref
|
||||
bind:checked
|
||||
data-slot="switch"
|
||||
data-size={size}
|
||||
class={cn(
|
||||
"data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 shrink-0 rounded-full border border-transparent focus-visible:ring-3 aria-invalid:ring-3 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer group/switch relative inline-flex items-center transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
class="bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground rounded-full group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 pointer-events-none block ring-0 transition-transform rtl:data-[state=checked]:translate-x-[calc(-100%)]"
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
@@ -1,18 +1,21 @@
|
||||
<script lang="ts">
|
||||
import './layout.css';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import AppSidebar from '$lib/components/app-sidebar.svelte';
|
||||
import { fetchAggSummary } from '$lib/api/client.js';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
|
||||
import { Toaster } from '$lib/components/ui/sonner/index.js';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
let serverAliases = $state<string[]>([]);
|
||||
|
||||
const serverMatch = $derived.by(() => {
|
||||
const m = $page.url.pathname.match(/^\/servers\/([^/]+)/);
|
||||
const m = page.url.pathname.match(/^\/servers\/([^/]+)/);
|
||||
return m ? m[1] : '';
|
||||
});
|
||||
const serverPath = $derived(serverMatch ? `/servers/${serverMatch}` : '');
|
||||
@@ -42,6 +45,8 @@
|
||||
<title>Telemt Panel</title>
|
||||
</svelte:head>
|
||||
|
||||
<ModeWatcher />
|
||||
<Tooltip.Provider>
|
||||
<Sidebar.Provider>
|
||||
<AppSidebar {serverAliases} {serverPath} />
|
||||
<Sidebar.Inset>
|
||||
@@ -55,3 +60,5 @@
|
||||
</div>
|
||||
</Sidebar.Inset>
|
||||
</Sidebar.Provider>
|
||||
<Toaster richColors closeButton position="top-right" />
|
||||
</Tooltip.Provider>
|
||||
|
||||
+43
-33
@@ -18,6 +18,10 @@
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import AliasFilterSelect from '$lib/components/alias-filter-select.svelte';
|
||||
import DataQueryState from '$lib/components/fleet/data-query-state.svelte';
|
||||
import FleetLiveStaleBadge from '$lib/components/fleet/fleet-live-stale-badge.svelte';
|
||||
import FleetRefreshInput from '$lib/components/fleet/fleet-refresh-input.svelte';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
import AlertTriangleIcon from '@lucide/svelte/icons/triangle-alert';
|
||||
@@ -243,6 +247,9 @@
|
||||
};
|
||||
});
|
||||
|
||||
let hasStaleData = $derived(err != null && summary != null);
|
||||
let showFleetSkeleton = $derived(loading && !summary && !err);
|
||||
|
||||
/** Число из ответа Telemt (поле может отсутствовать или прийти строкой). */
|
||||
function numStat(v: unknown, fallback = 0): number {
|
||||
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
||||
@@ -396,9 +403,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge variant={isStale ? 'secondary' : 'default'}>
|
||||
{isStale ? 'stale' : 'live'}{#if staleSeconds != null} · {staleSeconds}s{/if}
|
||||
</Badge>
|
||||
<FleetLiveStaleBadge {isStale} {staleSeconds} refreshSeconds={refreshSeconds} />
|
||||
<Button variant="outline" size="sm" onclick={() => void load()} disabled={loading}>
|
||||
<RefreshCwIcon class="mr-1 size-4 {loading ? 'animate-spin' : ''}" />
|
||||
Обновить
|
||||
@@ -408,37 +413,40 @@
|
||||
|
||||
<div class="mb-4 flex flex-wrap items-end gap-2">
|
||||
<AliasFilterSelect availableAliases={availableAliases} bind:value={aliasFilter} />
|
||||
<label class="text-xs text-muted-foreground">
|
||||
Refresh (sec)
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
class="mt-1 h-9 w-28 rounded-md border bg-background px-2 text-sm"
|
||||
bind:value={refreshInput}
|
||||
onchange={() => void applyControls()}
|
||||
/>
|
||||
</label>
|
||||
<FleetRefreshInput bind:value={refreshInput} onValueChange={() => void applyControls()} />
|
||||
</div>
|
||||
|
||||
{#if partial}
|
||||
<Alert variant="destructive" class="mb-4">
|
||||
<AlertTriangleIcon class="size-4" />
|
||||
<AlertTitle>Частичные данные</AlertTitle>
|
||||
<AlertDescription>
|
||||
Один или несколько upstream ответили с ошибкой — цифры могут быть неполными.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{#if err}
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Ошибка</AlertTitle>
|
||||
<AlertDescription>{err}</AlertDescription>
|
||||
</Alert>
|
||||
{:else if loading && !summary}
|
||||
<p class="text-muted-foreground">Загрузка…</p>
|
||||
{:else if summary}
|
||||
<DataQueryState
|
||||
partial={partial}
|
||||
{err}
|
||||
{hasStaleData}
|
||||
showSkeleton={showFleetSkeleton}
|
||||
isEmpty={false}
|
||||
>
|
||||
{#snippet partialSnippet()}
|
||||
<Alert variant="destructive" class="mb-4">
|
||||
<AlertTriangleIcon class="size-4" />
|
||||
<AlertTitle>Частичные данные</AlertTitle>
|
||||
<AlertDescription>
|
||||
Один или несколько upstream ответили с ошибкой — цифры могут быть неполными.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
{/snippet}
|
||||
{#snippet skeleton()}
|
||||
<div class="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||
{#each Array.from({ length: 6 }) as _, i (i)}
|
||||
<Card.Root>
|
||||
<Card.Header class="pb-2">
|
||||
<Skeleton class="h-4 w-24" />
|
||||
<Skeleton class="mt-2 h-8 w-16" />
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
</div>
|
||||
<Skeleton class="mb-4 h-32 w-full rounded-lg" />
|
||||
{/snippet}
|
||||
{#snippet children()}
|
||||
{#if summary}
|
||||
<div class="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||
<Card.Root
|
||||
class={cn('border-l-4 shadow-sm transition-colors', kpiAccent[nodesHealthAccent].border)}
|
||||
@@ -727,4 +735,6 @@
|
||||
</Table.Root>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</DataQueryState>
|
||||
|
||||
@@ -11,10 +11,14 @@
|
||||
} from '$lib/api/client.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
|
||||
import AliasFilterSelect from '$lib/components/alias-filter-select.svelte';
|
||||
import DataQueryState from '$lib/components/fleet/data-query-state.svelte';
|
||||
import FleetLiveStaleBadge from '$lib/components/fleet/fleet-live-stale-badge.svelte';
|
||||
import FleetRefreshInput from '$lib/components/fleet/fleet-refresh-input.svelte';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
|
||||
type TriageState = {
|
||||
@@ -209,6 +213,9 @@
|
||||
if (staleTimer) clearInterval(staleTimer);
|
||||
};
|
||||
});
|
||||
|
||||
let hasStaleData = $derived(err != null && data != null);
|
||||
let showSkeleton = $derived(loading && data === null && !err);
|
||||
</script>
|
||||
|
||||
<div class="mb-6 flex flex-wrap items-center justify-between gap-4">
|
||||
@@ -220,9 +227,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge variant={isStale ? 'secondary' : 'default'}>
|
||||
{isStale ? 'stale' : 'live'}{#if staleSeconds != null} · {staleSeconds}s{/if}
|
||||
</Badge>
|
||||
<FleetLiveStaleBadge {isStale} {staleSeconds} refreshSeconds={refreshSeconds} />
|
||||
<Button variant="outline" size="sm" onclick={() => void load()} disabled={loading}>
|
||||
<RefreshCwIcon class="mr-1 size-4 {loading ? 'animate-spin' : ''}" />
|
||||
Обновить
|
||||
@@ -232,32 +237,36 @@
|
||||
|
||||
<div class="mb-4 flex flex-wrap items-end gap-2">
|
||||
<AliasFilterSelect availableAliases={availableAliases} bind:value={aliasFilter} />
|
||||
<label class="text-xs text-muted-foreground">
|
||||
Refresh (sec)
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
class="mt-1 h-9 w-28 rounded-md border bg-background px-2 text-sm"
|
||||
bind:value={refreshInput}
|
||||
onchange={() => void applyControls()}
|
||||
/>
|
||||
</label>
|
||||
<FleetRefreshInput bind:value={refreshInput} onValueChange={() => void applyControls()} />
|
||||
</div>
|
||||
|
||||
{#if partial}
|
||||
<Alert class="mb-4">
|
||||
<AlertTitle>Частичные данные</AlertTitle>
|
||||
<AlertDescription>Не все upstream ответили успешно.</AlertDescription>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{#if err}
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Ошибка</AlertTitle>
|
||||
<AlertDescription>{err}</AlertDescription>
|
||||
</Alert>
|
||||
{:else}
|
||||
<DataQueryState
|
||||
partial={partial}
|
||||
{err}
|
||||
{hasStaleData}
|
||||
showSkeleton={showSkeleton}
|
||||
isEmpty={false}
|
||||
>
|
||||
{#snippet skeleton()}
|
||||
<div class="mb-4 grid gap-4 sm:grid-cols-3">
|
||||
{#each Array.from({ length: 3 }) as _, i (i)}
|
||||
<Card.Root>
|
||||
<Card.Header class="pb-2">
|
||||
<Skeleton class="h-4 w-20" />
|
||||
<Skeleton class="mt-2 h-8 w-12" />
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
</div>
|
||||
<Card.Root>
|
||||
<Card.Content class="space-y-2 p-4">
|
||||
{#each Array.from({ length: 6 }) as _, i (i)}
|
||||
<Skeleton class="h-12 w-full" />
|
||||
{/each}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/snippet}
|
||||
{#snippet children()}
|
||||
<div class="mb-4 grid gap-4 sm:grid-cols-3">
|
||||
<Card.Root>
|
||||
<Card.Header class="pb-2">
|
||||
@@ -348,9 +357,9 @@
|
||||
</Button>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
class="h-8 w-full rounded-md border bg-background px-2 text-xs"
|
||||
class="h-8 w-full text-xs"
|
||||
placeholder="owner"
|
||||
value={triage.owner ?? ''}
|
||||
oninput={(e) =>
|
||||
@@ -360,9 +369,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
class="h-8 w-full rounded-md border bg-background px-2 text-xs"
|
||||
class="h-8 w-full text-xs"
|
||||
placeholder="note"
|
||||
value={triage.note ?? ''}
|
||||
oninput={(e) =>
|
||||
@@ -379,4 +388,5 @@
|
||||
</Table.Root>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</DataQueryState>
|
||||
|
||||
@@ -9,11 +9,17 @@
|
||||
import { flagEmoji } from '$lib/format.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
|
||||
import { Switch } from '$lib/components/ui/switch/index.js';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
import AliasFilterSelect from '$lib/components/alias-filter-select.svelte';
|
||||
import DataQueryState from '$lib/components/fleet/data-query-state.svelte';
|
||||
import FleetLiveStaleBadge from '$lib/components/fleet/fleet-live-stale-badge.svelte';
|
||||
import FleetRefreshInput from '$lib/components/fleet/fleet-refresh-input.svelte';
|
||||
|
||||
let loading = $state(true);
|
||||
let err = $state<string | null>(null);
|
||||
@@ -410,6 +416,13 @@
|
||||
if (staleTimer) clearInterval(staleTimer);
|
||||
};
|
||||
});
|
||||
|
||||
let totalIpRows = $derived(
|
||||
rows.reduce((n, ur) => n + (ur.ips?.length ?? 0), 0)
|
||||
);
|
||||
let hasStaleData = $derived(err != null && rows.length > 0);
|
||||
let showSkeleton = $derived(loading && rows.length === 0 && !err);
|
||||
let isDataEmpty = $derived(!loading && totalIpRows === 0 && !err);
|
||||
</script>
|
||||
|
||||
<div class="mb-6 flex flex-wrap items-end justify-between gap-4">
|
||||
@@ -418,18 +431,15 @@
|
||||
<p class="text-sm text-muted-foreground">Снимок active/recent с шлюза; GeoIP из конфига шлюза.</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge variant={isStale ? 'secondary' : 'default'}>
|
||||
{isStale ? 'stale' : 'live'}{#if staleSeconds != null} · {staleSeconds}s{/if}
|
||||
</Badge>
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
<FleetLiveStaleBadge {isStale} {staleSeconds} refreshSeconds={refreshSeconds} />
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
id="ips-geoip"
|
||||
bind:checked={geo}
|
||||
onchange={() => void applyControls()}
|
||||
class="rounded border"
|
||||
onCheckedChange={() => void applyControls()}
|
||||
/>
|
||||
GeoIP
|
||||
</label>
|
||||
<Label for="ips-geoip" class="cursor-pointer text-sm font-normal">GeoIP</Label>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onclick={() => void load()} disabled={loading}>
|
||||
<RefreshCwIcon class="mr-1 size-4 {loading ? 'animate-spin' : ''}" />
|
||||
Обновить
|
||||
@@ -439,34 +449,53 @@
|
||||
|
||||
<div class="mb-4 flex flex-wrap items-end gap-2">
|
||||
<AliasFilterSelect availableAliases={availableAliases} bind:value={aliasFilter} />
|
||||
<label class="text-xs text-muted-foreground">
|
||||
Refresh (sec)
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
class="mt-1 h-9 w-28 rounded-md border bg-background px-2 text-sm"
|
||||
bind:value={refreshInput}
|
||||
onchange={() => void applyControls()}
|
||||
/>
|
||||
</label>
|
||||
<FleetRefreshInput bind:value={refreshInput} onValueChange={() => void applyControls()} />
|
||||
</div>
|
||||
|
||||
{#if partial}
|
||||
<Alert class="mb-4">
|
||||
<AlertTitle>Частичные данные</AlertTitle>
|
||||
<AlertDescription>Не все upstream успешны.</AlertDescription>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{#if err}
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Ошибка</AlertTitle>
|
||||
<AlertDescription>{err}</AlertDescription>
|
||||
</Alert>
|
||||
{:else if loading && rows.length === 0}
|
||||
<p class="text-muted-foreground">Загрузка…</p>
|
||||
{:else}
|
||||
<DataQueryState
|
||||
partial={partial}
|
||||
{err}
|
||||
{hasStaleData}
|
||||
showSkeleton={showSkeleton}
|
||||
isEmpty={isDataEmpty}
|
||||
>
|
||||
{#snippet skeleton()}
|
||||
<div class="space-y-4">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Skeleton class="h-5 w-48" />
|
||||
<Skeleton class="h-4 w-64" />
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-2 p-6">
|
||||
<Skeleton class="h-[420px] w-full rounded-md" />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<Card.Root>
|
||||
<Card.Content class="space-y-2 p-4">
|
||||
{#each Array.from({ length: 6 }) as _, i (i)}
|
||||
<Skeleton class="h-10 w-full" />
|
||||
{/each}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet empty()}
|
||||
<Card.Root class="border-dashed">
|
||||
<Card.Header>
|
||||
<Card.Title>Нет данных</Card.Title>
|
||||
<Card.Description>
|
||||
По текущим фильтрам нет IP в снимке. Смените alias или нажмите «Обновить».
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="flex flex-wrap gap-2">
|
||||
<Button variant="outline" size="sm" onclick={() => void load()} disabled={loading}>
|
||||
<RefreshCwIcon class="mr-1 size-4 {loading ? 'animate-spin' : ''}" />
|
||||
Обновить
|
||||
</Button>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/snippet}
|
||||
{#snippet children()}
|
||||
{#if geo}
|
||||
<Card.Root class="mb-6">
|
||||
<Card.Header>
|
||||
@@ -564,7 +593,8 @@
|
||||
</Table.Root>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</DataQueryState>
|
||||
|
||||
<style>
|
||||
/* Leaflet в shadcn/tailwind теме: подгоняем фон, радиус и контролы */
|
||||
|
||||
@@ -5,24 +5,36 @@
|
||||
&:where([data-state='open']), &:where([data-open]:not([data-open='false'])) {
|
||||
@slot;
|
||||
}
|
||||
&:where([data-state="open"]), &:where([data-open]:not([data-open="false"])) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant data-closed {
|
||||
&:where([data-state='closed']), &:where([data-closed]:not([data-closed='false'])) {
|
||||
@slot;
|
||||
}
|
||||
&:where([data-state="closed"]), &:where([data-closed]:not([data-closed="false"])) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant data-checked {
|
||||
&:where([data-state='checked']), &:where([data-checked]:not([data-checked='false'])) {
|
||||
@slot;
|
||||
}
|
||||
&:where([data-state="checked"]), &:where([data-checked]:not([data-checked="false"])) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant data-unchecked {
|
||||
&:where([data-state='unchecked']), &:where([data-unchecked]:not([data-unchecked='false'])) {
|
||||
@slot;
|
||||
}
|
||||
&:where([data-state="unchecked"]), &:where([data-unchecked]:not([data-unchecked="false"])) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant data-selected {
|
||||
@@ -35,24 +47,36 @@
|
||||
&:where([data-disabled='true']), &:where([data-disabled]:not([data-disabled='false'])) {
|
||||
@slot;
|
||||
}
|
||||
&:where([data-disabled="true"]), &:where([data-disabled]:not([data-disabled="false"])) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant data-active {
|
||||
&:where([data-state='active']), &:where([data-active]:not([data-active='false'])) {
|
||||
@slot;
|
||||
}
|
||||
&:where([data-state="active"]), &:where([data-active]:not([data-active="false"])) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant data-horizontal {
|
||||
&:where([data-orientation='horizontal']) {
|
||||
@slot;
|
||||
}
|
||||
&:where([data-orientation="horizontal"]) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@custom-variant data-vertical {
|
||||
&:where([data-orientation='vertical']) {
|
||||
@slot;
|
||||
}
|
||||
&:where([data-orientation="vertical"]) {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
|
||||
@utility no-scrollbar {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import AliasFilterSelect from '$lib/components/alias-filter-select.svelte';
|
||||
import FleetLiveStaleBadge from '$lib/components/fleet/fleet-live-stale-badge.svelte';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
|
||||
type LiveStatus = 'healthy' | 'degraded' | 'critical';
|
||||
@@ -266,9 +267,14 @@
|
||||
<Badge variant={connected ? 'default' : 'secondary'}>
|
||||
{connected ? 'connected' : 'disconnected'}
|
||||
</Badge>
|
||||
<Badge variant={isStale ? 'secondary' : 'default'}>
|
||||
{isStale ? 'stale' : 'live'}{#if staleSeconds != null} · {staleSeconds}s{/if}
|
||||
</Badge>
|
||||
<FleetLiveStaleBadge
|
||||
{isStale}
|
||||
{staleSeconds}
|
||||
refreshSeconds={30}
|
||||
tooltipOverride={isStale
|
||||
? 'Нет событий SSE дольше 12 с — снимок считается устаревшим.'
|
||||
: `Последнее событие потока: ${staleSeconds ?? 0} с назад.`}
|
||||
/>
|
||||
<Button variant="outline" size="sm" onclick={() => connectStream()}>
|
||||
<RefreshCwIcon class="mr-1 size-4" />
|
||||
Reconnect
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import {
|
||||
ApiError,
|
||||
fetchStatsSummary,
|
||||
@@ -11,9 +11,11 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
|
||||
import DataQueryState from '$lib/components/fleet/data-query-state.svelte';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
|
||||
const alias = $derived($page.params.alias ?? '');
|
||||
const alias = $derived(page.params.alias ?? '');
|
||||
|
||||
let loading = $state(true);
|
||||
let err = $state<string | null>(null);
|
||||
@@ -51,6 +53,9 @@
|
||||
if (!a) return;
|
||||
await loadFor(a);
|
||||
}
|
||||
|
||||
let hasStaleData = $derived(err != null && (health != null || summary != null));
|
||||
let showSkeleton = $derived(loading && !err && health == null && summary == null);
|
||||
</script>
|
||||
|
||||
<div class="mb-6 flex items-center justify-between gap-4">
|
||||
@@ -64,14 +69,27 @@
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if err}
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Ошибка</AlertTitle>
|
||||
<AlertDescription>{err}</AlertDescription>
|
||||
</Alert>
|
||||
{:else if loading}
|
||||
<p class="text-muted-foreground">Загрузка…</p>
|
||||
{:else}
|
||||
<DataQueryState
|
||||
{err}
|
||||
{hasStaleData}
|
||||
showSkeleton={showSkeleton}
|
||||
isEmpty={false}
|
||||
partial={false}
|
||||
>
|
||||
{#snippet skeleton()}
|
||||
<div class="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{#each Array.from({ length: 4 }) as _, i (i)}
|
||||
<Card.Root>
|
||||
<Card.Header class="pb-2">
|
||||
<Skeleton class="h-4 w-24" />
|
||||
<Skeleton class="mt-2 h-8 w-16" />
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
</div>
|
||||
<Skeleton class="h-40 w-full rounded-lg" />
|
||||
{/snippet}
|
||||
{#snippet children()}
|
||||
{#if health?.status === 'ok'}
|
||||
<Alert class="mb-4 border-emerald-500/30 bg-emerald-500/10">
|
||||
<AlertTitle>Telemt отвечает</AlertTitle>
|
||||
@@ -129,4 +147,5 @@
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</DataQueryState>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
|
||||
const alias = $derived($page.params.alias ?? '');
|
||||
const alias = $derived(page.params.alias ?? '');
|
||||
</script>
|
||||
|
||||
<div class="mb-6">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { ApiError, fetchTelemt } from '$lib/api/client.js';
|
||||
import type { DcStatusData, RuntimeGatesData } from '$lib/api/telemt-v1.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
@@ -9,7 +9,7 @@
|
||||
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 ?? '');
|
||||
|
||||
let loading = $state(true);
|
||||
let err = $state<string | null>(null);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { ApiError, fetchTelemt } from '$lib/api/client.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
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 ?? '');
|
||||
|
||||
let loading = $state(true);
|
||||
let err = $state<string | null>(null);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { ApiError, fetchTelemt } from '$lib/api/client.js';
|
||||
import type { SystemInfoData } from '$lib/api/telemt-v1.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
@@ -7,7 +7,7 @@
|
||||
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 ?? '');
|
||||
|
||||
let loading = $state(true);
|
||||
let err = $state<string | null>(null);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { ApiError, fetchTelemt } from '$lib/api/client.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
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 ?? '');
|
||||
|
||||
let loading = $state(true);
|
||||
let err = $state<string | null>(null);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import {
|
||||
ApiError,
|
||||
createUser,
|
||||
@@ -24,8 +24,9 @@
|
||||
import TrashIcon from '@lucide/svelte/icons/trash-2';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const alias = $derived($page.params.alias ?? '');
|
||||
const alias = $derived(page.params.alias ?? '');
|
||||
|
||||
let loading = $state(true);
|
||||
let err = $state<string | null>(null);
|
||||
@@ -164,10 +165,15 @@
|
||||
|
||||
function copy(text: string) {
|
||||
const value = String(text ?? '');
|
||||
const done = () => toast.success('Скопировано в буфер');
|
||||
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
||||
void navigator.clipboard.writeText(value).catch(() => fallbackCopy(value));
|
||||
void navigator.clipboard.writeText(value).then(done).catch(() => {
|
||||
fallbackCopy(value);
|
||||
done();
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(value);
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,19 @@
|
||||
import { fetchAggUsers, fetchAggSummary, ApiError } from '$lib/api/client.js';
|
||||
import type { components } from '$lib/api/aggregate.gen.js';
|
||||
import { formatMiB } from '$lib/format.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 Card from '$lib/components/ui/card/index.js';
|
||||
import { Alert, AlertDescription, AlertTitle } from '$lib/components/ui/alert/index.js';
|
||||
import { Skeleton } from '$lib/components/ui/skeleton/index.js';
|
||||
import { Switch } from '$lib/components/ui/switch/index.js';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import AliasFilterSelect from '$lib/components/alias-filter-select.svelte';
|
||||
import DataQueryState from '$lib/components/fleet/data-query-state.svelte';
|
||||
import FleetLiveStaleBadge from '$lib/components/fleet/fleet-live-stale-badge.svelte';
|
||||
import FleetRefreshInput from '$lib/components/fleet/fleet-refresh-input.svelte';
|
||||
import CopyIcon from '@lucide/svelte/icons/copy';
|
||||
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let loading = $state(true);
|
||||
let err = $state<string | null>(null);
|
||||
@@ -153,12 +158,36 @@
|
||||
};
|
||||
});
|
||||
|
||||
const usersPerPage = 25;
|
||||
let usersPage = $state(1);
|
||||
let hasStaleData = $derived(err != null && rows.length > 0);
|
||||
let showSkeleton = $derived(loading && rows.length === 0 && !err);
|
||||
let isDataEmpty = $derived(!loading && rows.length === 0 && !err);
|
||||
let pageCount = $derived(Math.max(1, Math.ceil(rows.length / usersPerPage)));
|
||||
let pagedRows = $derived.by(() => {
|
||||
const start = (usersPage - 1) * usersPerPage;
|
||||
return rows.slice(start, start + usersPerPage);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
aliasFilter;
|
||||
usersPage = 1;
|
||||
});
|
||||
$effect(() => {
|
||||
if (usersPage > pageCount) usersPage = pageCount;
|
||||
});
|
||||
|
||||
function copy(text: string) {
|
||||
const value = String(text ?? '');
|
||||
const done = () => toast.success('Скопировано в буфер');
|
||||
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
||||
void navigator.clipboard.writeText(value).catch(() => fallbackCopy(value));
|
||||
void navigator.clipboard.writeText(value).then(done).catch(() => {
|
||||
fallbackCopy(value);
|
||||
done();
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(value);
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,18 +225,17 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Badge variant={isStale ? 'secondary' : 'default'}>
|
||||
{isStale ? 'stale' : 'live'}{#if staleSeconds != null} · {staleSeconds}s{/if}
|
||||
</Badge>
|
||||
<label class="flex cursor-pointer items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
<FleetLiveStaleBadge {isStale} {staleSeconds} refreshSeconds={refreshSeconds} />
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
id="users-include-links"
|
||||
bind:checked={includeLinks}
|
||||
onchange={() => void applyControls()}
|
||||
class="rounded border"
|
||||
onCheckedChange={() => void applyControls()}
|
||||
/>
|
||||
Ссылки tg://proxy
|
||||
</label>
|
||||
<Label for="users-include-links" class="cursor-pointer text-sm font-normal"
|
||||
>Ссылки tg://proxy</Label
|
||||
>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onclick={() => void load()} disabled={loading}>
|
||||
<RefreshCwIcon class="mr-1 size-4 {loading ? 'animate-spin' : ''}" />
|
||||
Обновить
|
||||
@@ -217,34 +245,40 @@
|
||||
|
||||
<div class="mb-4 flex flex-wrap items-end gap-2">
|
||||
<AliasFilterSelect availableAliases={availableAliases} bind:value={aliasFilter} />
|
||||
<label class="text-xs text-muted-foreground">
|
||||
Refresh (sec)
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
class="mt-1 h-9 w-28 rounded-md border bg-background px-2 text-sm"
|
||||
bind:value={refreshInput}
|
||||
onchange={() => void applyControls()}
|
||||
/>
|
||||
</label>
|
||||
<FleetRefreshInput bind:value={refreshInput} onValueChange={() => void applyControls()} />
|
||||
</div>
|
||||
|
||||
{#if partial}
|
||||
<Alert class="mb-4">
|
||||
<AlertTitle>Частичные данные</AlertTitle>
|
||||
<AlertDescription>Не все upstream ответили успешно.</AlertDescription>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{#if err}
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Ошибка</AlertTitle>
|
||||
<AlertDescription>{err}</AlertDescription>
|
||||
</Alert>
|
||||
{:else if loading && rows.length === 0}
|
||||
<p class="text-muted-foreground">Загрузка…</p>
|
||||
{:else}
|
||||
<DataQueryState
|
||||
partial={partial}
|
||||
{err}
|
||||
{hasStaleData}
|
||||
showSkeleton={showSkeleton}
|
||||
isEmpty={isDataEmpty}
|
||||
>
|
||||
{#snippet skeleton()}
|
||||
<Card.Root>
|
||||
<Card.Content class="space-y-2 p-4">
|
||||
{#each Array.from({ length: 8 }) as _, i (i)}
|
||||
<Skeleton class="h-10 w-full" />
|
||||
{/each}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/snippet}
|
||||
{#snippet empty()}
|
||||
<Card.Root class="border-dashed">
|
||||
<Card.Header>
|
||||
<Card.Title>Нет пользователей</Card.Title>
|
||||
<Card.Description>По фильтру нет записей. Смените alias или обновите снимок.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<Button variant="outline" size="sm" onclick={() => void load()} disabled={loading}>
|
||||
<RefreshCwIcon class="mr-1 size-4 {loading ? 'animate-spin' : ''}" />
|
||||
Обновить
|
||||
</Button>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/snippet}
|
||||
{#snippet children()}
|
||||
<Card.Root>
|
||||
<Card.Content class="p-0">
|
||||
<Table.Root>
|
||||
@@ -258,7 +292,7 @@
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each rows as row (row.username ?? '')}
|
||||
{#each pagedRows as row (row.username ?? '')}
|
||||
<Table.Row
|
||||
class="cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
|
||||
tabindex={0}
|
||||
@@ -319,6 +353,32 @@
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
{#if rows.length > usersPerPage}
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between gap-2 border-t px-4 py-3 text-sm text-muted-foreground"
|
||||
>
|
||||
<span>
|
||||
{rows.length === 0
|
||||
? '0'
|
||||
: `${(usersPage - 1) * usersPerPage + 1}–${Math.min(usersPage * usersPerPage, rows.length)}`} из {rows.length}
|
||||
</span>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={usersPage <= 1}
|
||||
onclick={() => (usersPage = Math.max(1, usersPage - 1))}>Назад</Button
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={usersPage >= pageCount}
|
||||
onclick={() => (usersPage = Math.min(pageCount, usersPage + 1))}>Вперёд</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</DataQueryState>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
import { fetchAggUniqueIps, fetchAggUser, ApiError } from '$lib/api/client.js';
|
||||
import type { components } from '$lib/api/aggregate.gen.js';
|
||||
@@ -10,6 +10,7 @@
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import CopyIcon from '@lucide/svelte/icons/copy';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let loading = $state(true);
|
||||
let err = $state<string | null>(null);
|
||||
@@ -18,7 +19,7 @@
|
||||
let uniqueIps = $state<components['schemas']['UniqueIPsRow'][]>([]);
|
||||
let ipErr = $state<string | null>(null);
|
||||
|
||||
const username = $derived($page.params.username ?? '');
|
||||
const username = $derived(page.params.username ?? '');
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
@@ -57,10 +58,15 @@
|
||||
|
||||
function copy(text: string) {
|
||||
const value = String(text ?? '');
|
||||
const done = () => toast.success('Скопировано в буфер');
|
||||
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
||||
void navigator.clipboard.writeText(value).catch(() => fallbackCopy(value));
|
||||
void navigator.clipboard.writeText(value).then(done).catch(() => {
|
||||
fallbackCopy(value);
|
||||
done();
|
||||
});
|
||||
} else {
|
||||
fallbackCopy(value);
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user