fix: monitoring support for shared volumes and cache management for volumes in app detail

This commit is contained in:
biersoeckli
2026-01-31 15:14:51 +00:00
parent bcd9e8a7b2
commit 05b1f1ee2a
9 changed files with 46 additions and 23 deletions

View File

@@ -47,7 +47,8 @@ export default function AppVolumeMonitoring({
const fetchVolumeMonitoringUsage = async () => {
try {
const data = await Actions.run(() => getVolumeMonitoringUsage());
let data = await Actions.run(() => getVolumeMonitoringUsage());
data = data?.filter((volume) => !!volume.isBaseVolume);
setUpdatedVolumeUsage(convertToExtendedModel(data));
setUsedAndCapacityBytes(convertToExtendedModel(data));
} catch (ex) {

View File

@@ -30,6 +30,8 @@ export default async function ResourceNodesInfoPage() {
// filter by role
volumesUsage = volumesUsage?.filter((volume) => UserGroupUtils.sessionHasReadAccessForApp(session, volume.appId));
// only base volumes, no shared volumes
volumesUsage = volumesUsage?.filter((volume) => !!volume.isBaseVolume);
updatedNodeRessources = updatedNodeRessources?.filter((app) => UserGroupUtils.sessionHasReadAccessForApp(session, app.appId));
return (

View File

@@ -67,9 +67,13 @@ export default function SharedStorageEditDialog({ children, app }: {
setIsLoadingVolumes(true);
getShareableVolumes(app.id).then(result => {
if (result.status === 'success' && result.data) {
setShareableVolumes(result.data);
const alreadyAddedSharedVolumes = app.appVolumes
.filter(v => !!v.sharedVolumeId)
.map(v => v.sharedVolumeId);
setShareableVolumes(result.data.filter(v => !alreadyAddedSharedVolumes.includes(v.id)));
} else {
setShareableVolumes([]);
toast.error('An error occurred while fetching shareable volumes');
}
setIsLoadingVolumes(false);
});

View File

@@ -108,6 +108,8 @@ export default function StorageEditDialog({ children, volume, app, nodesInfo }:
});
}, [volume]);
const values = form.watch();
return (
<>
<div onClick={() => setIsOpen(true)}>
@@ -154,6 +156,12 @@ export default function StorageEditDialog({ children, volume, app, nodesInfo }:
)}
/>
{volume && volume.size !== values.size && volume.shareWithOtherApps && <>
<p className="text-sm text-yellow-600">
When changing the size of a shared volume, ensure that all apps using this volume are shut down before deploying the changes.
</p>
</>}
<FormField
control={form.control}
name="accessMode"

View File

@@ -4,7 +4,7 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import { AppExtendedModel } from "@/shared/model/app-extended.model";
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Download, EditIcon, Folder, TrashIcon, Share2 } from "lucide-react";
import { Download, EditIcon, Folder, TrashIcon, Share2, Unlink2, Unlink } from "lucide-react";
import DialogEditDialog from "./storage-edit-overlay";
import SharedStorageEditDialog from "./shared-storage-edit-overlay";
import { Toast } from "@/frontend/utils/toast.utils";
@@ -47,7 +47,7 @@ export default function StorageList({ app, readonly, nodesInfo }: {
if (response.status === 'success' && response.data) {
const mappedVolumeData = [...app.appVolumes] as AppVolumeWithCapacity[];
for (let item of mappedVolumeData) {
const volume = response.data.find(x => x.pvcName === KubeObjectNameUtils.toPvcName(item.id));
const volume = response.data.find(x => x.pvcName === KubeObjectNameUtils.toPvcName(item.sharedVolumeId || item.id));
if (volume) {
item.usedBytes = volume.usedBytes;
item.capacityBytes = KubeSizeConverter.fromMegabytesToBytes(item.size);
@@ -62,16 +62,17 @@ export default function StorageList({ app, readonly, nodesInfo }: {
React.useEffect(() => {
loadAndMapStorageData();
}, [app.appVolumes]);
}, [app.appVolumes, app]);
const { openConfirmDialog: openDialog } = useConfirmDialog();
const asyncDeleteVolume = async (volumeId: string) => {
const asyncDeleteVolume = async (volumeId: string, isBaseVolume: boolean) => {
try {
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?",
okButton: "Delete Volume"
title: isBaseVolume ? "Delete Volume" : "Detach Volume",
description: isBaseVolume ? "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?" :
"The volume will be detached from the app. The data will remain on the cluster and can be re-attached later. The changes will take effect, after you deploy the app. Are you sure you want to detach this volume?",
okButton: isBaseVolume ? "Delete Volume" : "Detach Volume"
});
if (confirm) {
setIsLoading(true);
@@ -281,12 +282,12 @@ export default function StorageList({ app, readonly, nodesInfo }: {
<TooltipProvider>
<Tooltip delayDuration={200}>
<TooltipTrigger>
<Button variant="ghost" onClick={() => asyncDeleteVolume(volume.id)} disabled={isLoading}>
<TrashIcon />
<Button variant="ghost" onClick={() => asyncDeleteVolume(volume.id, !volume.sharedVolumeId)} disabled={isLoading}>
{volume.sharedVolumeId ? <Unlink /> : <TrashIcon />}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Delete volume</p>
<p>{volume.sharedVolumeId ? 'Detach Volume' : 'Delete Volume'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -298,7 +298,8 @@ class AppService {
}
});
if (existingAppWithSameVolumeMountPath.filter(x => x.id !== volumeToBeSaved.id)
if (existingAppWithSameVolumeMountPath
.filter(x => x.id !== volumeToBeSaved.id)
.some(x => x.containerMountPath === volumeToBeSaved.containerMountPath)) {
throw new ServiceException("Mount Path is already configured within the same app.");
}
@@ -329,12 +330,16 @@ class AppService {
where: {
id
}, include: {
app: true
app: true,
sharedVolumes: true
}
});
if (!existingVolume) {
return;
}
// get ids of all apps that use this volume as shared volume --> to reset cache
let additionalAppIds = existingVolume.sharedVolumes.map(v => v.appId);
try {
await dataAccess.client.appVolume.delete({
where: {
@@ -344,6 +349,9 @@ class AppService {
} finally {
revalidateTag(Tags.app(existingVolume.appId));
revalidateTag(Tags.apps(existingVolume.app.projectId));
for (const appId of additionalAppIds) {
revalidateTag(Tags.app(appId));
}
}
}

View File

@@ -9,7 +9,6 @@ import longhornApiAdapter from "../adapter/longhorn-api.adapter";
import dataAccess from "../adapter/db.client";
import pvcService from "./pvc.service";
import { KubeObjectNameUtils } from "../utils/kube-object-name.utils";
import appService from "./app.service";
import projectService from "./project.service";
import { AppMonitoringUsageModel } from "@/shared/model/app-monitoring-usage.model";
@@ -58,6 +57,7 @@ class MonitorService {
mountPath: appVolume.containerMountPath,
usedBytes: longhornVolume.actualSizeBytes,
capacityBytes: KubeSizeConverter.fromMegabytesToBytes(baseVolume?.size ?? appVolume.size),
isBaseVolume: !sharedVolumeId
});
}

View File

@@ -100,13 +100,11 @@ class PvcService {
}
async createPvcForVolumeIfNotExists(projectId: string, app: AppVolumeWithSharing) {
const baseVolume = app.sharedVolumeId
? await dataAccess.client.appVolume.findFirstOrThrow({
where: {
id: app.sharedVolumeId
}
})
: app;
const baseVolume = app.sharedVolumeId ? await dataAccess.client.appVolume.findFirstOrThrow({
where: {
id: app.sharedVolumeId
}
}) : app;
const pvcName = KubeObjectNameUtils.toPvcName(baseVolume.id);
const existingPvc = await this.getExistingPvcByVolumeId(projectId, baseVolume.id);

View File

@@ -5,5 +5,6 @@ export interface AppVolumeMonitoringUsageModel {
appId: string,
mountPath: string,
usedBytes: number,
capacityBytes: number
capacityBytes: number,
isBaseVolume: boolean
}