mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-11 05:59:23 -06:00
feat/add database credentials display in app tabs
This commit is contained in:
@@ -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} />
|
||||
|
||||
15
src/app/project/app/[appId]/credentials/actions.ts
Normal file
15
src/app/project/app/[appId]/credentials/actions.ts
Normal 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>>;
|
||||
82
src/app/project/app/[appId]/credentials/db-crendentials.tsx
Normal file
82
src/app/project/app/[appId]/credentials/db-crendentials.tsx
Normal 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>
|
||||
</>;
|
||||
}
|
||||
39
src/components/custom/copy-input-field.tsx
Normal file
39
src/components/custom/copy-input-field.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
18
src/frontend/utils/nextjs-actions.utils.ts
Normal file
18
src/frontend/utils/nextjs-actions.utils.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user