diff --git a/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx b/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx index d0c4870c..08484d53 100644 --- a/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx +++ b/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx @@ -2,21 +2,16 @@ import Link from "next/link"; import {GearIcon} from "@radix-ui/react-icons"; import {PageParams} from "@/types/next"; import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page"; -import {formatDateLastContact} from "@/utils/date-formatting"; import {buttonVariants} from "@/components/ui/button"; -import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"; -import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; -import {DatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card"; -import {AgentCardKey} from "@/components/wrappers/dashboard/agent/agent-card-key/agent-card-key"; import {db} from "@/db"; import * as drizzleDb from "@/db"; import {eq} from "drizzle-orm"; import {notFound} from "next/navigation"; import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent"; import {capitalizeFirstLetter} from "@/utils/text"; -import {Server} from "lucide-react"; import {generateEdgeKey} from "@/utils/edge_key"; import {getServerUrl} from "@/utils/get-server-url"; +import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content"; export default async function RoutePage(props: PageParams<{ agentId: string }>) { @@ -54,42 +49,10 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>) {agent.description} )} -
- - - Databases - - - -
{agent.databases.length}
-

Databases linked to this agent

-
-
- - - - Last contact - - - -
{formatDateLastContact(agent.lastContact)}
-

Last contact with agent

-
-
- -
- - - Edge Key - - - - - - +
) diff --git a/app/(customer)/dashboard/(organization)/_databases/[databaseId]/restore/page.tsx b/app/(customer)/dashboard/(organization)/_databases/[databaseId]/restore/page.tsx deleted file mode 100644 index c2dd2316..00000000 --- a/app/(customer)/dashboard/(organization)/_databases/[databaseId]/restore/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import {PageParams} from "@/types/next"; -import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; -import {notFound} from "next/navigation"; -// import {RestoreForm} from "@/components/wrappers/dashboard/database/restore-form"; -import {currentUser} from "@/lib/auth/current-user"; - -import {db} from "@/db"; -import {eq} from "drizzle-orm"; -import * as drizzleDb from "@/db"; - - -export default async function RoutePage( - props: PageParams<{ - databaseId: string; - }> -) { - const {databaseId} = await props.params; - - const user = await currentUser(); - if (!user) notFound(); - - const [dbToRestore] = await db.select().from(drizzleDb.schemas.database).where(eq(drizzleDb.schemas.database.id, databaseId)); - - if (!dbToRestore) { - notFound(); - } - - const dbsOfSameType = await db.select().from(drizzleDb.schemas.database).where(eq(drizzleDb.schemas.database.dbms!, dbToRestore.dbms!)); - const successfulBackups = await db.select().from(drizzleDb.schemas.backup).where(eq(drizzleDb.schemas.backup.status, "success")); - - return ( - - - Restore {dbToRestore.name} - - - {/**/} - - - ); -} diff --git a/app/providers.tsx b/app/providers.tsx index 7bb5f6d5..8ec7abcd 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -5,6 +5,7 @@ import {ThemeProvider} from "@/features/theme/theme-provider"; import {Toaster} from "@/components/ui/sonner"; import {QueryClient, QueryClientProvider} from "@tanstack/react-query"; +import {ThemeMetaUpdaterRoot} from "@/features/browser/theme-meta-updater-root"; export type ProviderProps = PropsWithChildren<{}>; @@ -18,6 +19,7 @@ export const Providers = (props: ProviderProps) => { defaultTheme="system" enableSystem > + {props.children} diff --git a/src/components/wrappers/dashboard/agent/agent-content.tsx b/src/components/wrappers/dashboard/agent/agent-content.tsx new file mode 100644 index 00000000..b6485398 --- /dev/null +++ b/src/components/wrappers/dashboard/agent/agent-content.tsx @@ -0,0 +1,79 @@ +"use client" + +import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"; +import {Server} from "lucide-react"; +import {formatDateLastContact} from "@/utils/date-formatting"; +import {AgentCardKey} from "@/components/wrappers/dashboard/agent/agent-card-key/agent-card-key"; +import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; +import {DatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card"; +import {AgentWithDatabases} from "@/db/schema/08_agent"; +import {eventUpdate} from "@/types/events"; +import {useAutoRefresh} from "@/hooks/use-auto-refresh"; + +type AgentContentPageProps = { + edgeKey: string; + agent: AgentWithDatabases + +} + +export const AgentContentPage = ({edgeKey, agent}: AgentContentPageProps) => { + + useAutoRefresh({ + poll: { + enabled: true, + intervalMs: 5000, + }, + sse: { + enabled: true, + url: "/api/events", + eventName: "modification", + shouldRefresh: (data) => { + const update = data as eventUpdate; + return Boolean(update?.update); + }, + }, + }); + + + return ( + <> +
+ + + Databases + + + +
{agent.databases.length}
+

Databases linked to this agent

+
+
+ + + + Last contact + + + +
{formatDateLastContact(agent.lastContact)}
+

Last contact with agent

+
+
+ +
+ + + Edge Key + + + + + + + + + ) +} \ No newline at end of file diff --git a/src/db/schema/08_agent.ts b/src/db/schema/08_agent.ts index bf4f2018..5e2b7529 100644 --- a/src/db/schema/08_agent.ts +++ b/src/db/schema/08_agent.ts @@ -28,3 +28,7 @@ export const agentRelations = relations(agent, ({many}) => ({ export type AgentWith = Agent & { databases?: Database[] | null; }; + +export type AgentWithDatabases = Agent & { + databases: Database[] | []; +}; diff --git a/src/features/browser/theme-meta-updater-root.tsx b/src/features/browser/theme-meta-updater-root.tsx new file mode 100644 index 00000000..d532e70f --- /dev/null +++ b/src/features/browser/theme-meta-updater-root.tsx @@ -0,0 +1,28 @@ +"use client"; + +import {useTheme} from "next-themes"; +import {useEffect, useState} from "react"; + +export function ThemeMetaUpdaterRoot() { + const {resolvedTheme} = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!mounted || !resolvedTheme) return; + + const color = resolvedTheme === "dark" ? "#000000" : "#ffffff"; + let tag = document.querySelector('meta[name="theme-color"]'); + if (!tag) { + tag = document.createElement("meta"); + tag.name = "theme-color"; + document.head.appendChild(tag); + } + tag.content = color; + }, [mounted, resolvedTheme]); + + return null; +} diff --git a/src/hooks/use-auto-refresh.tsx b/src/hooks/use-auto-refresh.tsx new file mode 100644 index 00000000..ab53c04a --- /dev/null +++ b/src/hooks/use-auto-refresh.tsx @@ -0,0 +1,71 @@ +"use client"; + +import {useEffect, useRef} from "react"; +import {useRouter} from "next/navigation"; + +type UseAutoRefreshOptions = { + poll?: { + intervalMs: number; + enabled?: boolean; + }; + sse?: { + url: string; + eventName?: string; + enabled?: boolean; + shouldRefresh?: (data: unknown) => boolean; + }; +}; + +export function useAutoRefresh(options: UseAutoRefreshOptions) { + const router = useRouter(); + const eventSourceRef = useRef(null); + + useEffect(() => { + if (!options.poll?.enabled) return; + if (!options.poll.intervalMs) return; + + const interval = setInterval(() => { + router.refresh(); + }, options.poll.intervalMs); + + return () => clearInterval(interval); + }, [options.poll?.enabled, options.poll?.intervalMs, router]); + + useEffect(() => { + if (!options.sse?.enabled) return; + if (!options.sse.url) return; + + const eventSource = new EventSource(options.sse.url); + eventSourceRef.current = eventSource; + + const eventName = options.sse.eventName ?? "message"; + + eventSource.addEventListener(eventName, (event: MessageEvent) => { + let payload: unknown = event.data; + + try { + payload = JSON.parse(event.data); + } catch { + // keep raw payload + } + + const shouldRefresh = + options.sse?.shouldRefresh?.(payload) ?? true; + + if (shouldRefresh) { + console.log("Received event source", eventSource); + router.refresh(); + } + }); + + return () => { + eventSource.close(); + eventSourceRef.current = null; + }; + }, [ + options.sse?.enabled, + options.sse?.url, + options.sse?.eventName, + router, + ]); +}