diff --git a/services/frontend/src/components/Modal/network/form-utils.ts b/services/frontend/src/components/Modal/network/form-utils.ts index fdaa24f..184056c 100644 --- a/services/frontend/src/components/Modal/network/form-utils.ts +++ b/services/frontend/src/components/Modal/network/form-utils.ts @@ -1,5 +1,11 @@ import * as yup from "yup"; -import { IEditNetworkForm, INetworkNodeItem } from "../../../types"; +import { + IEditNetworkForm, + IIPAM, + INetworkNodeItem, + IPAMConfig +} from "../../../types"; +import { pruneArray, pruneObject } from "../../../utils/forms"; export const validationSchema = yup.object({ entryName: yup @@ -12,10 +18,7 @@ export const validationSchema = yup.object({ .max(256, "Network name should be 256 characters or less") .required("Network name is required"), - driver: yup - .string() - .max(256, "Driver should be 256 characters or less") - .default("default"), + driver: yup.string().max(256, "Driver should be 256 characters or less"), configurations: yup.array( yup.object({ @@ -70,7 +73,7 @@ export const tabs = [ export const initialValues: IEditNetworkForm = { entryName: "", networkName: "", - driver: "default", + driver: "", configurations: [], options: [], labels: [] @@ -93,30 +96,29 @@ export const getInitialValues = (node?: INetworkNodeItem): IEditNetworkForm => { networkName: name, driver: ipam?.driver ?? "", configurations: - ipam?.config.map((item) => ({ - subnet: item.subnet, - ipRange: item.ip_range, - gateway: item.gateway, - auxAddresses: Object.entries(item.aux_addresses).map( + ipam?.config?.map((item) => ({ + subnet: item.subnet ?? "", + ipRange: item.ip_range ?? "", + gateway: item.gateway ?? "", + auxAddresses: Object.entries(item.aux_addresses ?? []).map( ([hostName, ipAddress]) => ({ hostName, ipAddress }) ) })) ?? [], - options: Object.keys(ipam?.options || {}).map((key) => { - if (!ipam) { - throw new Error("Control should not reach here."); - } + options: Object.keys(ipam?.options ?? {}).map((key) => { return { key, - value: ipam.options[key].toString() + value: ipam?.options?.[key].toString() ?? "" }; }), - labels: Object.entries(labels as any).map(([key, value]: any) => ({ - key, - value - })) + labels: labels + ? Object.entries(labels as any).map(([key, value]: any) => ({ + key, + value + })) + : [] }; }; @@ -129,34 +131,63 @@ export const getFinalValues = ( return { key: previous?.key ?? "network", type: "NETWORK", + position: { + left: 0, + top: 0 + }, inputs: previous?.inputs ?? [], outputs: previous?.outputs ?? [], - config: (previous as any)?.config ?? {}, canvasConfig: { node_name: values.entryName }, networkConfig: { name: values.networkName, - ipam: { - driver, - config: configurations.map((configuration) => ({ - subnet: configuration.subnet, - ip_range: configuration.ipRange, - gateway: configuration.gateway, - aux_addresses: Object.fromEntries( - configuration.auxAddresses.map((auxAddress) => [ - auxAddress.hostName, - auxAddress.ipAddress - ]) + ipam: pruneObject({ + driver: driver ? driver : undefined, + config: pruneArray( + configurations.map((configuration) => + pruneObject({ + subnet: configuration.subnet ? configuration.subnet : undefined, + ip_range: configuration.ipRange + ? configuration.ipRange + : undefined, + gateway: configuration.gateway + ? configuration.gateway + : undefined, + aux_addresses: (() => { + if (configuration.auxAddresses.length === 0) { + return undefined; + } + + /* We do not have to worry about empty `hostName` and `ipAddress` + * values because Yup would report such values as error. + */ + return Object.fromEntries( + configuration.auxAddresses.map((auxAddress) => [ + auxAddress.hostName, + auxAddress.ipAddress + ]) + ); + })() + }) ) - })), - options: Object.fromEntries( - options.map((option) => [option.key, option.value]) - ) - }, - labels: Object.fromEntries( - labels.map((label) => [label.key, label.value]) - ) + ) as IPAMConfig[], + options: (() => { + if (options.length === 0) { + return undefined; + } + + /* We do not have to worry about empty `key` and `value` + * values because Yup would report such values as error. + */ + return Object.fromEntries( + options.map((option) => [option.key, option.value]) + ); + })() + }) as IIPAM, + labels: pruneObject( + Object.fromEntries(labels.map((label) => [label.key, label.value])) + ) as Record } - } as any; + }; }; diff --git a/services/frontend/src/components/Modal/service/form-utils.ts b/services/frontend/src/components/Modal/service/form-utils.ts index 286b6a5..625b090 100644 --- a/services/frontend/src/components/Modal/service/form-utils.ts +++ b/services/frontend/src/components/Modal/service/form-utils.ts @@ -1,7 +1,6 @@ import type { IEditServiceForm, IServiceNodeItem } from "../../../types"; import * as yup from "yup"; -import lodash from "lodash"; -import { checkArray } from "../../../utils/forms"; +import { checkArray, pruneArray, pruneObject } from "../../../utils/forms"; const initialValues: IEditServiceForm = { imageName: "", @@ -95,10 +94,9 @@ export const getInitialValues = (node?: IServiceNodeItem): IEditServiceForm => { labels } = serviceConfig; - const environment0: string[] = checkArray(environment, "environment"); + const environment0: string[] = checkArray(environment || [], "environment"); const volumes0: string[] = checkArray(volumes, "volumes"); const ports0: string[] = checkArray(ports, "ports"); - const labels0: string[] = checkArray(labels, "labels"); const [imageName, imageTag] = (image ?? ":").split(":"); return { @@ -137,13 +135,12 @@ export const getInitialValues = (node?: IServiceNodeItem): IEditServiceForm => { return { hostPort, containerPort, protocol } as any; }), - labels: labels0.map((label) => { - const [key, value] = label.split("="); - return { - key, - value - }; - }) + labels: labels + ? Object.entries(labels as any).map(([key, value]: any) => ({ + key, + value + })) + : [] }; }; @@ -153,51 +150,44 @@ export const getFinalValues = ( ): IServiceNodeItem => { const { environmentVariables, ports, volumes, labels } = values; - return lodash.mergeWith( - lodash.cloneDeep(previous) || { - key: "service", - type: "SERVICE", - inputs: ["op_source"], - outputs: [], - config: {} + return { + key: previous?.key ?? "service", + type: "SERVICE", + position: previous?.position ?? { left: 0, top: 0 }, + inputs: previous?.inputs ?? ["op_source"], + outputs: previous?.outputs ?? [], + config: (previous as any)?.config ?? {}, + canvasConfig: { + node_name: values.serviceName }, - { - canvasConfig: { - node_name: values.serviceName - }, - serviceConfig: { - image: `${values.imageName}${ - values.imageTag ? `:${values.imageTag}` : "" - }`, - container_name: values.containerName, - environment: environmentVariables.map( + serviceConfig: { + image: `${values.imageName}${ + values.imageTag ? `:${values.imageTag}` : "" + }`, + container_name: values.containerName, + environment: pruneArray( + environmentVariables.map( (variable) => `${variable.key}${variable.value ? `=${variable.value}` : ""}` - ), - volumes: volumes.length - ? volumes.map( - (volume) => - volume.name + - (volume.containerPath ? `:${volume.containerPath}` : "") + - (volume.accessMode ? `:${volume.accessMode}` : "") - ) - : [], - ports: ports.map( - (port) => - port.hostPort + - (port.containerPort ? `:${port.containerPort}` : "") + - (port.protocol ? `/${port.protocol}` : "") - ), - labels: labels.map( - (label) => `${label.key}${label.value ? `=${label.value}` : ""}` ) - } - }, - (obj, src) => { - if (!lodash.isNil(src)) { - return src; - } - return obj; + ), + volumes: volumes.length + ? volumes.map( + (volume) => + volume.name + + (volume.containerPath ? `:${volume.containerPath}` : "") + + (volume.accessMode ? `:${volume.accessMode}` : "") + ) + : [], + ports: ports.map( + (port) => + port.hostPort + + (port.containerPort ? `:${port.containerPort}` : "") + + (port.protocol ? `/${port.protocol}` : "") + ), + labels: pruneObject( + Object.fromEntries(labels.map((label) => [label.key, label.value])) + ) } - ) as any; + } as any; }; diff --git a/services/frontend/src/components/Modal/volume/form-utils.ts b/services/frontend/src/components/Modal/volume/form-utils.ts index 8b986bc..b3da6d9 100644 --- a/services/frontend/src/components/Modal/volume/form-utils.ts +++ b/services/frontend/src/components/Modal/volume/form-utils.ts @@ -1,7 +1,6 @@ -import lodash from "lodash"; import * as yup from "yup"; import { IEditVolumeForm, IVolumeNodeItem } from "../../../types"; -import { checkArray } from "../../../utils/forms"; +import { pruneObject } from "../../../utils/forms"; export const validationSchema = yup.object({ entryName: yup @@ -36,19 +35,16 @@ export const getInitialValues = (node?: IVolumeNodeItem): IEditVolumeForm => { const { node_name = "" } = canvasConfig; const { name = "", labels } = volumeConfig; - const labels0: string[] = checkArray(labels, "labels"); - return { ...initialValues, entryName: node_name, volumeName: name, - labels: labels0.map((label) => { - const [key, value] = label.split("="); - return { - key, - value - }; - }) + labels: labels + ? Object.entries(labels as any).map(([key, value]: any) => ({ + key, + value + })) + : [] }; }; @@ -58,32 +54,22 @@ export const getFinalValues = ( ): IVolumeNodeItem => { const { labels } = values; - return lodash.mergeWith( - lodash.cloneDeep(previous) || { - key: "volume", - type: "VOLUME", - inputs: [], - outputs: [], - config: {} - }, - { - canvasConfig: { - node_name: values.entryName - }, - volumeConfig: { - name: values.volumeName, - labels: labels.map( - (label) => `${label.key}${label.value ? `=${label.value}` : ""}` - ) - } + return { + key: previous?.key ?? "volume", + type: "VOLUME", + position: previous?.position ?? { left: 0, top: 0 }, + inputs: previous?.inputs ?? [], + outputs: previous?.outputs ?? [], + canvasConfig: { + node_name: values.entryName }, - (obj, src) => { - if (!lodash.isNil(src)) { - return src; - } - return obj; + volumeConfig: { + name: values.volumeName, + labels: pruneObject( + Object.fromEntries(labels.map((label) => [label.key, label.value])) + ) as Record } - ) as any; + }; }; export const tabs = [ diff --git a/services/frontend/src/types/index.ts b/services/frontend/src/types/index.ts index 76db33c..e62eaae 100644 --- a/services/frontend/src/types/index.ts +++ b/services/frontend/src/types/index.ts @@ -78,31 +78,31 @@ export interface IVolumeTopLevel { device: string; }; external: boolean; - labels: string[] | KeyValuePair; + labels?: string[] | KeyValuePair; name: string; } +export interface IPAMConfig { + subnet?: string; + ip_range?: string; + gateway?: string; + aux_addresses?: Record; +} + +export interface IIPAM { + driver?: string; + config?: IPAMConfig[]; + options?: Record; +} + export interface INetworkTopLevel { driver: string; driver_opts: KeyValPair; attachable: boolean; enable_ipv6: boolean; - ipam: { - driver: string; - config: { - subnet: string; - ip_range: string; - gateway: string; - aux_addresses: { - host1: string; - host2: string; - host3: string; - }; - }[]; - options: KeyValPair; - }; + ipam?: IIPAM; internal: boolean; - labels: string[] | KeyValPair; + labels?: string[] | KeyValPair; external: boolean; name: string; } diff --git a/services/frontend/src/utils/forms.ts b/services/frontend/src/utils/forms.ts index 931a359..9704779 100644 --- a/services/frontend/src/utils/forms.ts +++ b/services/frontend/src/utils/forms.ts @@ -1,3 +1,5 @@ +import lodash from "lodash"; + export const checkArray = (array: any, name: string): T => { if (!Array.isArray(array)) { throw new Error( @@ -6,3 +8,19 @@ export const checkArray = (array: any, name: string): T => { } return array as unknown as T; }; + +export const pruneArray = (array: (T | undefined)[]): T[] | undefined => { + const result = array.filter(Boolean); + if (array.length === 0) { + return undefined; + } + return result as T[]; +}; + +export const pruneObject = (object: T): unknown | undefined => { + const result = lodash.pickBy(object ?? {}, (value) => value !== undefined); + if (Object.keys(result).length === 0) { + return undefined; + } + return result as unknown; +};