(state, form);
+ }, [state]);
+
+
+ return (
+ <>
+ setIsOpen(true)}>
+ {children}
+
+
+ >
+ )
+
+
+
+}
\ No newline at end of file
diff --git a/src/app/project/app/[tabName]/storage/storages.tsx b/src/app/project/app/[tabName]/storage/storages.tsx
new file mode 100644
index 0000000..5c2ed23
--- /dev/null
+++ b/src/app/project/app/[tabName]/storage/storages.tsx
@@ -0,0 +1,75 @@
+'use client';
+
+import { SubmitButton } from "@/components/custom/submit-button";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
+import { FormUtils } from "@/lib/form.utilts";
+import { AppSourceInfoInputModel, appSourceInfoInputZodModel } from "@/model/app-source-info.model";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { useFormState } from "react-dom";
+import { ServerActionResult } from "@/model/server-action-error-return.model";
+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";
+import { useEffect } from "react";
+import { toast } from "sonner";
+import { AppEnvVariablesModel, appEnvVariablesZodModel } from "@/model/env-edit.model";
+import { Textarea } from "@/components/ui/textarea";
+import { AppExtendedModel } from "@/model/app-extended.model";
+import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
+import { Button } from "@/components/ui/button";
+import { CheckIcon, CrossIcon, DeleteIcon, EditIcon, TrashIcon, XIcon } from "lucide-react";
+import DialogEditDialog from "./storage-edit-overlay";
+import { Toast } from "@/lib/toast.utils";
+import { deleteVolume } from "./actions";
+
+
+export default function StorageList({ app }: {
+ app: AppExtendedModel
+}) {
+ return <>
+
+
+ Storage
+ Add one or more volumes to your application.
+
+
+
+ {app.appVolumes.length} Storage
+
+
+ Mount Path
+ Size in GB
+ Action
+
+
+
+ {app.appVolumes.map(volume => (
+
+ {volume.containerMountPath}
+ {volume.size}
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ >;
+}
\ No newline at end of file
diff --git a/src/model/volume-edit.model.ts b/src/model/volume-edit.model.ts
new file mode 100644
index 0000000..a147fef
--- /dev/null
+++ b/src/model/volume-edit.model.ts
@@ -0,0 +1,9 @@
+import { stringToNumber } from "@/lib/zod.utils";
+import { z } from "zod";
+
+export const appVolumeEditZodModel = z.object({
+ containerMountPath: z.string().trim().min(1),
+ size: stringToNumber,
+})
+
+export type AppVolumeEditModel = z.infer;
\ No newline at end of file
diff --git a/src/server/services/app.service.ts b/src/server/services/app.service.ts
index c85fa81..bfc33ef 100644
--- a/src/server/services/app.service.ts
+++ b/src/server/services/app.service.ts
@@ -1,7 +1,7 @@
import { revalidateTag, unstable_cache } from "next/cache";
import dataAccess from "../adapter/db.client";
import { Tags } from "../utils/cache-tag-generator.utils";
-import { App, AppDomain, Prisma } from "@prisma/client";
+import { App, AppDomain, AppVolume, Prisma } from "@prisma/client";
import { DefaultArgs } from "@prisma/client/runtime/library";
import { AppExtendedModel } from "@/model/app-extended.model";
import { ServiceException } from "@/model/service.exception.model";
@@ -153,6 +153,62 @@ class AppService {
revalidateTag(Tags.apps(existingDomain.app.projectId));
}
}
+
+ async saveVolume(volumeToBeSaved: Prisma.AppVolumeUncheckedCreateInput | Prisma.AppVolumeUncheckedUpdateInput) {
+ let savedItem: AppVolume;
+ const existingApp = await this.getExtendedById(volumeToBeSaved.appId as string);
+ const existingAppWithSameVolumeMountPath = await dataAccess.client.appVolume.findFirst({
+ where: {
+ appId: volumeToBeSaved.appId as string,
+ containerMountPath: volumeToBeSaved.containerMountPath as string,
+ }
+ });
+ if (volumeToBeSaved.appId == existingAppWithSameVolumeMountPath?.appId && volumeToBeSaved.id !== existingAppWithSameVolumeMountPath?.id) {
+ throw new ServiceException("Volume mount path is already in use from another volume within the same app.");
+ }
+ try {
+ if (volumeToBeSaved.id) {
+ savedItem = await dataAccess.client.appVolume.update({
+ where: {
+ id: volumeToBeSaved.id as string
+ },
+ data: volumeToBeSaved
+ });
+ } else {
+ savedItem = await dataAccess.client.appVolume.create({
+ data: volumeToBeSaved as Prisma.AppVolumeUncheckedCreateInput
+ });
+ }
+
+ } finally {
+ revalidateTag(Tags.apps(existingApp.projectId as string));
+ revalidateTag(Tags.app(existingApp.id as string));
+ }
+ return savedItem;
+ }
+
+ async deleteVolumeById(id: string) {
+ const existingVolume = await dataAccess.client.appVolume.findFirst({
+ where: {
+ id
+ }, include: {
+ app: true
+ }
+ });
+ if (!existingVolume) {
+ return;
+ }
+ try {
+ await dataAccess.client.appVolume.delete({
+ where: {
+ id
+ }
+ });
+ } finally {
+ revalidateTag(Tags.app(existingVolume.appId));
+ revalidateTag(Tags.apps(existingVolume.app.projectId));
+ }
+ }
}
const appService = new AppService();