added env vars

This commit is contained in:
biersoeckli
2024-10-24 13:47:05 +00:00
parent feb1a349f4
commit 35c838df81
15 changed files with 306 additions and 78 deletions

View File

@@ -0,0 +1,50 @@
'use client'
import { useRouter } 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 EnvEdit from "./environment/env-edit";
import { App } from "@prisma/client";
import DomainsList from "./domains/domains";
import { AppExtendedModel } from "@/model/app-extended.model";
export default function AppTabs({
app,
tabName
}: {
app: AppExtendedModel;
tabName: string;
}) {
const router = useRouter();
const openTab = (tabName: string) => {
router.push(`/project/app/${tabName}?appId=${app.id}`);
}
return (
<Tabs defaultValue="general" value={tabName} onValueChange={(newTab) => openTab(newTab)} 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 app={app} />
<GeneralAppRateLimits app={app} />
</TabsContent>
<TabsContent value="environment" className="space-y-4">
<EnvEdit app={app} />
</TabsContent>
<TabsContent value="domains" className="space-y-4">
<DomainsList app={app} />
</TabsContent>
<TabsContent value="storage">storage</TabsContent>
<TabsContent value="logs">logs</TabsContent>
</Tabs>
)
}

View File

@@ -0,0 +1,17 @@
'use server'
import { AppEnvVariablesModel, appEnvVariablesZodModel } from "@/model/env-edit.model";
import appService from "@/server/services/app.service";
import { getAuthUserSession, saveFormAction } from "@/server/utils/action-wrapper.utils";
export const saveEnvVariables = async (prevState: any, inputData: AppEnvVariablesModel, appId: string) =>
saveFormAction(inputData, appEnvVariablesZodModel, async (validatedData) => {
await getAuthUserSession();
const existingApp = await appService.getById(appId);
await appService.save({
...existingApp,
...validatedData,
id: appId,
});
});

View File

@@ -0,0 +1,39 @@
'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 { saveEnvVariables } 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";
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";
export default function DomainsList({ app }: {
app: AppExtendedModel
}) {
return <>
<Card>
<CardHeader>
<CardTitle>Domains</CardTitle>
<CardDescription>Add custom domains to your application. If your app has a domain configured, it will be public and accessible via the internet.</CardDescription>
</CardHeader>
</Card >
</>;
}

View File

@@ -0,0 +1,17 @@
'use server'
import { AppEnvVariablesModel, appEnvVariablesZodModel } from "@/model/env-edit.model";
import appService from "@/server/services/app.service";
import { getAuthUserSession, saveFormAction } from "@/server/utils/action-wrapper.utils";
export const saveEnvVariables = async (prevState: any, inputData: AppEnvVariablesModel, appId: string) =>
saveFormAction(inputData, appEnvVariablesZodModel, async (validatedData) => {
await getAuthUserSession();
const existingApp = await appService.getById(appId);
await appService.save({
...existingApp,
...validatedData,
id: appId,
});
});

View File

@@ -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 { saveEnvVariables } 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";
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";
export default function EnvEdit({ app }: {
app: AppExtendedModel
}) {
const form = useForm<AppEnvVariablesModel>({
resolver: zodResolver(appEnvVariablesZodModel),
defaultValues: app
});
const [state, formAction] = useFormState((state: ServerActionResult<any, any>, payload: AppEnvVariablesModel) => saveEnvVariables(state, payload, app.id), FormUtils.getInitialFormState<typeof appEnvVariablesZodModel>());
useEffect(() => {
if (state.status === 'success') {
toast.success('Env Variables Limits Saved');
}
FormUtils.mapValidationErrorsToForm<typeof appEnvVariablesZodModel>(state, form);
}, [state]);
const sourceTypeField = form.watch();
return <>
<Card>
<CardHeader>
<CardTitle>Environment Variables</CardTitle>
<CardDescription>Provide optional environment variables for your application.</CardDescription>
</CardHeader>
<Form {...form}>
<form action={(e) => form.handleSubmit((data) => {
return formAction(data);
})()}>
<CardContent className="space-y-4">
<FormField
control={form.control}
name="envVars"
render={({ field }) => (
<FormItem>
<FormLabel>Env Variables</FormLabel>
<FormControl>
<Textarea className="h-96" placeholder="NAME=VALUE..." {...field} value={field.value} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
<CardFooter>
<SubmitButton>Save</SubmitButton>
</CardFooter>
</form>
</Form >
</Card >
</>;
}

View File

@@ -17,10 +17,11 @@ import { AppRateLimitsModel, appRateLimitsZodModel } from "@/model/app-rate-limi
import { App } from "@prisma/client";
import { useEffect } from "react";
import { toast } from "sonner";
import { AppExtendedModel } from "@/model/app-extended.model";
export default function GeneralAppRateLimits({ app }: {
app: App
app: AppExtendedModel
}) {
const form = useForm<AppRateLimitsModel>({
resolver: zodResolver(appRateLimitsZodModel),

View File

@@ -16,9 +16,10 @@ import { Label } from "@/components/ui/label";
import { useEffect } from "react";
import { App } from "@prisma/client";
import { toast } from "sonner";
import { AppExtendedModel } from "@/model/app-extended.model";
export default function GeneralAppSource({ app }: {
app: App
app: AppExtendedModel
}) {
const form = useForm<AppSourceInfoInputModel>({
resolver: zodResolver(appSourceInfoInputZodModel),

View File

@@ -0,0 +1,52 @@
import { getAuthUserSession } from "@/server/utils/action-wrapper.utils";
import appService from "@/server/services/app.service";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import projectService from "@/server/services/project.service";
import PageTitle from "@/components/custom/page-title";
import AppTabs from "./app-tabs";
export default async function AppPage({
searchParams,
params
}: {
searchParams?: { [key: string]: string | undefined };
params: { tabName: string };
}) {
await getAuthUserSession();
const appId = searchParams?.appId;
if (!appId) {
return <p>Could not find app with id {appId}</p>
}
const app = await appService.getExtendedById(appId);
return (
<div className="flex-1 space-y-6 p-8 pt-6">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Projects</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href={`/project?projectId=${app.projectId}`}>{app.project.name}</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink>{app.name}</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<PageTitle
title={app.name}
subtitle={`App ID: "${app.id}"`}>
</PageTitle>
<AppTabs app={app} tabName={params.tabName} />
</div>
)
}

View File

@@ -1,72 +0,0 @@
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";
import appService from "@/server/services/app.service";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import projectService from "@/server/services/project.service";
export default async function AppPage({
searchParams,
}: {
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);
const project = await projectService.getById(app.projectId);
return (
<div className="flex-1 space-y-4 p-8 pt-6">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/">Projects</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href={`/project?projectId=${app.projectId}`}>{project.name}</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink>{app.name}</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<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 app={app} />
<GeneralAppRateLimits app={app} />
</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

@@ -16,6 +16,7 @@ import {
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"
import PageTitle from "@/components/custom/page-title";
export default async function AppsPage({
@@ -44,10 +45,11 @@ export default async function AppsPage({
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="flex gap-4">
<h2 className="text-3xl font-bold tracking-tight flex-1">Apps</h2>
<PageTitle
title="Apps"
subtitle={`All Apps for Project "${project.name}"`}>
<CreateAppDialog projectId={projectId} />
</div>
</PageTitle>
<AppTable data={data} />
</div>
)

View File

@@ -0,0 +1,15 @@
'use client'
export default function PageTitle({ title, subtitle, children }: {
title: string;
subtitle?: string;
children?: React.ReactNode;
}) {
return <div className="flex gap-4">
<div className="flex-1 space-y-2">
<h2 className="text-3xl font-bold tracking-tight ">{title}</h2>
<p className="text-xs text-slate-600">{subtitle}</p>
</div>
{children}
</div>
}

View File

@@ -0,0 +1,11 @@
import { z } from "zod";
import { AppDomainModel, AppModel, AppVolumeModel, ProjectModel, RelatedAppModel } from "./generated-zod";
export const AppExtendedZodModel= z.lazy(() => AppModel.extend({
project: ProjectModel,
appDomains: AppDomainModel.array(),
appVolumes: AppVolumeModel.array(),
}))
export type AppExtendedModel = z.infer<typeof AppExtendedZodModel>;

View File

@@ -0,0 +1,7 @@
import { z } from "zod";
export const appEnvVariablesZodModel = z.object({
envVars: z.string(),
})
export type AppEnvVariablesModel = z.infer<typeof appEnvVariablesZodModel>;

View File

@@ -3,6 +3,7 @@ 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";
import { AppExtendedModel } from "@/model/app-extended.model";
class AppService {
@@ -20,7 +21,7 @@ class AppService {
}
async getAllAppsByProjectID(projectId: string) {
return await unstable_cache(async (projectId:string) => await dataAccess.client.app.findMany({
return await unstable_cache(async (projectId: string) => await dataAccess.client.app.findMany({
where: {
projectId
}
@@ -30,6 +31,18 @@ class AppService {
})(projectId as string);
}
async getExtendedById(id: string): Promise<AppExtendedModel> {
return dataAccess.client.app.findFirstOrThrow({
where: {
id
}, include: {
project: true,
appDomains: true,
appVolumes: true,
}
});
}
async getById(id: string) {
return dataAccess.client.app.findFirstOrThrow({
where: {