mirror of
https://github.com/LogicLabs-OU/OpenArchiver.git
synced 2026-04-06 00:31:57 +02:00
Dashboard revamp
This commit is contained in:
@@ -1,121 +1,121 @@
|
||||
@import "tailwindcss";
|
||||
@import 'tailwindcss';
|
||||
|
||||
@import "tw-animate-css";
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.129 0.042 264.695);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.129 0.042 264.695);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.129 0.042 264.695);
|
||||
--primary: oklch(0.208 0.042 265.755);
|
||||
--primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--secondary: oklch(0.968 0.007 247.896);
|
||||
--secondary-foreground: oklch(0.208 0.042 265.755);
|
||||
--muted: oklch(0.968 0.007 247.896);
|
||||
--muted-foreground: oklch(0.554 0.046 257.417);
|
||||
--accent: oklch(0.968 0.007 247.896);
|
||||
--accent-foreground: oklch(0.208 0.042 265.755);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.929 0.013 255.508);
|
||||
--input: oklch(0.929 0.013 255.508);
|
||||
--ring: oklch(0.704 0.04 256.788);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.984 0.003 247.858);
|
||||
--sidebar-foreground: oklch(0.129 0.042 264.695);
|
||||
--sidebar-primary: oklch(0.208 0.042 265.755);
|
||||
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-accent: oklch(0.968 0.007 247.896);
|
||||
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
|
||||
--sidebar-border: oklch(0.929 0.013 255.508);
|
||||
--sidebar-ring: oklch(0.704 0.04 256.788);
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.129 0.042 264.695);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.129 0.042 264.695);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.129 0.042 264.695);
|
||||
--primary: oklch(0.208 0.042 265.755);
|
||||
--primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--secondary: oklch(0.968 0.007 247.896);
|
||||
--secondary-foreground: oklch(0.208 0.042 265.755);
|
||||
--muted: oklch(0.968 0.007 247.896);
|
||||
--muted-foreground: oklch(0.554 0.046 257.417);
|
||||
--accent: oklch(0.968 0.007 247.896);
|
||||
--accent-foreground: oklch(0.208 0.042 265.755);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.929 0.013 255.508);
|
||||
--input: oklch(0.929 0.013 255.508);
|
||||
--ring: oklch(0.704 0.04 256.788);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.984 0.003 247.858);
|
||||
--sidebar-foreground: oklch(0.129 0.042 264.695);
|
||||
--sidebar-primary: oklch(0.208 0.042 265.755);
|
||||
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-accent: oklch(0.968 0.007 247.896);
|
||||
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
|
||||
--sidebar-border: oklch(0.929 0.013 255.508);
|
||||
--sidebar-ring: oklch(0.704 0.04 256.788);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.129 0.042 264.695);
|
||||
--foreground: oklch(0.984 0.003 247.858);
|
||||
--card: oklch(0.208 0.042 265.755);
|
||||
--card-foreground: oklch(0.984 0.003 247.858);
|
||||
--popover: oklch(0.208 0.042 265.755);
|
||||
--popover-foreground: oklch(0.984 0.003 247.858);
|
||||
--primary: oklch(0.929 0.013 255.508);
|
||||
--primary-foreground: oklch(0.208 0.042 265.755);
|
||||
--secondary: oklch(0.279 0.041 260.031);
|
||||
--secondary-foreground: oklch(0.984 0.003 247.858);
|
||||
--muted: oklch(0.279 0.041 260.031);
|
||||
--muted-foreground: oklch(0.704 0.04 256.788);
|
||||
--accent: oklch(0.279 0.041 260.031);
|
||||
--accent-foreground: oklch(0.984 0.003 247.858);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.551 0.027 264.364);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.208 0.042 265.755);
|
||||
--sidebar-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-accent: oklch(0.279 0.041 260.031);
|
||||
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.551 0.027 264.364);
|
||||
--background: oklch(0.129 0.042 264.695);
|
||||
--foreground: oklch(0.984 0.003 247.858);
|
||||
--card: oklch(0.208 0.042 265.755);
|
||||
--card-foreground: oklch(0.984 0.003 247.858);
|
||||
--popover: oklch(0.208 0.042 265.755);
|
||||
--popover-foreground: oklch(0.984 0.003 247.858);
|
||||
--primary: oklch(0.929 0.013 255.508);
|
||||
--primary-foreground: oklch(0.208 0.042 265.755);
|
||||
--secondary: oklch(0.279 0.041 260.031);
|
||||
--secondary-foreground: oklch(0.984 0.003 247.858);
|
||||
--muted: oklch(0.279 0.041 260.031);
|
||||
--muted-foreground: oklch(0.704 0.04 256.788);
|
||||
--accent: oklch(0.279 0.041 260.031);
|
||||
--accent-foreground: oklch(0.984 0.003 247.858);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.551 0.027 264.364);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.208 0.042 265.755);
|
||||
--sidebar-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-accent: oklch(0.279 0.041 260.031);
|
||||
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.551 0.027 264.364);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,21 @@ export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||
export type WithoutChild<T> = T extends { child?: any; } ? Omit<T, "child"> : T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||
export type WithoutChildren<T> = T extends { children?: any; } ? Omit<T, "children"> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null; };
|
||||
|
||||
@@ -52,10 +52,17 @@ export const load: PageServerLoad = async (event) => {
|
||||
}
|
||||
};
|
||||
|
||||
const [stats, ingestionHistory, ingestionSources, recentSyncs] = await Promise.all([
|
||||
fetchStats(),
|
||||
fetchIngestionHistory(),
|
||||
fetchIngestionSources(),
|
||||
fetchRecentSyncs()
|
||||
]);
|
||||
|
||||
return {
|
||||
stats: fetchStats(),
|
||||
ingestionHistory: fetchIngestionHistory(),
|
||||
ingestionSources: fetchIngestionSources(),
|
||||
recentSyncs: fetchRecentSyncs()
|
||||
stats,
|
||||
ingestionHistory,
|
||||
ingestionSources,
|
||||
recentSyncs
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,226 +1,154 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '$lib/components/ui/card';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import * as Chart from '$lib/components/ui/chart/index.js';
|
||||
import { BarChart, PieChart, LineChart } from 'layerchart';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/components/ui/table';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { BarChart } from 'layerchart';
|
||||
import { formatBytes } from '$lib/utils';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
export let data: PageData;
|
||||
const { stats, ingestionHistory, ingestionSources, recentSyncs } = data;
|
||||
const chartConfig = {
|
||||
storageUsed: {
|
||||
label: 'Storage Used'
|
||||
},
|
||||
count: {
|
||||
label: 'Emails'
|
||||
label: 'Emails Ingested',
|
||||
color: '#2563eb'
|
||||
}
|
||||
} satisfies Chart.ChartConfig;
|
||||
|
||||
const formatBytes = (bytes: number, decimals = 2) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Dashboard - OpenArchiver</title>
|
||||
<meta name="description" content="System health and activity overview." />
|
||||
<meta name="description" content="Overview of your email archive." />
|
||||
</svelte:head>
|
||||
|
||||
<div class="container mx-auto">
|
||||
<h1 class="mb-6 text-3xl font-bold">Dashboard</h1>
|
||||
<div class="flex-1 space-y-4 p-8 pt-6">
|
||||
<div class="flex items-center justify-between space-y-2">
|
||||
<h2 class="text-3xl font-bold tracking-tight">Dashboard</h2>
|
||||
</div>
|
||||
|
||||
<!-- Ingestion Overview -->
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-2xl font-semibold">Ingestion Overview</h2>
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{#await stats}
|
||||
<p>Loading stats...</p>
|
||||
{:then statsData}
|
||||
{#if statsData}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Total Emails Archived</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p class="text-4xl font-bold">{statsData.totalEmailsArchived.toLocaleString()}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Total Storage Used</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p class="text-4xl font-bold">{formatBytes(statsData.totalStorageUsed)}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Failed Ingestions (7 days)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p
|
||||
class="text-4xl font-bold"
|
||||
class:text-red-500={statsData.failedIngestionsLast7Days > 0}
|
||||
>
|
||||
{statsData.failedIngestionsLast7Days}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
{/await}
|
||||
{#if data.stats}
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Card.Root>
|
||||
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Card.Title class="text-sm font-medium">Total Emails Archived</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="text-2xl font-bold">{data.stats.totalEmailsArchived}</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<Card.Root>
|
||||
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Card.Title class="text-sm font-medium">Total Storage Used</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="text-2xl font-bold">{formatBytes(data.stats.totalStorageUsed)}</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<Card.Root>
|
||||
<Card.Header class="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<Card.Title class="text-sm font-medium">Failed Ingestions (Last 7 Days)</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="text-2xl font-bold">{data.stats.failedIngestionsLast7Days}</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Charts -->
|
||||
<section class="mb-8 grid gap-8 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Emails Archived (Last 30 Days)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#await ingestionHistory}
|
||||
<p>Loading chart...</p>
|
||||
{:then historyData}
|
||||
{#if historyData}
|
||||
<div class="h-64">
|
||||
<Chart.Container config={chartConfig}>
|
||||
<LineChart data={historyData.history} x="date" y="count" />
|
||||
</Chart.Container>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Storage Usage by Source</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{#await ingestionSources}
|
||||
<p>Loading chart...</p>
|
||||
{:then sourceData}
|
||||
{#if sourceData}
|
||||
<div class="h-64">
|
||||
<Chart.Container config={chartConfig}>
|
||||
<PieChart
|
||||
data={sourceData}
|
||||
key="id"
|
||||
label="name"
|
||||
value="storageUsed"
|
||||
innerRadius={0.5}
|
||||
/>
|
||||
</Chart.Container>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
||||
<Card.Root class="col-span-4">
|
||||
<Card.Header>
|
||||
<Card.Title>Ingestion History</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="pl-2">
|
||||
{#if data.ingestionHistory && data.ingestionHistory.history.length > 0}
|
||||
<Chart.Container config={chartConfig} class="min-h-[200px] w-full">
|
||||
<BarChart
|
||||
data={data.ingestionHistory.history}
|
||||
x="date"
|
||||
y="count"
|
||||
axis="x"
|
||||
seriesLayout="group"
|
||||
props={{
|
||||
xAxis: {
|
||||
format: (d) =>
|
||||
new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#snippet tooltip()}
|
||||
<Chart.Tooltip />
|
||||
{/snippet}
|
||||
</BarChart>
|
||||
</Chart.Container>
|
||||
{:else}
|
||||
<p>No ingestion history available.</p>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- Ingestion Source Status -->
|
||||
<section class="mb-8">
|
||||
<h2 class="mb-4 text-2xl font-semibold">Ingestion Source Status</h2>
|
||||
<Card>
|
||||
<CardContent>
|
||||
{#await ingestionSources}
|
||||
<p>Loading sources...</p>
|
||||
{:then sourceData}
|
||||
{#if sourceData}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Provider</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead class="text-right">Storage Used</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each sourceData as source}
|
||||
<TableRow>
|
||||
<TableCell>{source.name}</TableCell>
|
||||
<TableCell>{source.provider}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
class={source.status === 'Active'
|
||||
? 'bg-green-500'
|
||||
: source.status === 'Error'
|
||||
? 'bg-red-500'
|
||||
: 'bg-gray-500'}
|
||||
>
|
||||
{source.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell class="text-right">{formatBytes(source.storageUsed)}</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/if}
|
||||
{/await}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
<Card.Root class="col-span-3">
|
||||
<Card.Header>
|
||||
<Card.Title>Recent Syncs</Card.Title>
|
||||
<Card.Description>Most recent sync activities.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
{#if data.recentSyncs && data.recentSyncs.length > 0}
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Source</Table.Head>
|
||||
<Table.Head>Status</Table.Head>
|
||||
<Table.Head>Processed</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each data.recentSyncs as sync}
|
||||
<Table.Row>
|
||||
<Table.Cell class="font-medium">{sync.sourceName}</Table.Cell>
|
||||
<Table.Cell>{sync.status}</Table.Cell>
|
||||
<Table.Cell>{sync.emailsProcessed}</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
{:else}
|
||||
<p>No recent syncs.</p>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
<!-- Recent Sync Jobs -->
|
||||
<section>
|
||||
<h2 class="mb-4 text-2xl font-semibold">Recent Sync Jobs</h2>
|
||||
<Card>
|
||||
<CardContent>
|
||||
{#await recentSyncs}
|
||||
<p>Loading syncs...</p>
|
||||
{:then syncData}
|
||||
{#if syncData && syncData.length > 0}
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Source</TableHead>
|
||||
<TableHead>Start Time</TableHead>
|
||||
<TableHead>Duration</TableHead>
|
||||
<TableHead>Emails Processed</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each syncData as sync}
|
||||
<TableRow>
|
||||
<TableCell>{sync.sourceName}</TableCell>
|
||||
<TableCell>{new Date(sync.startTime).toLocaleString()}</TableCell>
|
||||
<TableCell>{sync.duration}s</TableCell>
|
||||
<TableCell>{sync.emailsProcessed}</TableCell>
|
||||
<TableCell>
|
||||
<Badge class={sync.status === 'Completed' ? 'bg-green-500' : 'bg-red-500'}>
|
||||
{sync.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{:else}
|
||||
<p class="p-4 text-center">No recent sync jobs found.</p>
|
||||
{/if}
|
||||
{/await}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Ingestion Sources</Card.Title>
|
||||
<Card.Description>Overview of your ingestion sources.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
{#if data.ingestionSources && data.ingestionSources.length > 0}
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head>Name</Table.Head>
|
||||
<Table.Head>Provider</Table.Head>
|
||||
<Table.Head>Status</Table.Head>
|
||||
<Table.Head class="text-right">Storage Used</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each data.ingestionSources as source}
|
||||
<Table.Row>
|
||||
<Table.Cell class="font-medium">{source.name}</Table.Cell>
|
||||
<Table.Cell>{source.provider}</Table.Cell>
|
||||
<Table.Cell>{source.status}</Table.Cell>
|
||||
<Table.Cell class="text-right">{formatBytes(source.storageUsed)}</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
{:else}
|
||||
<p>No ingestion sources found.</p>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user