diff --git a/services/frontend/src/components/Project/index.tsx b/services/frontend/src/components/Project/index.tsx index 01328d2..37e3130 100644 --- a/services/frontend/src/components/Project/index.tsx +++ b/services/frontend/src/components/Project/index.tsx @@ -8,7 +8,8 @@ import { IServiceNodeItem, IVolumeNodeItem, IServiceNodePosition, - IProject + IProject, + IEditServiceFormDependsOn } from "../../types"; import eventBus from "../../events/eventBus"; import { useMutation } from "react-query"; @@ -40,6 +41,7 @@ import EditVolumeModal from "../modals/docker-compose/volume/EditVolumeModal"; import CodeEditor from "../CodeEditor"; import { useTitle } from "../../hooks"; import VisibilitySwitch from "../global/VisibilitySwitch"; +import _ from "lodash"; interface IProjectProps { isAuthenticated: boolean; @@ -248,6 +250,22 @@ export default function Project(props: IProjectProps) { setNetworks({ ..._networks }); }; + const getDependsOnKeys = (dependsOn: any): string[] => { + let dependsOnKeys: string[] = []; + + if (dependsOn) { + if (dependsOn.constructor === Object) { + dependsOnKeys = Object.keys(dependsOn); + } + + if (Array.isArray(dependsOn)) { + dependsOnKeys = dependsOn as []; + } + } + + return dependsOnKeys; + }; + const onUpdateEndpoint = (nodeItem: IServiceNodeItem) => { const key = nodeItem.key; @@ -266,23 +284,21 @@ export default function Project(props: IProjectProps) { }); } - if ( - nodeItem.serviceConfig?.depends_on && - Array.isArray(nodeItem.serviceConfig.depends_on) - ) { - nodeItem.serviceConfig.depends_on.forEach((dep: string) => { - const depObject = Object.keys(nodes).find((key: string) => { - const node = nodes[key]; - if (node.canvasConfig.node_name === dep) { - return node; - } - }); + const dependsOnData = nodeItem.serviceConfig.depends_on; + const dependsOnKeys = getDependsOnKeys(dependsOnData); - if (depObject) { - onConnectionAttached([key, depObject]); + dependsOnKeys.forEach((dep: string) => { + const depObject = Object.keys(nodes).find((key: string) => { + const node = nodes[key]; + if (node.canvasConfig.node_name === dep) { + return node; } }); - } + + if (depObject) { + onConnectionAttached([key, depObject]); + } + }); setNodes({ ...nodes, [nodeItem.key]: nodeItem }); }; @@ -301,19 +317,25 @@ export default function Project(props: IProjectProps) { } as IServiceNodeItem; const targetNode = stateNodesRef.current[data[1]]; const targetServiceName = targetNode.canvasConfig.node_name; - const sourceDependsOn = sourceNode.serviceConfig.depends_on as string[]; + const sourceDependsOn = sourceNode.serviceConfig.depends_on as any; - if (sourceDependsOn && sourceDependsOn.length) { - if (targetServiceName) { - const filtered = sourceDependsOn.filter( - (nodeName: string) => nodeName !== targetServiceName - ); - - if (filtered.length) { - sourceNode.serviceConfig.depends_on = filtered; - } else { - delete sourceNode.serviceConfig.depends_on; + if (targetServiceName) { + const dependsOnKeys = getDependsOnKeys(sourceDependsOn); + + dependsOnKeys.forEach((key: string) => { + if (key === targetServiceName) { + if (Array.isArray(sourceDependsOn)) { + _.remove(sourceDependsOn, (key) => key === targetServiceName); + } + + if (sourceDependsOn && sourceDependsOn.constructor === Object) { + delete sourceDependsOn[key]; + } } + }); + + if (!getDependsOnKeys(sourceDependsOn).length) { + delete sourceNode.serviceConfig.depends_on; } } } @@ -337,12 +359,21 @@ export default function Project(props: IProjectProps) { } as IServiceNodeItem; const targetNode = stateNodesRef.current[data[1]]; const targetServiceName = targetNode.canvasConfig.node_name; - let sourceDependsOn = sourceNode.serviceConfig.depends_on as string[]; + let sourceDependsOn = sourceNode.serviceConfig.depends_on as any; + const dependsOnKeys = getDependsOnKeys(sourceDependsOn); - if (sourceDependsOn && sourceDependsOn.length) { + if (sourceDependsOn) { if (targetServiceName) { - if (!sourceDependsOn.includes(targetServiceName)) { - sourceDependsOn.push(targetServiceName); + if (!dependsOnKeys.includes(targetServiceName)) { + if (Array.isArray(sourceDependsOn)) { + sourceDependsOn.push(targetServiceName); + } + + if (sourceDependsOn.constructor === Object) { + sourceDependsOn[targetServiceName] = { + condition: "service_healthy" + }; + } } } } else { diff --git a/services/frontend/src/components/modals/docker-compose/service/General.tsx b/services/frontend/src/components/modals/docker-compose/service/General.tsx index 4ef0381..e41fafd 100644 --- a/services/frontend/src/components/modals/docker-compose/service/General.tsx +++ b/services/frontend/src/components/modals/docker-compose/service/General.tsx @@ -222,14 +222,38 @@ const General = () => { title: "Depends on", fields: (index: number): TFinalFormField[] => [ { - id: `dependsOn[${index}]`, + id: `dependsOn[${index}].serviceName`, type: "text", - name: `dependsOn[${index}]`, + name: `dependsOn[${index}].serviceName`, placeholder: "Service name", - required: false + required: true + }, + { + id: `dependsOn[${index}].condition`, + type: "toggle", + name: `dependsOn[${index}].condition`, + label: "Condition", + required: true, + options: [ + { + value: "service_started", + text: "Service started" + }, + { + value: "service_healthy", + text: "Service healthy" + }, + { + value: "service_completed_successfully", + text: "Service completed successfully" + } + ] } ], - newValue: "" + newValue: { + serviceName: "", + condition: "service_started" + } }, { id: "networks", diff --git a/services/frontend/src/components/modals/docker-compose/service/form-utils.ts b/services/frontend/src/components/modals/docker-compose/service/form-utils.ts index 397152d..a0b8837 100644 --- a/services/frontend/src/components/modals/docker-compose/service/form-utils.ts +++ b/services/frontend/src/components/modals/docker-compose/service/form-utils.ts @@ -1,4 +1,8 @@ -import type { IEditServiceForm, IServiceNodeItem } from "../../../../types"; +import type { + IEditServiceForm, + IEditServiceFormDependsOn, + IServiceNodeItem +} from "../../../../types"; import * as yup from "yup"; import { checkArray, @@ -278,9 +282,43 @@ export const validationSchema = yup.object({ key: yup.string().required("Key is required"), value: yup.string() }) - ) + ), + dependsOn: yup.array( + yup.object({ + serviceName: yup.string().required("Service name is required"), + condition: yup + .string() + .oneOf( + [ + "service_started", + "service_healthy", + "service_completed_successfully" + ], + "Condition should be one of: service_started, service_healthy, or service_completed_successfully." + ) + }) + ), + workingDir: yup.string() }); +const extractDependsOn = (object: string[] | any) => { + if (!object) { + return object; + } + + if (Array.isArray(object)) { + return object.map((item) => ({ + serviceName: item, + condition: "service_started" + })); + } + + return Object.keys(object).map((key) => ({ + serviceName: key, + condition: object[key].condition + })); +}; + export const getInitialValues = (node?: IServiceNodeItem): IEditServiceForm => { if (!node) { return { @@ -456,8 +494,7 @@ export const getInitialValues = (node?: IServiceNodeItem): IEditServiceForm => { initialValues.deploy.labels } : initialValues.deploy, - dependsOn: - (depends_on as string[]) ?? (initialValues.dependsOn as string[]), + dependsOn: extractDependsOn(depends_on) ?? initialValues.dependsOn, entrypoint: (entrypoint as string) ?? (initialValues.entrypoint as string), envFile: (env_file as string) ?? (initialValues.envFile as string), imageName, @@ -500,6 +537,31 @@ export const getInitialValues = (node?: IServiceNodeItem): IEditServiceForm => { }; }; +const getFinalDependsOn = (dependsOn: IEditServiceFormDependsOn[]) => { + let shortForm = true; + for (const item of dependsOn) { + if (item.condition !== "service_started") { + shortForm = false; + break; + } + } + + if (shortForm) { + return pruneArray(dependsOn.map((item) => item.serviceName)); + } + + return pruneObject( + Object.fromEntries( + dependsOn.map((item) => [ + item.serviceName, + { + condition: item.condition + } + ]) + ) + ); +}; + export const getFinalValues = ( values: IEditServiceForm, previous?: IServiceNodeItem @@ -606,7 +668,7 @@ export const getFinalValues = ( }), labels: packArrayAsObject(deploy.labels, "key", "value") }), - depends_on: pruneArray(dependsOn), + depends_on: getFinalDependsOn(dependsOn), entrypoint: pruneString(entrypoint), env_file: pruneString(envFile), image: pruneString( diff --git a/services/frontend/src/types/index.ts b/services/frontend/src/types/index.ts index 1bf9585..eadd3c5 100644 --- a/services/frontend/src/types/index.ts +++ b/services/frontend/src/types/index.ts @@ -149,11 +149,15 @@ export interface IService { credential_spec: KeyValPair; depends_on: | string[] - | { - [key: string]: { - condition: string; - }; - }; + | Record< + string, + { + condition: + | "service_started" + | "service_healthy" + | "service_completed_successfully"; + } + >; deploy?: { endpoint_mode?: "vip" | "dnsrr"; labels?: string[] | Record; @@ -480,10 +484,18 @@ export interface IEditServiceForm { key: string; value: string; }[]; - dependsOn: string[]; + dependsOn: IEditServiceFormDependsOn[]; workingDir: string; } +export interface IEditServiceFormDependsOn { + serviceName: string; + condition: + | "service_started" + | "service_healthy" + | "service_completed_successfully"; +} + export interface IEditVolumeForm { entryName: string; volumeName: string;