mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-01-12 14:39:35 -06:00
added env vars
This commit is contained in:
50
src/app/project/app/[tabName]/app-tabs.tsx
Normal file
50
src/app/project/app/[tabName]/app-tabs.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
17
src/app/project/app/[tabName]/domains/actions.ts
Normal file
17
src/app/project/app/[tabName]/domains/actions.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
39
src/app/project/app/[tabName]/domains/domains.tsx
Normal file
39
src/app/project/app/[tabName]/domains/domains.tsx
Normal 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 >
|
||||
|
||||
</>;
|
||||
}
|
||||
17
src/app/project/app/[tabName]/environment/actions.ts
Normal file
17
src/app/project/app/[tabName]/environment/actions.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
75
src/app/project/app/[tabName]/environment/env-edit.tsx
Normal file
75
src/app/project/app/[tabName]/environment/env-edit.tsx
Normal 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 >
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -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),
|
||||
@@ -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),
|
||||
52
src/app/project/app/[tabName]/page.tsx
Normal file
52
src/app/project/app/[tabName]/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
15
src/components/custom/page-title.tsx
Normal file
15
src/components/custom/page-title.tsx
Normal 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>
|
||||
}
|
||||
11
src/model/app-extended.model.ts
Normal file
11
src/model/app-extended.model.ts
Normal 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>;
|
||||
7
src/model/env-edit.model.ts
Normal file
7
src/model/env-edit.model.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const appEnvVariablesZodModel = z.object({
|
||||
envVars: z.string(),
|
||||
})
|
||||
|
||||
export type AppEnvVariablesModel = z.infer<typeof appEnvVariablesZodModel>;
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user