diff --git a/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx b/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx index 39e970fa..862a8bd8 100644 --- a/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx +++ b/app/(customer)/dashboard/(admin)/agents/[agentId]/page.tsx @@ -4,7 +4,7 @@ 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} from "@/components/ui/card"; +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"; @@ -16,6 +16,8 @@ import { ButtonDeleteProject } from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project"; import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent"; +import {capitalizeFirstLetter} from "@/utils/text"; +import {Server} from "lucide-react"; export default async function RoutePage(props: PageParams<{ agentId: string }>) { @@ -30,49 +32,16 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>) }) - if (!agent) { notFound() } - // - // const databaseId = 'db-123'; - // - - - - - // const totalBackupsResult = await db - // .select({ count: drizzleDb.schemas.backup.id }) - // .from(drizzleDb.schemas.backup) - // .where(eq(drizzleDb.schemas.backup.databaseId, databaseId)) - // .execute(); - // - // const totalBackups = totalBackupsResult.length; - // - // const successfulBackupsResult = await db - // .select({ count: drizzleDb.schemas.backup.id }) - // .from(drizzleDb.schemas.backup) - // .where( - // and( - // eq(drizzleDb.schemas.backup.databaseId, databaseId), - // eq(drizzleDb.schemas.backup.status, "success") - // ) - // ) - // .execute(); - // - // - // const successfulBackups = successfulBackupsResult.length; - // - // const successRate = - // totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null; - return (
- {agent.name} + {capitalizeFirstLetter(agent.name)} @@ -85,26 +54,26 @@ export default async function RoutePage(props: PageParams<{ agentId: string }>) {agent.description}
+ - - Databases - - {agent.databases.length} - - - - Success rate + + Databases + - {/*{successRate ?? "Unavailable for now."}*/} +
{agent.databases.length}
+

Databases linked to this agent

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

Last contact with agent

diff --git a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx index f8834372..952bc89e 100644 --- a/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx +++ b/app/(customer)/dashboard/(organization)/projects/[projectId]/database/[databaseId]/page.tsx @@ -86,11 +86,14 @@ export default async function RoutePage(props: PageParams<{ return (
- - {capitalizeFirstLetter(dbItem.name)} -
+ +
+ {capitalizeFirstLetter(dbItem.name)} +
+
- + {/* Do not delete*/} + {/**/}
diff --git a/app/(customer)/dashboard/home/page.tsx b/app/(customer)/dashboard/home/page.tsx index 1c0cbec2..da3bd95c 100644 --- a/app/(customer)/dashboard/home/page.tsx +++ b/app/(customer)/dashboard/home/page.tsx @@ -1,16 +1,13 @@ import {PageParams} from "@/types/next"; import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"; -import {Building2, DatabaseBackup, Folder, RefreshCcw} from "lucide-react"; -import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart"; -import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart"; +import {Building2, Database, DatabaseBackup, Folder, RefreshCcw, Server, Workflow} from "lucide-react"; import {currentUser} from "@/lib/auth/current-user"; import {notFound} from "next/navigation"; import {db} from "@/db"; -import {asc, eq, inArray} from "drizzle-orm"; +import {asc, inArray} from "drizzle-orm"; import * as drizzleDb from "@/db"; -import {auth, listOrganizations} from "@/lib/auth/auth"; -import {authClient} from "@/lib/auth/auth-client"; +import {listOrganizations} from "@/lib/auth/auth"; export default async function RoutePage(props: PageParams<{}>) { @@ -21,6 +18,7 @@ export default async function RoutePage(props: PageParams<{}>) { const organizationIds = organizations.map(project => project.id); + const agents = await db.query.agent.findMany({}); const projects = await db.query.project.findMany({ where: inArray(drizzleDb.schemas.project.organizationId, organizationIds), @@ -39,50 +37,96 @@ export default async function RoutePage(props: PageParams<{}>) { columns: { id: true, createdAt: true, + deletedAt: true, }, orderBy: [asc(drizzleDb.schemas.backup.id)], where: inArray(drizzleDb.schemas.backup.databaseId, databaseIds), }); + const availableBackups = backupsEvolution.map(backup => backup.deletedAt == null); + return ( - Dashboard -
- - - - Organizations +
+ + + Organizations + - {organizations.length} - - - - - Projects - - {projects.length} - - - - - Backups - - {backupsEvolution.length} + +
{organizations.length}
+

Number of organizations

+
+ + + Projects + + + +
{projects.length}
+

Number of projects

+
+
+ + + + Databases + + + +
{databasesOfAllProjects.length}
+

Databases across all projects

+
+
+ + + + Agents + + + +
{agents.length}
+

Registered agents

+
+
+ + + + Total Backups + + + +
{backupsEvolution.length}
+

All backups recorded

+
+
+ + + + Available Backups + + + +
{availableBackups.length}
+

Currently active backups

+
+
-
-
-
-
-
-
-
+ {/*Do not delete*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} diff --git a/app/api/agent/[agentId]/status/route.ts b/app/api/agent/[agentId]/status/route.ts index 09e559a0..3b0d2193 100644 --- a/app/api/agent/[agentId]/status/route.ts +++ b/app/api/agent/[agentId]/status/route.ts @@ -34,29 +34,33 @@ export async function POST( const agentId = (await params).agentId const body: Body = await request.json(); const lastContact = new Date(); + let message: string + if (!isUuidv4(agentId)) { + message = "agentId is not a valid uuid" + console.log(message) return NextResponse.json( {error: "agentId is not a valid uuid"}, {status: 500} ); } - - const agent = await db.query.agent.findFirst({ where: eq(drizzleDb.schemas.agent.id, agentId), }) if (!agent) { - return NextResponse.json({error: "Agent not found"}, {status: 404}) + message = "Agent not found" + console.log(message) + return NextResponse.json({error: message}, {status: 404}) } const databasesResponse = await handleDatabases(body, agent, lastContact) await db .update(drizzleDb.schemas.agent) - .set({ lastContact: lastContact }) + .set({lastContact: lastContact}) .where(eq(drizzleDb.schemas.agent.id, agentId)); eventEmitter.emit('modification', {update: true}); @@ -69,6 +73,7 @@ export async function POST( databases: databasesResponse } console.log(response) + return Response.json(response) } catch (error) { console.error('Error in POST handler:', error); diff --git a/src/components/wrappers/common/bread-crumbs/bread-crumbs.tsx b/src/components/wrappers/common/bread-crumbs/bread-crumbs.tsx new file mode 100644 index 00000000..78232fa5 --- /dev/null +++ b/src/components/wrappers/common/bread-crumbs/bread-crumbs.tsx @@ -0,0 +1,95 @@ +"use client" +import { + Breadcrumb, + BreadcrumbItem, BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import {usePathname} from "next/navigation"; +import Link from "next/link"; +import {useIsMobile} from "@/hooks/use-mobile"; +import {capitalizeFirstLetter, isUUID} from "@/utils/text"; + + +export function useBreadCrumbs() { + const pathname = usePathname(); + + const route_history = pathname + .split("/") + .filter((x: any) => x && x.length > 0); + + const breadcrumb_routes = route_history.reduce( + (acc: { name: string; path: string }[], route) => { + const prev_path = acc[acc.length - 1]?.path ?? ""; + acc.push({name: route, path: `${prev_path}/${route}`}); + return acc; + }, + [], + ); + return {breadcrumb_routes}; +} + + +interface BreadCrumbsProps { +} + + +export function BreadCrumbsWrapper() { + const isMobile = useIsMobile() + return ( + <> + {!isMobile ? : null} + + ) +} + +const FORBIDDEN_LINKS = ["organization", "dashboard", "database"]; + + +export function BreadCrumbs({}: BreadCrumbsProps) { + const {breadcrumb_routes} = useBreadCrumbs(); + if (breadcrumb_routes.length < 2) return null; + return ( +
+ + + {breadcrumb_routes.map((crumb: any) => { + const label = isUUID(crumb.name) ? "details" : crumb.name; + + const isLast = breadcrumb_routes.length - 1 === breadcrumb_routes.indexOf(crumb); + const isForbidden = FORBIDDEN_LINKS.includes(crumb.name.toLowerCase()); + + return ( +
+ + {isLast ? ( + {capitalizeFirstLetter(label)} + ) : ( + <> {isForbidden ? + + e.preventDefault()} + className="cursor-not-allowed " + > + {capitalizeFirstLetter(label)} + + + : + + {capitalizeFirstLetter(label)} + + } + + )} + + {!isLast && } +
+ ); + })} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/features/dashboard/backup/columns.tsx b/src/features/dashboard/backup/columns.tsx index 815d8290..768a217d 100644 --- a/src/features/dashboard/backup/columns.tsx +++ b/src/features/dashboard/backup/columns.tsx @@ -101,12 +101,10 @@ export function backupColumns(isAlreadyRestore: boolean, settings: Setting, data const mutationDeleteBackup = useMutation({ mutationFn: async () => { - - const deletion = await deleteBackupAction({ backupId: rowData.id, databaseId: rowData.databaseId, - file: rowData.file!, + file: rowData.file ?? "", projectSlug: database.project?.slug! }); // @ts-ignore diff --git a/src/features/layout/Header.tsx b/src/features/layout/Header.tsx index a2c5edff..8ddf724e 100644 --- a/src/features/layout/Header.tsx +++ b/src/features/layout/Header.tsx @@ -1,9 +1,10 @@ -import { notFound } from "next/navigation"; +import {notFound} from "next/navigation"; -import { SidebarTrigger } from "@/components/ui/sidebar"; -import { ModeToggle } from "@/features/theme/ModeToggle"; -import { currentUser } from "@/lib/auth/current-user"; +import {SidebarTrigger} from "@/components/ui/sidebar"; +import {ModeToggle} from "@/features/theme/ModeToggle"; +import {currentUser} from "@/lib/auth/current-user"; import {LoggedInButton} from "@/components/wrappers/dashboard/common/logged-in/logged-in-button"; +import {BreadCrumbsWrapper} from "@/components/wrappers/common/bread-crumbs/bread-crumbs"; export const Header = async () => { const user = await currentUser(); @@ -12,10 +13,14 @@ export const Header = async () => { } return (
- +
+ + +
+
- - + +
); diff --git a/src/features/layout/page.tsx b/src/features/layout/page.tsx index 1192a3b2..b85bb7ad 100644 --- a/src/features/layout/page.tsx +++ b/src/features/layout/page.tsx @@ -1,7 +1,7 @@ import { twx } from "@/lib/twx"; import { cn } from "@/lib/utils"; -export const Page = twx.div((props) => [cn(`flex flex-col h-full px-10 py-6`, props.className)]); +export const Page = twx.div((props) => [cn(`flex flex-col h-full px-5 md:px-10 py-6`, props.className)]); export const PageHeader = twx.div((props) => [cn(`flex justify-between`, props.className)]); diff --git a/src/utils/text.ts b/src/utils/text.ts index db8fbfa9..505b5469 100644 --- a/src/utils/text.ts +++ b/src/utils/text.ts @@ -8,3 +8,10 @@ export function truncateWords(text: string, wordLimit: number = 10): string { export function capitalizeFirstLetter(text: string): string { return text ? text.charAt(0).toUpperCase() + text.slice(1) : ""; } + + + + +export function isUUID(str: string) { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(str); +} \ No newline at end of file