diff --git a/src/app/monitoring/app-volumes-monitoring.tsx b/src/app/monitoring/app-volumes-monitoring.tsx index e628af5..bad7d9d 100644 --- a/src/app/monitoring/app-volumes-monitoring.tsx +++ b/src/app/monitoring/app-volumes-monitoring.tsx @@ -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) { diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx index ed7286f..e46dc0e 100644 --- a/src/app/monitoring/page.tsx +++ b/src/app/monitoring/page.tsx @@ -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 ( diff --git a/src/app/project/app/[appId]/volumes/shared-storage-edit-overlay.tsx b/src/app/project/app/[appId]/volumes/shared-storage-edit-overlay.tsx index ef51964..9f5d4da 100644 --- a/src/app/project/app/[appId]/volumes/shared-storage-edit-overlay.tsx +++ b/src/app/project/app/[appId]/volumes/shared-storage-edit-overlay.tsx @@ -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); }); diff --git a/src/app/project/app/[appId]/volumes/storage-edit-overlay.tsx b/src/app/project/app/[appId]/volumes/storage-edit-overlay.tsx index adc07ab..db83818 100644 --- a/src/app/project/app/[appId]/volumes/storage-edit-overlay.tsx +++ b/src/app/project/app/[appId]/volumes/storage-edit-overlay.tsx @@ -108,6 +108,8 @@ export default function StorageEditDialog({ children, volume, app, nodesInfo }: }); }, [volume]); + const values = form.watch(); + return ( <>
setIsOpen(true)}> @@ -154,6 +156,12 @@ export default function StorageEditDialog({ children, volume, app, nodesInfo }: )} /> + {volume && volume.size !== values.size && volume.shareWithOtherApps && <> +

+ When changing the size of a shared volume, ensure that all apps using this volume are shut down before deploying the changes. +

+ } + 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 }: { - -

Delete volume

+

{volume.sharedVolumeId ? 'Detach Volume' : 'Delete Volume'}

diff --git a/src/server/services/app.service.ts b/src/server/services/app.service.ts index fb2e351..008b956 100644 --- a/src/server/services/app.service.ts +++ b/src/server/services/app.service.ts @@ -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)); + } } } diff --git a/src/server/services/monitoring.service.ts b/src/server/services/monitoring.service.ts index b2bf4da..4c1588d 100644 --- a/src/server/services/monitoring.service.ts +++ b/src/server/services/monitoring.service.ts @@ -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 }); } diff --git a/src/server/services/pvc.service.ts b/src/server/services/pvc.service.ts index a80bf62..1af4711 100644 --- a/src/server/services/pvc.service.ts +++ b/src/server/services/pvc.service.ts @@ -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); diff --git a/src/shared/model/app-volume-monitoring-usage.model.ts b/src/shared/model/app-volume-monitoring-usage.model.ts index 238aa1b..418036d 100644 --- a/src/shared/model/app-volume-monitoring-usage.model.ts +++ b/src/shared/model/app-volume-monitoring-usage.model.ts @@ -5,5 +5,6 @@ export interface AppVolumeMonitoringUsageModel { appId: string, mountPath: string, usedBytes: number, - capacityBytes: number + capacityBytes: number, + isBaseVolume: boolean }