added first version of app edit view

This commit is contained in:
biersoeckli
2024-10-24 11:27:55 +00:00
parent a97cd08217
commit 89a480fa6d
11 changed files with 448 additions and 5 deletions

View File

@@ -19,7 +19,8 @@
"esbenp.prettier-vscode",
"VisualStudioExptTeam.vscodeintellicode",
"mhutchie.git-graph",
"donjayamanne.githistory"
"donjayamanne.githistory",
"qwtel.sqlite-viewer"
]
}
},

View File

@@ -28,6 +28,7 @@
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.3",
"@tanstack/react-table": "^8.20.5",
"@types/bcrypt": "^5.0.2",

View File

@@ -0,0 +1,34 @@
'use server'
import { AppRateLimitsModel, appRateLimitsZodModel } from "@/model/app-rate-limits.model";
import { appSourceInfoContainerZodModel, appSourceInfoGitZodModel, AppSourceInfoInputModel, appSourceInfoInputZodModel } from "@/model/app-source-info.model";
import { AuthFormInputSchema, authFormInputSchemaZod } from "@/model/auth-form";
import { ErrorActionResult, ServerActionResult } from "@/model/server-action-error-return.model";
import { ServiceException } from "@/model/service.exception.model";
import userService from "@/server/services/user.service";
import { getAuthUserSession, saveFormAction, simpleAction } from "@/server/utils/action-wrapper.utils";
export const saveGeneralAppSourceInfo = async (prevState: any, inputData: AppSourceInfoInputModel, appId: string) => {
if (inputData.sourceType === 'GIT') {
return saveFormAction(inputData, appSourceInfoGitZodModel, async (validatedData) => {
console.log(validatedData)
await getAuthUserSession();
});
} else if (inputData.sourceType === 'CONTAINER') {
return saveFormAction(inputData, appSourceInfoContainerZodModel, async (validatedData) => {
console.log(validatedData)
await getAuthUserSession();
});
} else {
return simpleAction(async () => new ServerActionResult('error', undefined, 'Invalid Source Type', undefined));
}
};
export const saveGeneralAppRateLimits = async (prevState: any, inputData: AppRateLimitsModel, appId: string) =>
saveFormAction(inputData, appRateLimitsZodModel, async (validatedData) => {
console.log(validatedData)
await getAuthUserSession();
});

View File

@@ -0,0 +1,107 @@
'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 { saveGeneralAppRateLimits, saveGeneralAppSourceInfo } from "./actions";
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";
export default function GeneralAppRateLimits() {
const form = useForm<AppRateLimitsModel>({
resolver: zodResolver(appRateLimitsZodModel)
});
const appId = '123'; // todo get from url;
const [state, formAction] = useFormState((state: ServerActionResult<any, any>, payload: AppRateLimitsModel) => saveGeneralAppRateLimits(state, payload, appId), FormUtils.getInitialFormState<typeof appRateLimitsZodModel>());
const sourceTypeField = form.watch();
return <>
<Card>
<CardHeader>
<CardTitle>Rate Limits</CardTitle>
<CardDescription>Provide optional rate Limits per running container instance.</CardDescription>
</CardHeader>
<Form {...form}>
<form action={(e) => form.handleSubmit((data) => {
return formAction(data);
})()}>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="memoryLimit"
render={({ field }) => (
<FormItem>
<FormLabel>Memory Limit (MB)</FormLabel>
<FormControl>
<Input type="number" {...field} value={field.value as string | number | readonly string[] | undefined} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="memoryReservation"
render={({ field }) => (
<FormItem>
<FormLabel>Memory Reservation (MB)</FormLabel>
<FormControl>
<Input type="number" {...field} value={field.value as string | number | readonly string[] | undefined} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="cpuLimit"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Limit (m)</FormLabel>
<FormControl>
<Input type="number" {...field} value={field.value as string | number | readonly string[] | undefined} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="cpuReservation"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Reservation (m)</FormLabel>
<FormControl>
<Input type="number" {...field} value={field.value as string | number | readonly string[] | undefined} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
<CardFooter>
<SubmitButton>Save</SubmitButton>
</CardFooter>
</form>
</Form >
</Card >
</>;
}

View File

@@ -0,0 +1,166 @@
'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 { saveGeneralAppSourceInfo } from "./actions";
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 { useEffect } from "react";
export default function GeneralAppSource() {
const form = useForm<AppSourceInfoInputModel>({
resolver: zodResolver(appSourceInfoInputZodModel)
});
const appId = '123'; // todo get from url;
const [state, formAction] = useFormState((state: ServerActionResult<any, any>, payload: AppSourceInfoInputModel) => saveGeneralAppSourceInfo(state, payload, appId), FormUtils.getInitialFormState<typeof appSourceInfoInputZodModel>());
useEffect(() => {
FormUtils.mapValidationErrorsToForm<typeof appSourceInfoInputZodModel>(state, form)
}, [state]);
const sourceTypeField = form.watch();
return <>
<Card>
<CardHeader>
<CardTitle>Source</CardTitle>
<CardDescription>Provide Information about the Source of your Application.</CardDescription>
</CardHeader>
<Form {...form}>
<form action={(e) => form.handleSubmit((data) => {
return formAction(data);
})()}>
<CardContent className="space-y-4">
<div className="hidden">
<FormField
control={form.control}
name="sourceType"
render={({ field }) => (
<FormItem>
<FormLabel>Source Type</FormLabel>
<FormControl>
<Input {...field} value={field.value as string | number | readonly string[] | undefined} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Label>Source Type</Label>
<Tabs defaultValue="GIT" value={sourceTypeField.sourceType} onValueChange={(val) => {
form.setValue('sourceType', val as 'GIT' | 'CONTAINER');
}} className="mt-2">
<TabsList>
<TabsTrigger value="GIT">Git</TabsTrigger>
<TabsTrigger value="CONTAINER">Docker Container</TabsTrigger>
</TabsList>
<TabsContent value="GIT" className="space-y-4 mt-4">
<FormField
control={form.control}
name="gitUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Git Repo URL</FormLabel>
<FormControl>
<Input {...field} value={field.value as string | number | readonly string[] | undefined} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="gitUsername"
render={({ field }) => (
<FormItem>
<FormLabel>Git Username (optional)</FormLabel>
<FormControl>
<Input {...field} value={field.value as string | number | readonly string[] | undefined} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="gitToken"
render={({ field }) => (
<FormItem>
<FormLabel>Git Token (optional)</FormLabel>
<FormControl>
<Input type="password" {...field} value={field.value as string | number | readonly string[] | undefined} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="gitBranch"
render={({ field }) => (
<FormItem>
<FormLabel>Git Branch</FormLabel>
<FormControl>
<Input {...field} value={field.value as string | number | readonly string[] | undefined} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="dockerfilePath"
render={({ field }) => (
<FormItem>
<FormLabel>Path to Dockerfile</FormLabel>
<FormControl>
<Input placeholder="./Dockerfile" {...field} value={field.value as string | number | readonly string[] | undefined} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</TabsContent>
<TabsContent value="CONTAINER" className="space-y-4 mt-4">
<FormField
control={form.control}
name="containerImageSource"
render={({ field }) => (
<FormItem>
<FormLabel>Docker Container Name</FormLabel>
<FormControl>
<Input {...field} value={field.value as string | number | readonly string[] | undefined} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
</Tabs>
</CardContent>
<CardFooter className="gap-4">
<SubmitButton>Save</SubmitButton>
<p className="text-red-500">{state?.message}</p>
</CardFooter>
</form>
</Form >
</Card >
</>;
}

43
src/app/app/page.tsx Normal file
View File

@@ -0,0 +1,43 @@
import userService from "@/server/services/user.service";
import { getAuthUserSession, getUserSession } from "@/server/utils/action-wrapper.utils";
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";
export default async function AppPage({
searchParams,
}: {
searchParams?: { [key: string]: string | undefined };
}) {
await getAuthUserSession();
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">App</h2>
</div>
<Tabs defaultValue="general" className="space-y-4">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="storage">Storage</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
</TabsList>
<TabsContent value="overview">Domains, Logs, etc.</TabsContent>
<TabsContent value="general" className="space-y-4">
<GeneralAppSource />
<GeneralAppRateLimits />
</TabsContent>
<TabsContent value="environment">environment</TabsContent>
<TabsContent value="domains">domains</TabsContent>
<TabsContent value="storage">storage</TabsContent>
<TabsContent value="logs">logs</TabsContent>
</Tabs>
</div>
)
}

View File

@@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -0,0 +1,10 @@
import { z } from "zod";
export const appRateLimitsZodModel = z.object({
memoryReservation: z.number().nullish(),
memoryLimit: z.number().nullish(),
cpuReservation: z.number().nullish(),
cpuLimit: z.number().nullish(),
})
export type AppRateLimitsModel = z.infer<typeof appRateLimitsZodModel>;

View File

@@ -0,0 +1,27 @@
import { z } from "zod";
export const appSourceInfoInputZodModel = z.object({
sourceType: z.enum(["GIT", "CONTAINER"]),
containerImageSource: z.string().nullish(),
gitUrl: z.string().nullish(),
gitBranch: z.string().nullish(),
gitUsername: z.string().nullish(),
gitToken: z.string().nullish(),
dockerfilePath: z.string().nullish(),
});
export type AppSourceInfoInputModel = z.infer<typeof appSourceInfoInputZodModel>;
export const appSourceInfoGitZodModel = z.object({
gitUrl: z.string(),
gitBranch: z.string(),
gitUsername: z.string().nullish(),
gitToken: z.string().nullish(),
dockerfilePath: z.string(),
});
export type AppSourceInfoGitModel = z.infer<typeof appSourceInfoGitZodModel>;
export const appSourceInfoContainerZodModel = z.object({
containerImageSource: z.string(),
});
export type AppSourceInfoContainerModel = z.infer<typeof appSourceInfoContainerZodModel>;

View File

@@ -1,5 +1,4 @@
import { FormZodErrorValidationCallback } from "@/lib/form.utilts";
import { z, ZodType } from "zod";
export class ServerActionResult<TErrorData, TReturnData> {

View File

@@ -56,12 +56,12 @@ export async function saveFormAction<ReturnType, TInputData, ZodType extends Zod
const validatedFields = schemaWithoutIgnoredFields.safeParse(inputData);
if (!validatedFields.success) {
console.error('Validation failed for input:', inputData, 'with errors:', validatedFields.error.flatten().fieldErrors);
throw new FormValidationException('Bitte überprüfen Sie Ihre eingaben.', validatedFields.error.flatten().fieldErrors);
throw new FormValidationException('Please correct the errors in the form.', validatedFields.error.flatten().fieldErrors);
}
if (!validatedFields.data) {
console.error('No data available after validation of input:', validatedFields.data);
throw new ServiceException('Ein unbekannter Fehler ist aufgetreten.');
throw new ServiceException('An unknown error occurred.');
}
return await func(validatedFields.data);
}, redirectOnSuccessPath);
@@ -104,7 +104,7 @@ export async function simpleAction<ReturnType, ValidationCallbackType>(
console.error(ex)
return {
status: 'error',
message: 'Ein unbekannter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.'
message: 'An unknown error occurred.'
} as ServerActionResult<ValidationCallbackType, ReturnType>;
}
}