mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-01-04 18:53:00 -06:00
created create process from project to app
This commit is contained in:
29
prisma/migrations/20241024113318_migration/migration.sql
Normal file
29
prisma/migrations/20241024113318_migration/migration.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_App" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"sourceType" TEXT NOT NULL DEFAULT 'GIT',
|
||||
"containerImageSource" TEXT,
|
||||
"gitUrl" TEXT,
|
||||
"gitBranch" TEXT,
|
||||
"gitUsername" TEXT,
|
||||
"gitToken" TEXT,
|
||||
"dockerfilePath" TEXT NOT NULL DEFAULT './Dockerfile',
|
||||
"replicas" INTEGER NOT NULL DEFAULT 1,
|
||||
"envVars" TEXT NOT NULL DEFAULT '',
|
||||
"memoryReservation" INTEGER,
|
||||
"memoryLimit" INTEGER,
|
||||
"cpuReservation" INTEGER,
|
||||
"cpuLimit" INTEGER,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "App_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_App" ("containerImageSource", "cpuLimit", "cpuReservation", "createdAt", "dockerfilePath", "envVars", "gitBranch", "gitToken", "gitUrl", "gitUsername", "id", "memoryLimit", "memoryReservation", "name", "projectId", "replicas", "sourceType", "updatedAt") SELECT "containerImageSource", "cpuLimit", "cpuReservation", "createdAt", "dockerfilePath", "envVars", "gitBranch", "gitToken", "gitUrl", "gitUsername", "id", "memoryLimit", "memoryReservation", "name", "projectId", "replicas", "sourceType", "updatedAt" FROM "App";
|
||||
DROP TABLE "App";
|
||||
ALTER TABLE "new_App" RENAME TO "App";
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -128,16 +128,16 @@ model App {
|
||||
project Project @relation(fields: [projectId], references: [id])
|
||||
sourceType String @default("GIT") // GIT, CONTAINER
|
||||
|
||||
containerImageSource String
|
||||
containerImageSource String?
|
||||
|
||||
gitUrl String
|
||||
gitBranch String
|
||||
gitUrl String?
|
||||
gitBranch String?
|
||||
gitUsername String?
|
||||
gitToken String?
|
||||
dockerfilePath String @default("./Dockerfile")
|
||||
|
||||
replicas Int @default(1)
|
||||
envVars String
|
||||
envVars String @default("")
|
||||
|
||||
memoryReservation Int?
|
||||
memoryLimit Int?
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
'use server'
|
||||
|
||||
import { AppModel } from "@/model/generated-zod";
|
||||
import { SuccessActionResult } from "@/model/server-action-error-return.model";
|
||||
import appService from "@/server/services/app.service";
|
||||
import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { z } from "zod";
|
||||
|
||||
const createAppSchema = z.object({
|
||||
projectName: z.string().min(1)
|
||||
});
|
||||
|
||||
export const createApp = async (projectName: string) =>
|
||||
saveFormAction({ projectName }, createAppSchema, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
|
||||
await appService.save({
|
||||
name: validatedData.projectName
|
||||
});
|
||||
|
||||
return new SuccessActionResult(undefined, "Project created successfully.");
|
||||
});
|
||||
|
||||
export const deleteProject = async (projectId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await appService.deleteById(projectId);
|
||||
return new SuccessActionResult(undefined, "Project deleted successfully.");
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { InputDialog } from "@/components/custom/input-dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Toast } from "@/lib/toast.utils";
|
||||
import { createProject } from "./actions";
|
||||
|
||||
|
||||
export function CreateProjectDialog() {
|
||||
|
||||
const createProj = async (name: string | undefined) => {
|
||||
if (!name) {
|
||||
return true;
|
||||
}
|
||||
const result = await Toast.fromAction(() => createProject(name));
|
||||
return result.status === "success";
|
||||
};
|
||||
|
||||
return <InputDialog
|
||||
title="Create Project"
|
||||
description="Name your new project."
|
||||
fieldName="Name"
|
||||
onResult={createProj}>
|
||||
<Button>Create Project</Button>
|
||||
</InputDialog>
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
'use server'
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import Link from "next/link";
|
||||
import { getAuthUserSession, getUserSession } from "@/server/utils/action-wrapper.utils";
|
||||
import projectService from "@/server/services/project.service";
|
||||
import ProjectsTable from "./apps-table";
|
||||
import { CreateProjectDialog } from "./create-app-dialog";
|
||||
|
||||
export default async function ProjectPage() {
|
||||
|
||||
await getAuthUserSession();
|
||||
const data = await projectService.getAllProjects();
|
||||
return (
|
||||
<div className="flex-1 space-y-4 p-8 pt-6">
|
||||
<div className="flex gap-4">
|
||||
<h2 className="text-3xl font-bold tracking-tight flex-1">Projects</h2>
|
||||
<CreateProjectDialog />
|
||||
</div>
|
||||
<ProjectsTable data={data} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import ProjectPage from "./projects/page";
|
||||
import ProjectPage from "./projects/project-page";
|
||||
|
||||
export default function Home() {
|
||||
return <ProjectPage />;
|
||||
|
||||
29
src/app/project/actions.ts
Normal file
29
src/app/project/actions.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
'use server'
|
||||
|
||||
import { SuccessActionResult } from "@/model/server-action-error-return.model";
|
||||
import appService from "@/server/services/app.service";
|
||||
import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
|
||||
import { z } from "zod";
|
||||
|
||||
const createAppSchema = z.object({
|
||||
appName: z.string().min(1)
|
||||
});
|
||||
|
||||
export const createApp = async (appName: string, projectId: string) =>
|
||||
saveFormAction({ appName }, createAppSchema, async (validatedData) => {
|
||||
await getAuthUserSession();
|
||||
|
||||
const returnData = await appService.save({
|
||||
name: validatedData.appName,
|
||||
projectId
|
||||
});
|
||||
|
||||
return new SuccessActionResult(returnData, "App created successfully.");
|
||||
});
|
||||
|
||||
export const deleteApp = async (appId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
await appService.deleteById(appId);
|
||||
return new SuccessActionResult(undefined, "App deleted successfully.");
|
||||
});
|
||||
@@ -14,11 +14,15 @@ import { Input } from "@/components/ui/input";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { AppRateLimitsModel, appRateLimitsZodModel } from "@/model/app-rate-limits.model";
|
||||
import { App } from "@prisma/client";
|
||||
|
||||
|
||||
export default function GeneralAppRateLimits() {
|
||||
export default function GeneralAppRateLimits({ app }: {
|
||||
app: App
|
||||
}) {
|
||||
const form = useForm<AppRateLimitsModel>({
|
||||
resolver: zodResolver(appRateLimitsZodModel)
|
||||
resolver: zodResolver(appRateLimitsZodModel),
|
||||
defaultValues: app
|
||||
});
|
||||
|
||||
const appId = '123'; // todo get from url;
|
||||
@@ -14,10 +14,17 @@ import { Input } from "@/components/ui/input";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useEffect } from "react";
|
||||
import { App } from "@prisma/client";
|
||||
|
||||
export default function GeneralAppSource() {
|
||||
export default function GeneralAppSource({ app }: {
|
||||
app: App
|
||||
}) {
|
||||
const form = useForm<AppSourceInfoInputModel>({
|
||||
resolver: zodResolver(appSourceInfoInputZodModel)
|
||||
resolver: zodResolver(appSourceInfoInputZodModel),
|
||||
defaultValues: {
|
||||
...app,
|
||||
sourceType: app.sourceType as 'GIT' | 'CONTAINER'
|
||||
}
|
||||
});
|
||||
|
||||
const appId = '123'; // todo get from url;
|
||||
@@ -4,6 +4,7 @@ import { redirect } from "next/navigation";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import GeneralAppRateLimits from "./general/app-rate-limits";
|
||||
import GeneralAppSource from "./general/app-source";
|
||||
import appService from "@/server/services/app.service";
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +14,11 @@ export default async function AppPage({
|
||||
searchParams?: { [key: string]: string | undefined };
|
||||
}) {
|
||||
await getAuthUserSession();
|
||||
const appId = searchParams?.appId;
|
||||
if (!appId) {
|
||||
return <p>Could not find app with id {appId}</p>
|
||||
}
|
||||
const app = await appService.getById(appId);
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-4 p-8 pt-6">
|
||||
@@ -30,8 +36,8 @@ export default async function AppPage({
|
||||
</TabsList>
|
||||
<TabsContent value="overview">Domains, Logs, etc.</TabsContent>
|
||||
<TabsContent value="general" className="space-y-4">
|
||||
<GeneralAppSource />
|
||||
<GeneralAppRateLimits />
|
||||
<GeneralAppSource app={app} />
|
||||
<GeneralAppRateLimits app={app} />
|
||||
</TabsContent>
|
||||
<TabsContent value="environment">environment</TabsContent>
|
||||
<TabsContent value="domains">domains</TabsContent>
|
||||
@@ -9,11 +9,11 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel,
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
import { Toast } from "@/lib/toast.utils";
|
||||
import { Project } from "@prisma/client";
|
||||
import { deleteProject } from "./actions";
|
||||
import { deleteApp } from "./actions";
|
||||
|
||||
|
||||
|
||||
export default function ProjectsTable({ data }: { data: Project[] }) {
|
||||
export default function AppTable({ data }: { data: Project[] }) {
|
||||
|
||||
return <>
|
||||
<SimpleDataTable columns={[
|
||||
@@ -23,7 +23,7 @@ export default function ProjectsTable({ data }: { data: Project[] }) {
|
||||
["updatedAt", "Updated At", false, (item) => formatDateTime(item.updatedAt)],
|
||||
]}
|
||||
data={data}
|
||||
onItemClickLink={(item) => `/project?id=${item.id}`}
|
||||
onItemClickLink={(item) => `/project/app?appId=${item.id}`}
|
||||
actionCol={(item) =>
|
||||
<>
|
||||
<div className="flex">
|
||||
@@ -37,14 +37,14 @@ export default function ProjectsTable({ data }: { data: Project[] }) {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<Link href={`/project?id=${item.id}`}>
|
||||
<Link href={`/project/app?appId=${item.id}`}>
|
||||
<DropdownMenuItem>
|
||||
<span>Show Apps of Project</span>
|
||||
<span>Show App Details</span>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => Toast.fromAction(() => deleteProject(item.id))}>
|
||||
<span className="text-red-500">Delete Project</span>
|
||||
<DropdownMenuItem onClick={() => Toast.fromAction(() => deleteApp(item.id))}>
|
||||
<span className="text-red-500">Delete App</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
38
src/app/project/create-app-dialog.tsx
Normal file
38
src/app/project/create-app-dialog.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
|
||||
import { InputDialog } from "@/components/custom/input-dialog"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Toast } from "@/lib/toast.utils";
|
||||
import { createApp } from "./actions";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
|
||||
export function CreateAppDialog({
|
||||
projectId
|
||||
}: {
|
||||
projectId: string;
|
||||
}) {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const createAppFunc = async (name: string | undefined) => {
|
||||
if (!name) {
|
||||
return true;
|
||||
}
|
||||
const result = await Toast.fromAction(() => createApp(name, projectId));
|
||||
if (result.status === "success") {
|
||||
router.push(`/project/app?appId=${result.data.id}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return <InputDialog
|
||||
title="Create App"
|
||||
description="Name your new App."
|
||||
fieldName="Name"
|
||||
onResult={createAppFunc}>
|
||||
<Button>Create App</Button>
|
||||
</InputDialog>
|
||||
}
|
||||
33
src/app/project/page.tsx
Normal file
33
src/app/project/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use server'
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import Link from "next/link";
|
||||
import { getAuthUserSession, getUserSession } from "@/server/utils/action-wrapper.utils";
|
||||
import projectService from "@/server/services/project.service";
|
||||
import AppTable from "./apps-table";
|
||||
import { CreateAppDialog } from "./create-app-dialog";
|
||||
import appService from "@/server/services/app.service";
|
||||
|
||||
export default async function AppsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams?: { [key: string]: string | undefined };
|
||||
}) {
|
||||
await getAuthUserSession();
|
||||
|
||||
const projectId = searchParams?.projectId;
|
||||
if (!projectId) {
|
||||
return <p>Could not find project with id {projectId}</p>
|
||||
}
|
||||
const data = await appService.getAllAppsByProjectID(projectId);
|
||||
return (
|
||||
<div className="flex-1 space-y-4 p-8 pt-6">
|
||||
<div className="flex gap-4">
|
||||
<h2 className="text-3xl font-bold tracking-tight flex-1">Apps</h2>
|
||||
<CreateAppDialog projectId={projectId} />
|
||||
</div>
|
||||
<AppTable data={data} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export default function ProjectsTable({ data }: { data: Project[] }) {
|
||||
["updatedAt", "Updated At", false, (item) => formatDateTime(item.updatedAt)],
|
||||
]}
|
||||
data={data}
|
||||
onItemClickLink={(item) => `/project?id=${item.id}`}
|
||||
onItemClickLink={(item) => `/project?projectId=${item.id}`}
|
||||
actionCol={(item) =>
|
||||
<>
|
||||
<div className="flex">
|
||||
@@ -37,7 +37,7 @@ export default function ProjectsTable({ data }: { data: Project[] }) {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<Link href={`/project?id=${item.id}`}>
|
||||
<Link href={`/project?projectId=${item.id}`}>
|
||||
<DropdownMenuItem>
|
||||
<span>Show Apps of Project</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -7,9 +7,9 @@ export const AppModel = z.object({
|
||||
name: z.string(),
|
||||
projectId: z.string(),
|
||||
sourceType: z.string(),
|
||||
containerImageSource: z.string(),
|
||||
gitUrl: z.string(),
|
||||
gitBranch: z.string(),
|
||||
containerImageSource: z.string().nullish(),
|
||||
gitUrl: z.string().nullish(),
|
||||
gitBranch: z.string().nullish(),
|
||||
gitUsername: z.string().nullish(),
|
||||
gitToken: z.string().nullish(),
|
||||
dockerfilePath: z.string(),
|
||||
|
||||
@@ -31,7 +31,7 @@ class AppService {
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
return dataAccess.client.app.findUnique({
|
||||
return dataAccess.client.app.findFirstOrThrow({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user