mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-01-01 09:10:26 -06:00
added first version of app edit view
This commit is contained in:
@@ -19,7 +19,8 @@
|
||||
"esbenp.prettier-vscode",
|
||||
"VisualStudioExptTeam.vscodeintellicode",
|
||||
"mhutchie.git-graph",
|
||||
"donjayamanne.githistory"
|
||||
"donjayamanne.githistory",
|
||||
"qwtel.sqlite-viewer"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
34
src/app/app/general/actions.ts
Normal file
34
src/app/app/general/actions.ts
Normal 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();
|
||||
|
||||
});
|
||||
107
src/app/app/general/app-rate-limits.tsx
Normal file
107
src/app/app/general/app-rate-limits.tsx
Normal 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 >
|
||||
|
||||
</>;
|
||||
}
|
||||
166
src/app/app/general/app-source.tsx
Normal file
166
src/app/app/general/app-source.tsx
Normal 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
43
src/app/app/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
55
src/components/ui/tabs.tsx
Normal file
55
src/components/ui/tabs.tsx
Normal 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 }
|
||||
10
src/model/app-rate-limits.model.ts
Normal file
10
src/model/app-rate-limits.model.ts
Normal 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>;
|
||||
27
src/model/app-source-info.model.ts
Normal file
27
src/model/app-source-info.model.ts
Normal 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>;
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FormZodErrorValidationCallback } from "@/lib/form.utilts";
|
||||
import { z, ZodType } from "zod";
|
||||
|
||||
export class ServerActionResult<TErrorData, TReturnData> {
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user