feat/add database credentials display in app tabs

This commit is contained in:
biersoeckli
2024-12-29 16:02:12 +00:00
parent b24c92bff1
commit 2c2e638c08
7 changed files with 164 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ import { useEffect } from "react";
import { useBreadcrumbs } from "@/frontend/states/zustand.states";
import FileMount from "./volumes/file-mount";
import WebhookDeploymentInfo from "./overview/webhook-deployment";
import DbCredentials from "./credentials/db-crendentials";
export default function AppTabs({
app,
@@ -37,6 +38,7 @@ export default function AppTabs({
<Tabs defaultValue="general" value={tabName} onValueChange={(newTab) => openTab(newTab)} className="space-y-4">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
{app.appType !== 'APP' && <TabsTrigger value="credentials">Credentials</TabsTrigger>}
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
@@ -48,6 +50,9 @@ export default function AppTabs({
<BuildsTab app={app} />
<WebhookDeploymentInfo app={app} />
</TabsContent>
{app.appType !== 'APP' && <TabsContent value="credentials" className="space-y-4">
<DbCredentials app={app} />
</TabsContent>}
<TabsContent value="general" className="space-y-4">
<GeneralAppSource app={app} />
<GeneralAppRateLimits app={app} />

View File

@@ -0,0 +1,15 @@
'use server'
import appService from "@/server/services/app.service";
import { getAuthUserSession, simpleAction } from "@/server/utils/action-wrapper.utils";
import { AppTemplateUtils } from "@/server/utils/app-template.utils";
import { DatabaseTemplateInfoModel } from "@/shared/model/database-template-info.model";
import { ServerActionResult, SuccessActionResult } from "@/shared/model/server-action-error-return.model";
export const getDatabaseCredentials = async (appId: string) =>
simpleAction(async () => {
await getAuthUserSession();
const app = await appService.getExtendedById(appId);
const credentials = AppTemplateUtils.getDatabaseModelFromApp(app);
return new SuccessActionResult(credentials);
}) as Promise<ServerActionResult<unknown, DatabaseTemplateInfoModel>>;

View File

@@ -0,0 +1,82 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { AppExtendedModel } from "@/shared/model/app-extended.model";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { useConfirmDialog } from "@/frontend/states/zustand.states";
import { Toast } from "@/frontend/utils/toast.utils";
import { ClipboardCopy } from "lucide-react";
import { toast } from "sonner";
import { DatabaseTemplateInfoModel } from "@/shared/model/database-template-info.model";
import { Actions } from "@/frontend/utils/nextjs-actions.utils";
import { getDatabaseCredentials } from "./actions";
import { Label } from "@/components/ui/label";
import CopyInputField from "@/components/custom/copy-input-field";
import FullLoadingSpinner from "@/components/ui/full-loading-spinnter";
export default function DbCredentials({
app
}: {
app: AppExtendedModel;
}) {
const [databaseCredentials, setDatabaseCredentials] = useState<DatabaseTemplateInfoModel | undefined>(undefined);
const loadCredentials = async (appId: string) => {
const response = await Actions.run(() => getDatabaseCredentials(appId));
setDatabaseCredentials(response);
}
useEffect(() => {
loadCredentials(app.id);
return () => {
setDatabaseCredentials(undefined);
}
}, [app]);
if (!databaseCredentials) {
return <FullLoadingSpinner />;
}
return <>
<Card>
<CardHeader>
<CardTitle>Database Credentials</CardTitle>
<CardDescription>Use these credentials to connect to your database from other apps within the same project.</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<CopyInputField
label="Database Name"
value={databaseCredentials?.databaseName || ''} />
<div></div>
<CopyInputField
label="Username"
value={databaseCredentials?.username || ''} />
<CopyInputField
label="Password"
secret={true}
value={databaseCredentials?.password || ''} />
<CopyInputField
label="Internal Hostname"
value={databaseCredentials?.hostname || ''} />
<CopyInputField
label="Internal Port"
value={(databaseCredentials?.port + '')} />
</div>
<div className="grid grid-cols-1 gap-4 pt-4">
<CopyInputField
label="Internal Connection URL"
secret={true}
value={databaseCredentials?.internalConnectionUrl || ''} />
</div>
</CardContent>
</Card>
</>;
}

View File

@@ -0,0 +1,39 @@
import { toast } from "sonner";
import { Button } from "../ui/button";
import { Label } from "../ui/label";
import { ClipboardCopy } from "lucide-react";
import { Input } from "../ui/input";
export default function CopyInputField({
label,
value,
secret = false
}: {
label: string;
value: string;
secret?: boolean;
}) {
const copyValue = (value: string, description?: string) => {
navigator.clipboard.writeText(value);
toast.success(description || 'Copied to clipboard.');
}
return (<>
<div className="">
<Label>{label}</Label>
<div className="flex items-center space-x-2 pt-2">
<Input
value={secret ? '***************' : value}
className="bg-slate-100 cursor-pointer"
readOnly
onClick={() => copyValue(value)} />
<Button onClick={() => copyValue(value)} variant="outline">
<ClipboardCopy />
</Button>
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,18 @@
import { ServerActionResult } from "@/shared/model/server-action-error-return.model";
import { toast } from "sonner";
export class Actions {
static async run<TReturnData>(action: () => Promise<ServerActionResult<unknown, TReturnData>>) {
try {
const retVal = await action();
if (!retVal || (retVal as ServerActionResult<unknown, TReturnData>).status !== 'success') {
toast.error(retVal?.message ?? 'An unknown error occurred.');
throw new Error(retVal?.message ?? 'An unknown error occurred.');
}
return retVal.data!;
} catch (error) {
toast.error('An unknown error occurred.');
throw error;
}
}
}

View File

@@ -69,6 +69,7 @@ export class AppTemplateUtils {
password: envVars.find(x => x.name === 'MONGO_INITDB_ROOT_PASSWORD')?.value!,
port,
hostname,
internalConnectionUrl: `mongodb://${hostname}:${port}/${envVars.find(x => x.name === 'MONGO_INITDB_DATABASE')?.value!}`,
};
} else if (app.appType === 'MYSQL') {
returnVal = {
@@ -77,6 +78,7 @@ export class AppTemplateUtils {
password: envVars.find(x => x.name === 'MYSQL_PASSWORD')?.value!,
port,
hostname,
internalConnectionUrl: `mysql://${envVars.find(x => x.name === 'MYSQL_USER')?.value!}:${envVars.find(x => x.name === 'MYSQL_PASSWORD')?.value!}@${hostname}:${port}/${envVars.find(x => x.name === 'MYSQL_DATABASE')?.value!}`,
};
} else if (app.appType === 'POSTGRES') {
returnVal = {
@@ -85,6 +87,7 @@ export class AppTemplateUtils {
password: envVars.find(x => x.name === 'POSTGRES_PASSWORD')?.value!,
port,
hostname,
internalConnectionUrl: `postgresql://${envVars.find(x => x.name === 'POSTGRES_USER')?.value!}:${envVars.find(x => x.name === 'POSTGRES_PASSWORD')?.value!}@${hostname}:${port}/${envVars.find(x => x.name === 'POSTGRES_DB')?.value!}`,
};
} else if (app.appType === 'MARIADB') {
returnVal = {
@@ -93,6 +96,7 @@ export class AppTemplateUtils {
password: envVars.find(x => x.name === 'MYSQL_PASSWORD')?.value!,
port,
hostname,
internalConnectionUrl: `mariadb://${envVars.find(x => x.name === 'MYSQL_USER')?.value!}:${envVars.find(x => x.name === 'MYSQL_PASSWORD')?.value!}@${hostname}:${port}/${envVars.find(x => x.name === 'MYSQL_DATABASE')?.value!}`,
};
} else {
throw new ServiceException('Unknown database type, could not load database information.');

View File

@@ -9,6 +9,7 @@ export const databaseTemplateInfoZodModel = z.object({
port: z.number(),
hostname: z.string(),
databaseName: z.string(),
internalConnectionUrl: z.string(),
});
export type DatabaseTemplateInfoModel = z.infer<typeof databaseTemplateInfoZodModel>;