Skip to content

Commit

Permalink
feat: settings & logs display
Browse files Browse the repository at this point in the history
  • Loading branch information
Pulkitxm committed Oct 14, 2024
1 parent a534c11 commit bff018a
Show file tree
Hide file tree
Showing 24 changed files with 4,612 additions and 1,898 deletions.
6 changes: 3 additions & 3 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

cd core && npm run format && npx tsc -b && npm run build
cd ../docker-builder npm run format && npm run build
cd ../proxy-server && npm run format
cd core && npm run format && npm run build
cd "../docker-builder "npm run format && npm run build
cd "../proxy-server" && npm run format
68 changes: 58 additions & 10 deletions core/actions/db/user.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use server";

import { prisma } from "@/db";
import { validateLogsZod } from "@/types/project";
import { getServerSession } from "next-auth";
Expand Down Expand Up @@ -57,17 +59,63 @@ export async function getProjectDetails(projectId: string) {
userId,
id: projectId,
},
select: {
id: true,
name: true,
repoName: true,
repoOwner: true,
branch: true,
updatedAt: true,
slug: true,
private: true,
},
});
if (!project) return null;

const validateLogs = validateLogsZod.safeParse(project.logs);
if (validateLogs.success)
return {
...project,
logs: validateLogs.data,
};
return {
...project,
logs: [],
};
return project;
}

export async function getProjectLogs(projectId: string) {
const logs = await prisma.project.findUnique({
where: {
id: projectId,
},
select: {
logs: true,
},
});
if (!logs) return null;

const parsedLogs = validateLogsZod.safeParse(logs.logs);
if (!parsedLogs.success) return null;

return parsedLogs.data;
}

export async function updateProject(
projectId: string,
values: {
name: string;
slug: string;
private: boolean;
},
) {
const session = await getServerSession();
if (!session) throw new Error("Session not found");

const userId = await getUserIdByEmail(session.user.email);
if (!userId) throw new Error("User not found");

const project = await prisma.project.update({
where: {
id: projectId,
},
data: {
name: values.name,
slug: values.slug,
private: values.private,
},
});

return project;
}
2 changes: 0 additions & 2 deletions core/actions/gh/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ export async function importProject(importProject: ImportProjectType) {
repoName: importProject.repoName,
repoOwner: importProject.repoOwner,
branch: importProject.branch,
buildCommand: importProject.build.buildCommand,
installCommand: importProject.build.installCommand,
slug: slug,
createdAt: new Date(),
updatedAt: new Date(),
Expand Down
2 changes: 2 additions & 0 deletions core/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "./globals.css";

import Navbar from "@/components/Navbar";
import AppWrapper from "../providers/app-wrapper";
import { Toaster } from "@/components/ui/toaster";

export const metadata: Metadata = {
title: "Create Next App",
Expand All @@ -20,6 +21,7 @@ export default function RootLayout({
<AppWrapper>
<Navbar />
{children}
<Toaster />
</AppWrapper>
</body>
</html>
Expand Down
42 changes: 42 additions & 0 deletions core/app/project/[projectId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { getProjectDetails } from "@/actions/db/user";
import ProtectRouteUI from "@/components/ProtectRoute";
import { getServerSession } from "next-auth";
import ProjectTabs from "@/components/ProjectDetails/ProjectTabs";

export default async function ProjectDetailsLayout({
children,
params,
}: {
children: React.ReactNode;
params: { projectId: string };
}) {
const session = await getServerSession();

if (!session) {
return (
<div className="flex h-screen w-screen items-center justify-center">
<ProtectRouteUI />
</div>
);
}

return (
<div className="container mx-auto mt-20 p-6">
<header className="mb-8">
<h1 className="mb-4 text-3xl font-bold text-gray-200">
Project Details
</h1>
<ProjectTabs projectId={params.projectId} />{" "}
{/* Use the client component */}
</header>
{children}
</div>
);
}

export async function generateMetadata({ params }: { params: { id: string } }) {
const project = await getProjectDetails(params.id);
return {
title: project ? `${project.name} - Project Details` : "Project Not Found",
};
}
17 changes: 17 additions & 0 deletions core/app/project/[projectId]/overview/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getProjectDetails } from "@/actions/db/user";
import ProjectDetails from "@/components/ProjectDetails";
import { notFound } from "next/navigation";

export default async function ProjectOverviewPage({
params,
}: {
params: { projectId: string };
}) {
const project = await getProjectDetails(params.projectId);

if (!project) {
return notFound();
}

return <ProjectDetails project={project} />;
}
47 changes: 5 additions & 42 deletions core/app/project/[projectId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,9 @@
import { getProjectDetails } from "@/actions/db/user";
import ErroDiv from "@/components/ui/ErrorDiv";
import ProtectRouteUI from "@/components/ProtectRoute";
import { getServerSession } from "next-auth";
import ProjectDetails from "@/components/ProjectDetails";
import { redirect } from "next/navigation";

export default async function ProjectDetailsPage({
projectId,
export default function ProjectPage({
params,
}: {
projectId: string;
params: { projectId: string };
}) {
const session = await getServerSession();
if (!session)
return (
<div className="flex h-screen w-screen items-center justify-center">
<ProtectRouteUI />
</div>
);

let project: Awaited<ReturnType<typeof getProjectDetails>> = null;
try {
project = await getProjectDetails(projectId);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
console.log(e);
return (
<div className="flex h-screen w-screen items-center justify-center">
<ProtectRouteUI />
</div>
);
}

if (!project) {
return (
<div className="flex h-screen w-screen items-center justify-center">
<ErroDiv
title="Project Not Found"
description="The project you're looking for doesn't exist."
link="/dashboard"
/>
</div>
);
}

return <ProjectDetails project={project} />;
redirect(`/project/${params.projectId}/overview`);
}
17 changes: 17 additions & 0 deletions core/app/project/[projectId]/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getProjectDetails } from "@/actions/db/user";
import ProjectSettings from "@/components/ProjectDetails/ProjectSetting";
import { notFound } from "next/navigation";

export default async function ProjectOverviewPage({
params,
}: {
params: { projectId: string };
}) {
const project = await getProjectDetails(params.projectId);

if (!project) {
return notFound();
}

return <ProjectSettings project={project} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@ import { Button } from "@/components/ui/button";
import { importProject } from "@/actions/gh/import";
import { ImportProjectType } from "@/types/project";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Loader2 } from "lucide-react";

export default function ImportButton({
importProjectData,
}: {
importProjectData: ImportProjectType;
}) {
const router = useRouter();
const [loading, setLoading] = useState(false);
async function handleImportProject() {
setLoading(true);
const { id } = await importProject(importProjectData);
if (id) router.push(`/project/${id}`);
setLoading(false);
}
return (
<Button className="w-full" onClick={handleImportProject}>
Deploy
{loading ? <Loader2 className="h-5 w-5 animate-spin" /> : "Deploy"}
</Button>
);
}

2 changes: 1 addition & 1 deletion core/components/ImportProject/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import ChooseDir from "./ChooseDir";
import BuilAndOutput from "./BuilAndOutput";
import EnvironmentVariable from "./EnvironmentVariable";
import ChooseBranch from "./ChooseBranch";
import ImportButton from "@/app/import/ImportButton";
import ImportButton from "@/components/ImportProject/ImportButton";

export default function ImportProject({
importProjectData,
Expand Down
107 changes: 107 additions & 0 deletions core/components/ProjectDetails/ProjectLogs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client";

import { getProjectDetails, getProjectLogs } from "@/actions/db/user";
import { LogsType } from "@/types/project";
import React, { useCallback, useEffect, useState } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { RefreshCw } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { ToastAction } from "@radix-ui/react-toast";

export default function ProjectLogs({
project,
}: {
project: Exclude<Awaited<ReturnType<typeof getProjectDetails>>, null>;
}) {
const [loading, setLoading] = useState(false);
const [projectLogs, setProjectLogs] = useState<LogsType>([]);
const { toast } = useToast();

const handleFetch = useCallback(async () => {
setLoading(true);
try {
const logs = await getProjectLogs(project.id);
if (logs) {
setProjectLogs(logs);
} else {
throw new Error("Failed to fetch logs");
}
} catch (error) {
toast({
title: "Error",
description:
error instanceof Error ? error.message : "An unknown error occurred",
variant: "destructive",
action: <ToastAction altText="Try again">Try again</ToastAction>,
});
} finally {
setLoading(false);
}
}, [project.id, toast]);

useEffect(() => {
handleFetch();
}, [handleFetch]);

const LogContent = () => {
if (loading) {
return (
<div className="space-y-3">
{[...Array(5)].map((_, index) => (
<div key={index} className="flex items-center space-x-2">
<Skeleton className="h-6 w-32" />
<Skeleton className="h-4 flex-1" />
</div>
))}
</div>
);
}

if (projectLogs.length === 0) {
return <p className="text-sm text-gray-400">No recent logs.</p>;
}

return (
<ul className="space-y-3">
{projectLogs.map((log, index) => (
<li
key={index}
className="flex items-start border-b border-gray-700 py-2 text-sm text-gray-300"
>
<p className="mr-2 shrink-0 whitespace-nowrap rounded bg-gray-700 px-2 py-1 text-xs font-medium text-gray-300">
{log.timestamp.toUTCString()}
</p>
<p className="break-words">{log.value}</p>
</li>
))}
</ul>
);
};

return (
<Card className="shadow-lg dark:bg-gray-900">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-xl font-semibold">Recent Activity</CardTitle>
<Button
variant="outline"
size="sm"
onClick={handleFetch}
disabled={loading}
>
<RefreshCw className="mr-2 h-4 w-4" />
Load Logs
</Button>
</CardHeader>
<CardContent>
<p className="mb-4 text-sm">
Last update: {project.updatedAt.toUTCString()}
</p>
<div className="max-h-[400px] overflow-y-auto pr-2">
<LogContent />
</div>
</CardContent>
</Card>
);
}
Loading

0 comments on commit bff018a

Please sign in to comment.