aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/components')
-rw-r--r--apps/web/components/admin/BasicStats.tsx (renamed from apps/web/components/admin/ServerStats.tsx)31
-rw-r--r--apps/web/components/admin/ServiceConnections.tsx150
2 files changed, 173 insertions, 8 deletions
diff --git a/apps/web/components/admin/ServerStats.tsx b/apps/web/components/admin/BasicStats.tsx
index e60f0fbe..67352f66 100644
--- a/apps/web/components/admin/ServerStats.tsx
+++ b/apps/web/components/admin/BasicStats.tsx
@@ -1,10 +1,10 @@
"use client";
-import LoadingSpinner from "@/components/ui/spinner";
+import { AdminCard } from "@/components/admin/AdminCard";
import { useClientConfig } from "@/lib/clientConfig";
import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
-import { keepPreviousData, useQuery } from "@tanstack/react-query";
+import { useQuery } from "@tanstack/react-query";
const REPO_LATEST_RELEASE_API =
"https://api.github.com/repos/karakeep-app/karakeep/releases/latest";
@@ -54,20 +54,35 @@ function ReleaseInfo() {
);
}
-export default function ServerStats() {
+function StatsSkeleton() {
+ return (
+ <AdminCard>
+ <div className="mb-4 h-7 w-32 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
+ <div className="flex flex-col gap-4 sm:flex-row">
+ {[1, 2, 3].map((i) => (
+ <div key={i} className="rounded-md border bg-background p-4 sm:w-1/4">
+ <div className="mb-2 h-4 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
+ <div className="h-9 w-16 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
+ </div>
+ ))}
+ </div>
+ </AdminCard>
+ );
+}
+
+export default function BasicStats() {
const { t } = useTranslation();
const { data: serverStats } = api.admin.stats.useQuery(undefined, {
refetchInterval: 5000,
- placeholderData: keepPreviousData,
});
if (!serverStats) {
- return <LoadingSpinner />;
+ return <StatsSkeleton />;
}
return (
- <div className="flex flex-col gap-4">
- <div className="mb-2 text-xl font-medium">
+ <AdminCard>
+ <div className="mb-4 text-xl font-medium">
{t("admin.server_stats.server_stats")}
</div>
<div className="flex flex-col gap-4 sm:flex-row">
@@ -92,6 +107,6 @@ export default function ServerStats() {
<ReleaseInfo />
</div>
</div>
- </div>
+ </AdminCard>
);
}
diff --git a/apps/web/components/admin/ServiceConnections.tsx b/apps/web/components/admin/ServiceConnections.tsx
new file mode 100644
index 00000000..8d79d8bb
--- /dev/null
+++ b/apps/web/components/admin/ServiceConnections.tsx
@@ -0,0 +1,150 @@
+"use client";
+
+import { AdminCard } from "@/components/admin/AdminCard";
+import { useTranslation } from "@/lib/i18n/client";
+import { api } from "@/lib/trpc";
+
+function ConnectionStatus({
+ label,
+ configured,
+ connected,
+ pluginName,
+ error,
+}: {
+ label: string;
+ configured: boolean;
+ connected: boolean;
+ pluginName?: string;
+ error?: string;
+}) {
+ const { t } = useTranslation();
+
+ let statusText = t("admin.service_connections.status.not_configured");
+ let badgeColor =
+ "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400";
+ let iconColor = "text-gray-400";
+ let borderColor = "border-gray-200 dark:border-gray-700";
+
+ if (configured) {
+ if (connected) {
+ statusText = t("admin.service_connections.status.connected");
+ badgeColor =
+ "bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400";
+ iconColor = "text-green-500";
+ borderColor = "border-green-200 dark:border-green-800";
+ } else {
+ statusText = t("admin.service_connections.status.disconnected");
+ badgeColor =
+ "bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400";
+ iconColor = "text-red-500";
+ borderColor = "border-red-200 dark:border-red-800";
+ }
+ }
+
+ return (
+ <div
+ className={`rounded-lg border ${borderColor} bg-background p-5 shadow-sm transition-all sm:w-1/3`}
+ >
+ <div className="mb-3 flex items-center justify-between">
+ <div>
+ <div className="text-base font-semibold">{label}</div>
+ {pluginName && (
+ <div className="mt-1 text-xs text-muted-foreground">
+ {pluginName}
+ </div>
+ )}
+ </div>
+ <div
+ className={`flex h-2 w-2 items-center justify-center rounded-full ${iconColor}`}
+ >
+ <div
+ className={`h-2 w-2 rounded-full ${connected && configured ? "animate-pulse" : ""} bg-current`}
+ ></div>
+ </div>
+ </div>
+ <div className="mb-2">
+ <span
+ className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${badgeColor}`}
+ >
+ {statusText}
+ </span>
+ </div>
+ {error && (
+ <div className="mt-3 rounded-md bg-red-50 p-2 dark:bg-red-900/10">
+ <p className="text-xs text-red-600 dark:text-red-400" title={error}>
+ {error.length > 60 ? `${error.substring(0, 60)}...` : error}
+ </p>
+ </div>
+ )}
+ </div>
+ );
+}
+
+function ConnectionsSkeleton() {
+ return (
+ <AdminCard>
+ <div className="mb-4 h-7 w-40 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
+ <div className="flex flex-col gap-4 sm:flex-row">
+ {[1, 2, 3].map((i) => (
+ <div
+ key={i}
+ className="rounded-lg border border-gray-200 bg-background p-5 shadow-sm dark:border-gray-700 sm:w-1/3"
+ >
+ <div className="mb-3 flex items-center justify-between">
+ <div className="h-5 w-28 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
+ <div className="h-2 w-2 animate-pulse rounded-full bg-gray-300 dark:bg-gray-600"></div>
+ </div>
+ <div className="mb-2">
+ <div className="h-5 w-20 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </AdminCard>
+ );
+}
+
+export default function ServiceConnections() {
+ const { t } = useTranslation();
+ const { data: connections } = api.admin.checkConnections.useQuery(undefined, {
+ refetchInterval: 10000,
+ });
+
+ if (!connections) {
+ return <ConnectionsSkeleton />;
+ }
+
+ return (
+ <AdminCard>
+ <div className="mb-2 text-xl font-medium">
+ {t("admin.service_connections.title")}
+ </div>
+ <p className="mb-4 text-sm text-muted-foreground">
+ {t("admin.service_connections.description")}
+ </p>
+ <div className="flex flex-col gap-4 sm:flex-row">
+ <ConnectionStatus
+ label={t("admin.service_connections.search_engine")}
+ configured={connections.searchEngine.configured}
+ connected={connections.searchEngine.connected}
+ pluginName={connections.searchEngine.pluginName}
+ error={connections.searchEngine.error}
+ />
+ <ConnectionStatus
+ label={t("admin.service_connections.browser")}
+ configured={connections.browser.configured}
+ connected={connections.browser.connected}
+ pluginName={connections.browser.pluginName}
+ error={connections.browser.error}
+ />
+ <ConnectionStatus
+ label={t("admin.service_connections.queue_system")}
+ configured={connections.queue.configured}
+ connected={connections.queue.connected}
+ pluginName={connections.queue.pluginName}
+ error={connections.queue.error}
+ />
+ </div>
+ </AdminCard>
+ );
+}