Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ServerActionResult } from "@/shared/model/server-action-error-return.mo
import { AgentVolumeEditModel, agentVolumeEditZodModel } from "@/shared/model/volume-edit.model";
import { useDialogContext } from "@/frontend/states/dialog-context";
import { saveAgentVolume } from "./actions";
import { StorageClassUtils } from "@/shared/utils/storage-class.utils";

export default function AgentVolumeEditOverlay({
existingVolume,
Expand All @@ -26,7 +27,7 @@ export default function AgentVolumeEditOverlay({
storageClasses: string[];
}) {
const { closeDialog } = useDialogContext();
const defaultStorageClassName = existingVolume?.storageClassName || storageClasses[0] || '';
const defaultStorageClassName = existingVolume?.storageClassName || StorageClassUtils.getDefaultStorageClass(storageClasses);

const form = useForm<z.input<typeof agentVolumeEditZodModel>, unknown, z.output<typeof agentVolumeEditZodModel>>({
resolver: zodResolver(agentVolumeEditZodModel),
Expand Down
3 changes: 2 additions & 1 deletion src/app/project/app/[appId]/volumes/storage-edit-overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { AppExtendedModel } from "@/shared/model/app-extended.model"
import CheckboxFormField from "@/components/custom/checkbox-form-field"
import StorageClassCombobox from "@/components/custom/storage-class-combobox"
import { useDialog } from "@/frontend/states/zustand.states";
import { StorageClassUtils } from "@/shared/utils/storage-class.utils";

const accessModes = [
{ label: "ReadWriteOnce", value: "ReadWriteOnce" },
Expand All @@ -55,7 +56,7 @@ export default function StorageEditDialog({ volume, app, storageClasses }: {
}) {

const { closeDialog } = useDialog();
const defaultStorageClassName = volume?.storageClassName ?? storageClasses[0] ?? "";
const defaultStorageClassName = volume?.storageClassName ?? StorageClassUtils.getDefaultStorageClass(storageClasses);

const form = useForm<z.input<typeof appVolumeEditZodModel>, unknown, z.output<typeof appVolumeEditZodModel>>({
resolver: zodResolver(appVolumeEditZodModel),
Expand Down
12 changes: 7 additions & 5 deletions src/server/services/cluster.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { revalidateTag, unstable_cache } from "next/cache";
import longhornApiAdapter from "../adapter/longhorn-api.adapter";
import { KubeSizeConverter } from "../../shared/utils/kubernetes-size-converter.utils";
import { CatchUtils } from "@/shared/utils/catch.utils";
import { StorageClassUtils } from "@/shared/utils/storage-class.utils";

class ClusterService {

Expand Down Expand Up @@ -53,11 +54,12 @@ class ClusterService {
async getStorageClasses(): Promise<string[]> {
return await unstable_cache(async () => {
const storageClasses = await k3s.storage.listStorageClass();
return storageClasses.items
.map((storageClass) => storageClass.metadata?.name)
.filter((name): name is string => !!name)
.filter((name) => name !== 'longhorn-static') // filter out longhorn-static, because in longhorn setup only local path or longhorn should be used
.sort((a, b) => a.localeCompare(b));
return StorageClassUtils.sortStorageClasses(
storageClasses.items
.map((storageClass) => storageClass.metadata?.name)
.filter((name): name is string => !!name)
.filter((name) => name !== 'longhorn-static') // filter out longhorn-static, because in longhorn setup only local path or longhorn should be used
);
},
[Tags.storageClasses()], {
revalidate: 60,
Expand Down
22 changes: 22 additions & 0 deletions src/shared/utils/storage-class.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export class StorageClassUtils {
static readonly LONGHORN = 'longhorn';
static readonly LOCAL_PATH = 'local-path';

static sortStorageClasses(storageClassNames: string[]): string[] {
return storageClassNames.slice().sort((a, b) => {
const priorityA = a === StorageClassUtils.LONGHORN ? 0 : 1;
const priorityB = b === StorageClassUtils.LONGHORN ? 0 : 1;
if (priorityA !== priorityB) {
return priorityA - priorityB;
}
return a.localeCompare(b);
});
}

static getDefaultStorageClass(storageClassNames: string[]): string {
if (storageClassNames.includes(StorageClassUtils.LONGHORN)) {
return StorageClassUtils.LONGHORN;
}
return storageClassNames[0] ?? '';
}
}
41 changes: 41 additions & 0 deletions src/shared/utils/storage-class.utils.unit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { StorageClassUtils } from '@/shared/utils/storage-class.utils';

describe('StorageClassUtils', () => {

describe('sortStorageClasses', () => {
it('should place longhorn before local-path', () => {
const result = StorageClassUtils.sortStorageClasses(['local-path', 'longhorn']);
expect(result).toEqual(['longhorn', 'local-path']);
});

it('should sort the remaining storage classes alphabetically', () => {
const result = StorageClassUtils.sortStorageClasses(['zfs', 'nfs', 'local-path']);
expect(result).toEqual(['local-path', 'nfs', 'zfs']);
});

it('should keep longhorn first and sort all other entries alphabetically', () => {
const result = StorageClassUtils.sortStorageClasses(['zfs', 'local-path', 'nfs', 'longhorn']);
expect(result).toEqual(['longhorn', 'local-path', 'nfs', 'zfs']);
});

it('should not mutate the input array', () => {
const input = ['local-path', 'longhorn'];
StorageClassUtils.sortStorageClasses(input);
expect(input).toEqual(['local-path', 'longhorn']);
});
});

describe('getDefaultStorageClass', () => {
it('should return longhorn when available', () => {
expect(StorageClassUtils.getDefaultStorageClass(['local-path', 'longhorn'])).toBe('longhorn');
});

it('should fall back to the first entry when longhorn is not available', () => {
expect(StorageClassUtils.getDefaultStorageClass(['local-path', 'nfs'])).toBe('local-path');
});

it('should return an empty string when no storage classes are available', () => {
expect(StorageClassUtils.getDefaultStorageClass([])).toBe('');
});
});
});
Loading