mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2025-12-31 16:30:10 -06:00
added delete dialog to certain pages
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -42,4 +42,5 @@ kube-config.config_clusteradmin
|
||||
kube-config.config_old
|
||||
kube-config.config_restricted
|
||||
internal
|
||||
dist
|
||||
dist
|
||||
storage/
|
||||
@@ -3,21 +3,28 @@ import { PathUtils } from "@/server/utils/path.utils";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import path from "path";
|
||||
import fs from 'fs/promises';
|
||||
import { getAuthUserSession } from "@/server/utils/action-wrapper.utils";
|
||||
import { ServiceException } from "@/shared/model/service.exception.model";
|
||||
|
||||
export const dynamic = 'force-dynamic' // defaults to auto
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
await getAuthUserSession();
|
||||
const requestUrl = new URL(request.url);
|
||||
const fileName = requestUrl.searchParams.get('fileName');
|
||||
if (!fileName) {
|
||||
throw new Error('No file name provided.');
|
||||
throw new ServiceException('No file name provided.');
|
||||
}
|
||||
|
||||
if (fileName.includes('..') || fileName.includes('/')) {
|
||||
throw new ServiceException('Invalid file name.');
|
||||
}
|
||||
|
||||
const dirOfTempDoanloadedData = PathUtils.tempVolumeDownloadPath;
|
||||
const tarPath = path.join(dirOfTempDoanloadedData, fileName);
|
||||
if (!await FsUtils.fileExists(tarPath)) {
|
||||
throw new Error(`File ${fileName} does not exist.`);
|
||||
throw new ServiceException(`File ${fileName} does not exist.`);
|
||||
}
|
||||
|
||||
const buffer = await fs.readFile(tarPath);
|
||||
@@ -25,7 +32,7 @@ export async function GET(request: NextRequest) {
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="data.tar.gz"`,
|
||||
'Content-Disposition': `attachment; filename="volume-data.tar.gz"`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -14,11 +14,26 @@ import { KubeObjectNameUtils } from "@/server/utils/kube-object-name.utils";
|
||||
import { Code } from "@/components/custom/code";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { OpenInNewWindowIcon, QuestionMarkCircledIcon } from "@radix-ui/react-icons";
|
||||
import { useConfirmDialog } from "@/frontend/states/zustand.states";
|
||||
|
||||
|
||||
export default function DomainsList({ app }: {
|
||||
app: AppExtendedModel
|
||||
}) {
|
||||
|
||||
const { openDialog } = useConfirmDialog();
|
||||
|
||||
const asyncDeleteDomain = async (domainId: string) => {
|
||||
const confirm = await openDialog({
|
||||
title: "Delete Domain",
|
||||
description: "The domain will be removed and the changes will take effect, after you deploy the app. Are you sure you want to remove this domain?",
|
||||
yesButton: "Delete Domain"
|
||||
});
|
||||
if (confirm) {
|
||||
await Toast.fromAction(() => deleteDomain(domainId));
|
||||
}
|
||||
};
|
||||
|
||||
return <>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -54,7 +69,7 @@ export default function DomainsList({ app }: {
|
||||
<DialogEditDialog appId={app.id} domain={domain}>
|
||||
<Button variant="ghost"><EditIcon /></Button>
|
||||
</DialogEditDialog>
|
||||
<Button variant="ghost" onClick={() => Toast.fromAction(() => deleteDomain(domain.id))}>
|
||||
<Button variant="ghost" onClick={() => asyncDeleteDomain(domain.id)}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</TableCell>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { z } from "zod";
|
||||
import { ServiceException } from "@/shared/model/service.exception.model";
|
||||
import pvcStatusService from "@/server/services/pvc.status.service";
|
||||
import pvcService from "@/server/services/pvc.service";
|
||||
import deploymentService from "@/server/services/deployment.service";
|
||||
|
||||
const actionAppVolumeEditZodModel = appVolumeEditZodModel.merge(z.object({
|
||||
appId: z.string(),
|
||||
@@ -44,5 +45,6 @@ export const getPvcUsage = async (pvcName: string, pvcNamespace: string) =>
|
||||
export const downloadPvcData = async (volumeId: string) =>
|
||||
simpleAction(async () => {
|
||||
await getAuthUserSession();
|
||||
return await pvcService.downloadPvcData(volumeId); // returns the download path on the server
|
||||
const fileNameOfDownloadedFile = await pvcService.downloadPvcData(volumeId);
|
||||
return new SuccessActionResult(fileNameOfDownloadedFile, 'Successfully zipped volume data'); // returns the download path on the server
|
||||
}) as Promise<ServerActionResult<any, string>>;
|
||||
|
||||
@@ -8,11 +8,41 @@ import { Download, EditIcon, TrashIcon } from "lucide-react";
|
||||
import DialogEditDialog from "./storage-edit-overlay";
|
||||
import { Toast } from "@/frontend/utils/toast.utils";
|
||||
import { deleteVolume, downloadPvcData } from "./actions";
|
||||
import { useConfirmDialog } from "@/frontend/states/zustand.states";
|
||||
|
||||
|
||||
export default function StorageList({ app }: {
|
||||
app: AppExtendedModel
|
||||
}) {
|
||||
|
||||
const { openDialog } = useConfirmDialog();
|
||||
|
||||
const asyncDeleteVolume = async (volumeId: string) => {
|
||||
const confirm = await openDialog({
|
||||
title: "Delete Volume",
|
||||
description: "The volume will be removed and the Data will be lost. The changes will take effect, after you deploy the app. Are you sure you want to remove this volume?",
|
||||
yesButton: "Delete Volume"
|
||||
});
|
||||
if (confirm) {
|
||||
await Toast.fromAction(() => deleteVolume(volumeId));
|
||||
}
|
||||
};
|
||||
|
||||
const asyncDownloadPvcData = async (volumeId: string) => {
|
||||
const confirm = await openDialog({
|
||||
title: "Download Volume Data",
|
||||
description: "The volume data will be zipped and downloaded. Depending on the size of the volume this can take a while. Are you sure you want to download the volume data?",
|
||||
yesButton: "Download"
|
||||
});
|
||||
if (confirm) {
|
||||
await Toast.fromAction(() => downloadPvcData(volumeId)).then(x => {
|
||||
if (x.status === 'success' && x.data) {
|
||||
window.open('/api/volume-data-download?fileName=' + x.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -37,17 +67,13 @@ export default function StorageList({ app }: {
|
||||
<TableCell className="font-medium">{volume.size}</TableCell>
|
||||
<TableCell className="font-medium">{volume.accessMode}</TableCell>
|
||||
<TableCell className="font-medium flex gap-2">
|
||||
<Button variant="ghost" onClick={() => asyncDownloadPvcData(volume.id)}>
|
||||
<Download />
|
||||
</Button>
|
||||
<DialogEditDialog appId={app.id} volume={volume}>
|
||||
<Button variant="ghost"><EditIcon /></Button>
|
||||
</DialogEditDialog>
|
||||
<Button variant="ghost" onClick={() => Toast.fromAction(() => downloadPvcData(volume.id)).then(x => {
|
||||
if (x.status === 'success' && x.data) {
|
||||
window.open('/api/volume-data-download?fileName=' + x.data);
|
||||
}
|
||||
})}>
|
||||
<Download />
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => Toast.fromAction(() => deleteVolume(volume.id))}>
|
||||
<Button variant="ghost" onClick={() => asyncDeleteVolume(volume.id)}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</TableCell>
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function AppTable({ data }: { data: App[] }) {
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => openDialog({
|
||||
title: "Delete App",
|
||||
description: "Are you sure you want to delete this app?",
|
||||
description: "Are you sure you want to delete this app? All data will be lost and this action cannot be undone.",
|
||||
}).then((result) => result ? Toast.fromAction(() => deleteApp(item.id)) : undefined)}>
|
||||
<span className="text-red-500">Delete App</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -10,11 +10,25 @@ import { MoreHorizontal } from "lucide-react";
|
||||
import { Toast } from "@/frontend/utils/toast.utils";
|
||||
import { Project } from "@prisma/client";
|
||||
import { deleteProject } from "./actions";
|
||||
import { useConfirmDialog } from "@/frontend/states/zustand.states";
|
||||
|
||||
|
||||
|
||||
export default function ProjectsTable({ data }: { data: Project[] }) {
|
||||
|
||||
const { openDialog } = useConfirmDialog();
|
||||
|
||||
const asyncDeleteProject = async (domainId: string) => {
|
||||
const confirm = await openDialog({
|
||||
title: "Delete Project",
|
||||
description: "Are you sure you want to delete this project? All data (apps, deployments, volumes, domains) will be lost and this action cannot be undone. Running apps will be stopped and removed.",
|
||||
yesButton: "Delete Project"
|
||||
});
|
||||
if (confirm) {
|
||||
await Toast.fromAction(() => deleteProject(domainId));
|
||||
}
|
||||
};
|
||||
|
||||
return <>
|
||||
<SimpleDataTable columns={[
|
||||
['id', 'ID', false],
|
||||
@@ -43,7 +57,7 @@ export default function ProjectsTable({ data }: { data: Project[] }) {
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => Toast.fromAction(() => deleteProject(item.id))}>
|
||||
<DropdownMenuItem onClick={() => asyncDeleteProject(item.id)}>
|
||||
<span className="text-red-500">Delete Project</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -6,8 +6,15 @@ import {
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card"
|
||||
import { Source_Code_Pro } from "next/font/google";
|
||||
import { cn } from "@/frontend/utils/utils";
|
||||
|
||||
|
||||
const sourceCodePro = Source_Code_Pro({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
export default function LogsStreamed({
|
||||
namespace,
|
||||
podName,
|
||||
@@ -87,7 +94,10 @@ export default function LogsStreamed({
|
||||
|
||||
return <>
|
||||
<div className="space-y-4">
|
||||
<Textarea ref={textAreaRef} value={logs} readOnly className={(fullHeight ? "h-[80vh]" : "h-[400px]") + " bg-slate-900 text-white"} />
|
||||
<Textarea ref={textAreaRef} value={logs} readOnly className={cn(
|
||||
(fullHeight ? "h-[80vh]" : "h-[400px]"),
|
||||
" bg-slate-900 text-white ",
|
||||
sourceCodePro.className)} />
|
||||
<div className="w-fit">
|
||||
<HoverCard>
|
||||
<HoverCardTrigger>
|
||||
|
||||
@@ -12,7 +12,7 @@ export class Toast {
|
||||
}
|
||||
return retVal;
|
||||
}, {
|
||||
loading: 'laden...',
|
||||
loading: 'loading...',
|
||||
success: (result: ServerActionResult<A, B>) => {
|
||||
resolve(result);
|
||||
return result.message ?? 'Operation successful';
|
||||
|
||||
@@ -30,7 +30,7 @@ class PvcService {
|
||||
|
||||
const pod = await podService.getPodsForApp(volume.app.projectId, volume.app.id);
|
||||
if (pod.length === 0) {
|
||||
throw new ServiceException(`No pod found for volume id ${volumeId} in app ${volume.app.id}`);
|
||||
throw new ServiceException(`There are no running pods for volume id ${volumeId} in app ${volume.app.id}. Make sure the app is running.`);
|
||||
}
|
||||
const firstPod = pod[0];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user