mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-17 01:29:30 -06:00
App service implemented
This commit is contained in:
35
prisma/migrations/20241024084212_migration/migration.sql
Normal file
35
prisma/migrations/20241024084212_migration/migration.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `containerImageSource` to the `App` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- 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 NOT NULL,
|
||||
"gitUrl" TEXT NOT NULL,
|
||||
"gitBranch" TEXT NOT NULL,
|
||||
"gitUsername" TEXT,
|
||||
"gitToken" TEXT,
|
||||
"dockerfilePath" TEXT NOT NULL DEFAULT './Dockerfile',
|
||||
"replicas" INTEGER NOT NULL DEFAULT 1,
|
||||
"envVars" TEXT NOT NULL,
|
||||
"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" ("cpuLimit", "cpuReservation", "createdAt", "dockerfilePath", "envVars", "gitBranch", "gitToken", "gitUrl", "gitUsername", "id", "memoryLimit", "memoryReservation", "name", "projectId", "replicas", "updatedAt") SELECT "cpuLimit", "cpuReservation", "createdAt", "dockerfilePath", "envVars", "gitBranch", "gitToken", "gitUrl", "gitUsername", "id", "memoryLimit", "memoryReservation", "name", "projectId", "replicas", "updatedAt" FROM "App";
|
||||
DROP TABLE "App";
|
||||
ALTER TABLE "new_App" RENAME TO "App";
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
29
src/app/apps/actions.ts
Normal file
29
src/app/apps/actions.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
'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.");
|
||||
});
|
||||
55
src/app/apps/apps-table.tsx
Normal file
55
src/app/apps/apps-table.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import Link from "next/link";
|
||||
import { SimpleDataTable } from "@/components/custom/simple-data-table";
|
||||
import { formatDateTime } from "@/lib/format.utils";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { MoreHorizontal } from "lucide-react";
|
||||
import { Toast } from "@/lib/toast.utils";
|
||||
import { Project } from "@prisma/client";
|
||||
import { deleteProject } from "./actions";
|
||||
|
||||
|
||||
|
||||
export default function ProjectsTable({ data }: { data: Project[] }) {
|
||||
|
||||
return <>
|
||||
<SimpleDataTable columns={[
|
||||
['id', 'ID', false],
|
||||
['name', 'Name', true],
|
||||
["createdAt", "Created At", true, (item) => formatDateTime(item.createdAt)],
|
||||
["updatedAt", "Updated At", false, (item) => formatDateTime(item.updatedAt)],
|
||||
]}
|
||||
data={data}
|
||||
onItemClickLink={(item) => `/project?id=${item.id}`}
|
||||
actionCol={(item) =>
|
||||
<>
|
||||
<div className="flex">
|
||||
<div className="flex-1"></div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<Link href={`/project?id=${item.id}`}>
|
||||
<DropdownMenuItem>
|
||||
<span>Show Apps of Project</span>
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => Toast.fromAction(() => deleteProject(item.id))}>
|
||||
<span className="text-red-500">Delete Project</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
26
src/app/apps/create-app-dialog.tsx
Normal file
26
src/app/apps/create-app-dialog.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'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>
|
||||
}
|
||||
24
src/app/apps/page.tsx
Normal file
24
src/app/apps/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
'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,3 +1,5 @@
|
||||
'use server'
|
||||
|
||||
import userService from "@/server/services/user.service";
|
||||
import UserRegistrationForm from "./register-from";
|
||||
import UserLoginForm from "./login-from";
|
||||
|
||||
@@ -6,6 +6,8 @@ export const AppModel = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
projectId: z.string(),
|
||||
sourceType: z.string(),
|
||||
containerImageSource: z.string(),
|
||||
gitUrl: z.string(),
|
||||
gitBranch: z.string(),
|
||||
gitUsername: z.string().nullish(),
|
||||
|
||||
62
src/server/services/app.service.ts
Normal file
62
src/server/services/app.service.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { revalidateTag, unstable_cache } from "next/cache";
|
||||
import dataAccess from "../adapter/db.client";
|
||||
import { Tags } from "../utils/cache-tag-generator.utils";
|
||||
import { App, Prisma, Project } from "@prisma/client";
|
||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
||||
|
||||
class AppService {
|
||||
|
||||
async deleteById(id: string) {
|
||||
const existingItem = await this.getById(id);
|
||||
if (!existingItem) {
|
||||
return;
|
||||
}
|
||||
await dataAccess.client.app.delete({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
});
|
||||
revalidateTag(Tags.apps(existingItem.projectId));
|
||||
}
|
||||
|
||||
async getAllAppsByProjectID(projectId: string) {
|
||||
return await unstable_cache(async (projectId:string) => await dataAccess.client.app.findMany({
|
||||
where: {
|
||||
projectId
|
||||
}
|
||||
}),
|
||||
[Tags.apps(projectId)], {
|
||||
tags: [Tags.apps(projectId)]
|
||||
})(projectId as string);
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
return dataAccess.client.app.findUnique({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async save(item: Prisma.AppUncheckedCreateInput | Prisma.AppUncheckedUpdateInput) {
|
||||
let savedItem: Prisma.Prisma__AppClient<App, never, DefaultArgs>;
|
||||
if (item.id) {
|
||||
savedItem = dataAccess.client.app.update({
|
||||
where: {
|
||||
id: item.id as string
|
||||
},
|
||||
data: item
|
||||
});
|
||||
} else {
|
||||
savedItem = dataAccess.client.app.create({
|
||||
data: item as Prisma.AppUncheckedCreateInput
|
||||
});
|
||||
}
|
||||
|
||||
revalidateTag(Tags.apps(item.projectId as string));
|
||||
return savedItem;
|
||||
}
|
||||
}
|
||||
|
||||
const appService = new AppService();
|
||||
export default appService;
|
||||
@@ -7,4 +7,8 @@ export class Tags {
|
||||
static projects() {
|
||||
return `projects`;
|
||||
}
|
||||
|
||||
static apps(projectId: string) {
|
||||
return `apps-${projectId}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user