mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-11 05:59:23 -06:00
feat: add PhpMyAdmin as Database Tool
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import appService from "@/server/services/app.service";
|
||||
import dbGateService from "@/server/services/db-tool-services/dbgate.service";
|
||||
import phpMyAdminService from "@/server/services/db-tool-services/phpmyadmin.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";
|
||||
@@ -16,11 +17,18 @@ export const getDatabaseCredentials = async (appId: string) =>
|
||||
return new SuccessActionResult(credentials);
|
||||
}) as Promise<ServerActionResult<unknown, DatabaseTemplateInfoModel>>;
|
||||
|
||||
export const getIsDbGateActive = async (appId: string) =>
|
||||
export const getIsDbToolActive = async (appId: string, dbTool: 'dbgate' | 'phpmyadmin') =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
const isActive = await dbGateService.isDbToolRunning(appId);
|
||||
return new SuccessActionResult(isActive);
|
||||
if (dbTool === 'dbgate') {
|
||||
const isActive = await dbGateService.isDbToolRunning(appId);
|
||||
return new SuccessActionResult(isActive);
|
||||
} else if (dbTool === 'phpmyadmin') {
|
||||
const isActive = await phpMyAdminService.isDbToolRunning(appId);
|
||||
return new SuccessActionResult(isActive);
|
||||
} else {
|
||||
throw new ServiceException('Unknown db tool');
|
||||
}
|
||||
}) as Promise<ServerActionResult<unknown, boolean>>;
|
||||
|
||||
export const deployDbTool = async (appId: string, dbTool: 'dbgate' | 'phpmyadmin') =>
|
||||
@@ -29,6 +37,9 @@ export const deployDbTool = async (appId: string, dbTool: 'dbgate' | 'phpmyadmin
|
||||
if (dbTool === 'dbgate') {
|
||||
await dbGateService.deploy(appId);
|
||||
return new SuccessActionResult();
|
||||
} else if (dbTool === 'phpmyadmin') {
|
||||
await phpMyAdminService.deploy(appId);
|
||||
return new SuccessActionResult();
|
||||
} else {
|
||||
throw new ServiceException('Unknown db tool');
|
||||
}
|
||||
@@ -39,6 +50,8 @@ export const getLoginCredentialsForRunningDbTool = async (appId: string, dbTool:
|
||||
await getAuthUserSession();
|
||||
if (dbTool === 'dbgate') {
|
||||
return new SuccessActionResult(await dbGateService.getLoginCredentialsForRunningDbGate(appId));
|
||||
} else if (dbTool === 'phpmyadmin') {
|
||||
return new SuccessActionResult(await phpMyAdminService.getLoginCredentialsForRunningDbGate(appId));
|
||||
} else {
|
||||
throw new ServiceException('Unknown db tool');
|
||||
}
|
||||
@@ -50,6 +63,9 @@ export const deleteDbToolDeploymentForAppIfExists = async (appId: string, dbTool
|
||||
if (dbTool === 'dbgate') {
|
||||
await dbGateService.deleteToolForAppIfExists(appId);
|
||||
return new SuccessActionResult();
|
||||
} else if (dbTool === 'phpmyadmin') {
|
||||
await phpMyAdminService.deleteToolForAppIfExists(appId);
|
||||
return new SuccessActionResult();
|
||||
} else {
|
||||
throw new ServiceException('Unknown db tool');
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { useConfirmDialog } from "@/frontend/states/zustand.states";
|
||||
import { Toast } from "@/frontend/utils/toast.utils";
|
||||
import { Actions } from "@/frontend/utils/nextjs-actions.utils";
|
||||
import { deleteDbToolDeploymentForAppIfExists, deployDbTool, downloadDbGateFilesForApp, getIsDbGateActive, getLoginCredentialsForRunningDbTool } from "./actions";
|
||||
import { deleteDbToolDeploymentForAppIfExists, deployDbTool, downloadDbGateFilesForApp, getIsDbToolActive, getLoginCredentialsForRunningDbTool } from "./actions";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import FullLoadingSpinner from "@/components/ui/full-loading-spinnter";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
@@ -25,7 +25,7 @@ export default function DbGateDbTool({
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const loadIsDbGateActive = async (appId: string) => {
|
||||
const response = await Actions.run(() => getIsDbGateActive(appId));
|
||||
const response = await Actions.run(() => getIsDbToolActive(appId, 'dbgate'));
|
||||
setIsDbGateActive(response);
|
||||
}
|
||||
|
||||
@@ -80,9 +80,9 @@ export default function DbGateDbTool({
|
||||
}, [app]);
|
||||
|
||||
return <>
|
||||
{isDbGateActive === undefined ? <FullLoadingSpinner /> : <div className="flex gap-4 items-center">
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="canary-channel-mode" disabled={loading} checked={isDbGateActive} onCheckedChange={async (checked) => {
|
||||
<Switch id="canary-channel-mode" disabled={loading || isDbGateActive === undefined} checked={isDbGateActive} onCheckedChange={async (checked) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (checked) {
|
||||
@@ -99,7 +99,7 @@ export default function DbGateDbTool({
|
||||
</div>
|
||||
{isDbGateActive && <>
|
||||
<Button variant='outline' onClick={() => openDbGateAsync()}
|
||||
disabled={!isDbGateActive || loading}>Open DB Gate</Button>
|
||||
disabled={loading}>Open DB Gate</Button>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={300}>
|
||||
@@ -113,7 +113,8 @@ export default function DbGateDbTool({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</>}
|
||||
{loading && <LoadingSpinner></LoadingSpinner>}
|
||||
</div>}
|
||||
{(loading || isDbGateActive === undefined) && <LoadingSpinner></LoadingSpinner>}
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { AppExtendedModel } from "@/shared/model/app-extended.model";
|
||||
import DbGateDbTool from "./db-gate";
|
||||
import DbGateDbTool from "./db-gate-db-tool";
|
||||
import PhpMyAdminDbTool from "./phpmyadmin-db-tool";
|
||||
|
||||
export default function DbToolsCard({
|
||||
app
|
||||
@@ -14,8 +15,9 @@ export default function DbToolsCard({
|
||||
<CardTitle>Database Access</CardTitle>
|
||||
<CardDescription>Activate one of the following tools to access the database through your browser.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="space-y-4">
|
||||
<DbGateDbTool app={app} />
|
||||
{['MYSQL', 'MARIADB'].includes(app.appType) && <PhpMyAdminDbTool app={app} />}
|
||||
</CardContent>
|
||||
</Card >
|
||||
</>;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
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 { Actions } from "@/frontend/utils/nextjs-actions.utils";
|
||||
import { deleteDbToolDeploymentForAppIfExists, deployDbTool, getIsDbToolActive, getLoginCredentialsForRunningDbTool } from "./actions";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import FullLoadingSpinner from "@/components/ui/full-loading-spinnter";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Code } from "@/components/custom/code";
|
||||
import LoadingSpinner from "@/components/ui/loading-spinner";
|
||||
|
||||
export default function PhpMyAdminDbTool({
|
||||
app
|
||||
}: {
|
||||
app: AppExtendedModel;
|
||||
}) {
|
||||
|
||||
const { openConfirmDialog } = useConfirmDialog();
|
||||
const [isDbToolActive, setIsDbToolActive] = useState<boolean | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const loadIdDbToolActive = async (appId: string) => {
|
||||
const response = await Actions.run(() => getIsDbToolActive(appId, 'phpmyadmin'));
|
||||
setIsDbToolActive(response);
|
||||
}
|
||||
|
||||
const openDbTool = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const credentials = await Actions.run(() => getLoginCredentialsForRunningDbTool(app.id, 'phpmyadmin'));
|
||||
setLoading(false);
|
||||
await openConfirmDialog({
|
||||
title: "Open DB Tool",
|
||||
description: <>
|
||||
PHP My Admin is ready and can be opened in a new tab. <br />
|
||||
Use the following credentials to login:
|
||||
<div className="pt-3 grid grid-cols-1 gap-1">
|
||||
<Label>Username</Label>
|
||||
<div> <Code>{credentials.username}</Code></div>
|
||||
</div>
|
||||
<div className="pt-3 pb-4 grid grid-cols-1 gap-1">
|
||||
<Label>Password</Label>
|
||||
<div><Code>{credentials.password}</Code></div>
|
||||
</div>
|
||||
<div>
|
||||
<Button variant='outline' onClick={() => window.open(credentials.url, '_blank')}>Open PHP My Admin</Button>
|
||||
</div>
|
||||
</>,
|
||||
okButton: '',
|
||||
cancelButton: "Close"
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadIdDbToolActive(app.id);
|
||||
return () => {
|
||||
setIsDbToolActive(undefined);
|
||||
}
|
||||
}, [app]);
|
||||
|
||||
return <>
|
||||
<div className="flex gap-4 items-center">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch id="canary-channel-mode" disabled={loading || isDbToolActive === undefined} checked={isDbToolActive} onCheckedChange={async (checked) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (checked) {
|
||||
await Toast.fromAction(() => deployDbTool(app.id, 'phpmyadmin'), 'PHP My Admin is now activated', 'activating PHP My Admin...');
|
||||
} else {
|
||||
await Toast.fromAction(() => deleteDbToolDeploymentForAppIfExists(app.id, 'phpmyadmin'), 'PHP My Admin has been deactivated', 'Deactivating PHP My Admin...');
|
||||
}
|
||||
await loadIdDbToolActive(app.id);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}} />
|
||||
<Label htmlFor="airplane-mode">PHP My Admin</Label>
|
||||
</div>
|
||||
{isDbToolActive && <>
|
||||
<Button variant='outline' onClick={() => openDbTool()}
|
||||
disabled={!isDbToolActive || loading}>Open PHP My Admin</Button>
|
||||
</>}
|
||||
{(loading || isDbToolActive === undefined) && <LoadingSpinner></LoadingSpinner>}
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
@@ -38,26 +38,30 @@ export default function EnvEdit({ app }: {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Environment Variables</CardTitle>
|
||||
<CardDescription>Provide optional environment variables for your application.</CardDescription>
|
||||
<CardDescription>
|
||||
Provide optional environment variables for your application.
|
||||
{app.appType !== 'APP' && <div className="text-sm text-red-500 pt-2">You should not change ENV variables for databases.</div>}
|
||||
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
<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>
|
||||
|
||||
@@ -64,7 +64,7 @@ export class BaseDbToolService {
|
||||
return { url: `https://${traefikHostname}`, username, password };
|
||||
}
|
||||
|
||||
async deployToolForDatabase(appId: string, deplyomentBuilder: (app: AppExtendedModel) => V1Deployment) {
|
||||
async deployToolForDatabase(appId: string, appPort: number, deplyomentBuilder: (app: AppExtendedModel) => V1Deployment) {
|
||||
const app = await appService.getExtendedById(appId);
|
||||
const toolAppName = this.appIdToToolNameConverter(appId);
|
||||
|
||||
@@ -84,7 +84,7 @@ export class BaseDbToolService {
|
||||
await svcService.createOrUpdateService(namespace, toolAppName, [{
|
||||
name: 'http',
|
||||
port: 80,
|
||||
targetPort: 3000,
|
||||
targetPort: appPort,
|
||||
}]);
|
||||
|
||||
console.log(`Creating ingress for DB Tool ${toolAppName} for app ${appId}`);
|
||||
|
||||
@@ -56,7 +56,7 @@ class DbGateService extends BaseDbToolService {
|
||||
}
|
||||
|
||||
async deploy(appId: string) {
|
||||
await this.deployToolForDatabase(appId, (app) => {
|
||||
await this.deployToolForDatabase(appId, 3000, (app) => {
|
||||
const authPassword = randomBytes(15).toString('hex');
|
||||
const dbGateAppName = KubeObjectNameUtils.toDbGateId(app.id);
|
||||
const projectId = app.projectId;
|
||||
|
||||
85
src/server/services/db-tool-services/phpmyadmin.service.ts
Normal file
85
src/server/services/db-tool-services/phpmyadmin.service.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { ServiceException } from "@/shared/model/service.exception.model";
|
||||
import { KubeObjectNameUtils } from "../../utils/kube-object-name.utils";
|
||||
import { randomBytes } from "crypto";
|
||||
import { V1Deployment, V1EnvVar } from "@kubernetes/client-node";
|
||||
import { Constants } from "@/shared/utils/constants";
|
||||
import podService from "../pod.service";
|
||||
import { AppTemplateUtils } from "../../utils/app-template.utils";
|
||||
import appService from "../app.service";
|
||||
import { PathUtils } from "../../utils/path.utils";
|
||||
import { FsUtils } from "../../utils/fs.utils";
|
||||
import path from "path";
|
||||
import { BaseDbToolService } from "./base-db-tool.service";
|
||||
|
||||
class PhpMyAdminService extends BaseDbToolService {
|
||||
|
||||
constructor() {
|
||||
super((app) => KubeObjectNameUtils.toPhpMyAdminId(app));
|
||||
}
|
||||
|
||||
async getLoginCredentialsForRunningDbGate(appId: string) {
|
||||
return await this.getLoginCredentialsForRunningTool(appId, (_, app) => {
|
||||
const dbCredentials = AppTemplateUtils.getDatabaseModelFromApp(app);
|
||||
return { username: dbCredentials.username, password: dbCredentials.password };
|
||||
});
|
||||
}
|
||||
|
||||
async deploy(appId: string) {
|
||||
await this.deployToolForDatabase(appId, 80, (app) => {
|
||||
const appName = this.appIdToToolNameConverter(app.id);
|
||||
const projectId = app.projectId;
|
||||
|
||||
const dbCredentials = AppTemplateUtils.getDatabaseModelFromApp(app);
|
||||
|
||||
const body: V1Deployment = {
|
||||
metadata: {
|
||||
name: appName
|
||||
},
|
||||
spec: {
|
||||
replicas: 1,
|
||||
selector: {
|
||||
matchLabels: {
|
||||
app: appName
|
||||
}
|
||||
},
|
||||
template: {
|
||||
metadata: {
|
||||
labels: {
|
||||
app: appName
|
||||
},
|
||||
annotations: {
|
||||
[Constants.QS_ANNOTATION_APP_ID]: app.id,
|
||||
[Constants.QS_ANNOTATION_PROJECT_ID]: projectId,
|
||||
deploymentTimestamp: new Date().getTime() + "",
|
||||
"kubernetes.io/change-cause": `Deployment ${new Date().toISOString()}`
|
||||
}
|
||||
},
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: appName,
|
||||
image: 'phpmyadmin/phpmyadmin:latest',
|
||||
imagePullPolicy: 'Always',
|
||||
env: [
|
||||
{
|
||||
name: 'PMA_PORT',
|
||||
value: dbCredentials.port + ''
|
||||
},
|
||||
{
|
||||
name: 'PMA_HOST',
|
||||
value: dbCredentials.hostname
|
||||
},
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return body;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const phpMyAdminService = new PhpMyAdminService();
|
||||
export default phpMyAdminService;
|
||||
@@ -72,4 +72,8 @@ export class KubeObjectNameUtils {
|
||||
static toDbGateId(appId: string): `dbgate-${string}` {
|
||||
return `dbgate-${appId}`;
|
||||
}
|
||||
|
||||
static toPhpMyAdminId(appId: string): `phpma-${string}` {
|
||||
return `phpma-${appId}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user