reverse update

pull/124/head
Artem Golub 9 months ago
parent 9d4046b3bb
commit 16e4b3d670

@ -3,21 +3,6 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/closebrackets": "^0.19.2",
"@codemirror/commands": "^6.10.2",
"@codemirror/comment": "^0.19.1",
"@codemirror/fold": "^0.19.4",
"@codemirror/history": "^0.19.2",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/legacy-modes": "^6.5.2",
"@codemirror/lint": "^6.9.4",
"@codemirror/matchbrackets": "^0.19.4",
"@codemirror/rectangular-selection": "^0.19.2",
"@codemirror/search": "^6.6.0",
"@codemirror/stream-parser": "^0.19.9",
"@codemirror/view": "^6.39.13",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@heroicons/react": "^2.2.0",
@ -34,7 +19,6 @@
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"axios": "^1.13.5",
"codemirror": "^6.0.2",
"d3": "^7.9.0",
"formik": "^2.4.9",
"lodash": "^4.17.23",

@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react';
import App from './App';
import { render, screen } from "@testing-library/react";
import App from "./App";
test('renders learn react link', () => {
test("renders learn react link", () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();

@ -1,99 +0,0 @@
// Based on https://github.com/codemirror/theme-one-dark
// Copyright (C) 2018-2021 by Marijn Haverbeke <marijnh@gmail.com> and others
// MIT License: https://github.com/codemirror/theme-one-dark/blob/main/LICENSE
import { HighlightStyle, tags as t } from "@codemirror/highlight";
const chalky = "#e5c07b",
coral = "#e06c75",
cyan = "#56b6c2",
invalid = "#ffffff",
ivory = "#abb2bf",
stone = "#5c6370",
malibu = "#61afef",
sage = "#98c379",
whiskey = "#d19a66",
violet = "#c678dd";
/// The highlighting style for code in the One Dark theme.
export default HighlightStyle.define([
{
tag: t.keyword,
color: violet
},
{
tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
color: coral
},
{
tag: [t.processingInstruction, t.string, t.inserted],
color: sage
},
{
tag: [t.function(t.variableName), t.labelName],
color: malibu
},
{
tag: [t.color, t.constant(t.name), t.standard(t.name)],
color: whiskey
},
{
tag: [t.definition(t.name), t.separator],
color: ivory
},
{
tag: [
t.typeName,
t.className,
t.number,
t.changed,
t.annotation,
t.modifier,
t.self,
t.namespace
],
color: chalky
},
{
tag: [
t.operator,
t.operatorKeyword,
t.url,
t.escape,
t.regexp,
t.link,
t.special(t.string)
],
color: cyan
},
{
tag: [t.meta, t.comment],
color: stone
},
{
tag: t.strong,
fontWeight: "bold"
},
{
tag: t.emphasis,
fontStyle: "italic"
},
{
tag: t.link,
color: stone,
textDecoration: "underline"
},
{
tag: t.heading,
fontWeight: "bold",
color: coral
},
{
tag: [t.atom, t.bool, t.special(t.variableName)],
color: whiskey
},
{
tag: t.invalid,
color: invalid
}
]);

@ -1,218 +0,0 @@
import { EditorView } from "@codemirror/view";
import { HighlightStyle, tags } from "@codemirror/highlight";
const base00 = "#1F2937",
base01 = "#073642",
base02 = "#586e75",
base03 = "#657b83",
base04 = "#839496",
base05 = "#dfdfdf",
base06 = "#efefef",
base07 = "#fdf6e3",
base_red = "#dc322f",
base_orange = "#cb4b16",
base_yellow = "#e9b100",
base_green = "#cfec11",
base_cyan = "#44e0d4",
base_blue = "#75c6ff",
base_violet = "#a1a6ff",
base_magenta = "#d33682";
const invalid = "#d30102",
stone = base04,
darkBackground = "#1F2937",
highlightBackground = "#173541",
background = base00,
tooltipBackground = base01,
selection = "#173541",
cursor = base04;
/**
The editor theme styles for Solarized Dark.
*/
const solarizedDarkTheme = /*@__PURE__*/ EditorView.theme(
{
"&": {
fontSize: "10.5pt",
color: base05,
backgroundColor: background
},
".cm-content": {
caretColor: cursor
},
".cm-cursor, .cm-dropCursor": { borderLeftColor: cursor },
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
{ backgroundColor: selection },
".cm-panels": { backgroundColor: darkBackground, color: base03 },
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
".cm-searchMatch": {
backgroundColor: "#72a1ff59",
outline: "1px solid #457dff"
},
".cm-searchMatch.cm-searchMatch-selected": {
backgroundColor: "#6199ff2f"
},
".cm-activeLine": { backgroundColor: highlightBackground },
".cm-selectionMatch": { backgroundColor: "#aafe661a" },
"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket": {
outline: `1px solid ${base06}`
},
".cm-gutters": {
backgroundColor: darkBackground,
color: stone,
border: "none"
},
".cm-activeLineGutter": {
backgroundColor: highlightBackground
},
".cm-foldPlaceholder": {
backgroundColor: "transparent",
border: "none",
color: "#ddd"
},
".cm-tooltip": {
border: "none",
backgroundColor: tooltipBackground
},
".cm-tooltip .cm-tooltip-arrow:before": {
borderTopColor: "transparent",
borderBottomColor: "transparent"
},
".cm-tooltip .cm-tooltip-arrow:after": {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground
},
".cm-tooltip-autocomplete": {
"& > ul > li[aria-selected]": {
backgroundColor: highlightBackground,
color: base03
}
}
},
{ dark: true }
);
/**
The highlighting style for code in the Solarized Dark theme.
*/
const solarizedDarkHighlightStyle = /*@__PURE__*/ HighlightStyle.define([
{ tag: tags.keyword, color: base_green },
{
tag: [
tags.name,
tags.deleted,
tags.character,
tags.propertyName,
tags.macroName
],
color: base_cyan
},
{ tag: [tags.variableName], color: base05 },
{ tag: [/*@__PURE__*/ tags.function(tags.variableName)], color: base_blue },
{ tag: [tags.labelName], color: base_magenta },
{
tag: [
tags.color,
/*@__PURE__*/ tags.constant(tags.name),
/*@__PURE__*/ tags.standard(tags.name)
],
color: base_yellow
},
{
tag: [/*@__PURE__*/ tags.definition(tags.name), tags.separator],
color: base_cyan
},
{ tag: [tags.brace], color: base_magenta },
{
tag: [tags.annotation],
color: invalid
},
{
tag: [
tags.number,
tags.changed,
tags.annotation,
tags.modifier,
tags.self,
tags.namespace
],
color: base_magenta
},
{
tag: [tags.typeName, tags.className],
color: base_orange
},
{
tag: [tags.operator, tags.operatorKeyword],
color: base_violet
},
{
tag: [tags.tagName],
color: base_blue
},
{
tag: [tags.squareBracket],
color: base_red
},
{
tag: [tags.angleBracket],
color: base02
},
{
tag: [tags.attributeName],
color: base05
},
{
tag: [tags.regexp],
color: invalid
},
{
tag: [tags.quote],
color: base_green
},
{ tag: [tags.string], color: base_yellow },
{
tag: [tags.url, tags.escape, /*@__PURE__*/ tags.special(tags.string)],
color: base_yellow
},
{ tag: [tags.meta], color: base_red },
{ tag: [tags.comment], color: base02, fontStyle: "italic" },
{ tag: tags.strong, fontWeight: "bold", color: base06 },
{ tag: tags.emphasis, fontStyle: "italic", color: base_green },
{ tag: tags.strikethrough, textDecoration: "line-through" },
{
tag: tags.link,
color: base_cyan,
textDecoration: "underline",
textUnderlinePosition: "under"
},
{ tag: tags.heading, fontWeight: "bold", color: base_yellow },
{ tag: tags.heading1, fontWeight: "bold", color: base07 },
{
tag: [tags.heading2, tags.heading3, tags.heading4],
fontWeight: "bold",
color: base06
},
{
tag: [tags.heading5, tags.heading6],
color: base06
},
{
tag: [tags.atom, tags.bool, /*@__PURE__*/ tags.special(tags.variableName)],
color: base_magenta
},
{
tag: [tags.processingInstruction, tags.inserted, tags.contentSeparator],
color: base_red
},
{
tag: [tags.contentSeparator],
color: base_yellow
},
{ tag: tags.invalid, color: base02, borderBottom: `1px dotted ${base_red}` }
]);
/**
Extension to enable the Solarized Dark theme (both the editor theme and
the highlight style).
*/
const solarizedDark = [solarizedDarkTheme, solarizedDarkHighlightStyle];
export { solarizedDark, solarizedDarkHighlightStyle, solarizedDarkTheme };

@ -1,30 +0,0 @@
import { useCallback, useEffect, useState } from "react";
import { EditorView } from "@codemirror/view";
import { EditorState } from "@codemirror/state";
import { Extension } from "@codemirror/state";
export default function useCodeMirror(extensions: Extension[], doc: any) {
const [element, setElement] = useState<HTMLElement>();
const ref = useCallback((node: HTMLElement | null) => {
if (!node) return;
setElement(node);
}, []);
useEffect(() => {
if (!element) return;
const view = new EditorView({
state: EditorState.create({
doc: doc,
extensions: [...extensions]
}),
parent: element
});
return () => view?.destroy();
}, [element, extensions, doc]);
return { ref };
}

@ -10,9 +10,15 @@ import eventBus from "../../events/eventBus";
import CodeEditor from "../CodeEditor";
import useWindowDimensions from "../../hooks/useWindowDimensions";
const CodeBox = () => {
interface ICodeBoxProps {
onCodeUpdate: (composeData: unknown) => string;
}
const CodeBox = (props: ICodeBoxProps) => {
const { onCodeUpdate } = props;
const versionRef = useRef<string>();
const manifestRef = useRef<string>();
const lastGeneratedCodeRef = useRef<string>("");
const [language, setLanguage] = useState("yaml");
const [version, setVersion] = useState("latest");
const [copyText, setCopyText] = useState("Copy");
@ -49,6 +55,45 @@ const CodeBox = () => {
[]
);
const parseComposeCode = (
data: string,
sourceLanguage: string
): Record<string, unknown> | null => {
if (!data.trim()) {
return null;
}
try {
const parsed =
sourceLanguage === "json" ? JSON.parse(data) : YAML.parse(data);
if (Array.isArray(parsed) && parsed.length > 0) {
return parsed[0]?.constructor === Object ? parsed[0] : null;
}
return parsed?.constructor === Object ? parsed : null;
} catch {
return null;
}
};
const debouncedOnCodeUpdate = useMemo(
() =>
debounce((data: string, sourceLanguage: string) => {
const parsedCompose = parseComposeCode(data, sourceLanguage);
if (!parsedCompose) {
return;
}
const parsedVersion = onCodeUpdate(parsedCompose);
if (parsedVersion && parsedVersion !== versionRef.current) {
setVersion(parsedVersion);
}
}, 600),
[onCodeUpdate]
);
const versionChange = (e: any) => {
setVersion(e.target.value);
};
@ -64,14 +109,23 @@ const CodeBox = () => {
useEffect(() => {
if (language === "json") {
setFormattedCode(
JSON.stringify(YAML.parseAllDocuments(generatedCode), null, 2)
);
try {
const docs = YAML.parseAllDocuments(generatedCode).map((doc) =>
doc.toJSON()
);
const jsonValue = docs.length <= 1 ? docs[0] || {} : docs;
const nextCode = JSON.stringify(jsonValue, null, 2);
lastGeneratedCodeRef.current = nextCode;
setFormattedCode(nextCode);
} catch {
lastGeneratedCodeRef.current = generatedCode;
setFormattedCode(generatedCode);
}
return;
}
if (language === "yaml") {
setFormattedCode(generatedCode);
}
lastGeneratedCodeRef.current = generatedCode;
setFormattedCode(generatedCode);
}, [language, generatedCode]);
useEffect(() => {
@ -94,6 +148,13 @@ const CodeBox = () => {
};
}, []);
useEffect(() => {
return () => {
debouncedOnGraphUpdate.cancel();
debouncedOnCodeUpdate.cancel();
};
}, [debouncedOnCodeUpdate, debouncedOnGraphUpdate]);
return (
<>
<div
@ -139,10 +200,16 @@ const CodeBox = () => {
<CodeEditor
data={formattedCode}
language={language}
onChange={() => {
return;
onChange={(value: string) => {
setFormattedCode(value);
if (value === lastGeneratedCodeRef.current) {
return;
}
debouncedOnCodeUpdate(value, language);
}}
disabled={true}
disabled={false}
lineWrapping={false}
height={height - 64}
/>

@ -1,4 +1,4 @@
import { useEffect, useState, useRef } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import { Dictionary, omit } from "lodash";
import {
@ -9,6 +9,7 @@ import {
import {
IServiceNodeItem,
IVolumeNodeItem,
INetworkNodeItem,
IServiceNodePosition,
IProject,
IProjectPayload
@ -40,6 +41,7 @@ import { useTitle } from "../../hooks";
import CodeBox from "./CodeBox";
import Header from "./Header";
import { useMutation } from "react-query";
import { composeToCanvasGraph } from "../../utils/compose";
interface IProjectProps {
isAuthenticated: boolean;
@ -55,6 +57,10 @@ export default function Project(props: IProjectProps) {
const stateConnectionsRef = useRef<[[string, string]] | []>();
const stateNetworksRef = useRef({});
const stateProjectRef = useRef();
const suppressGraphToCodeSyncRef = useRef(false);
const suppressGraphToCodeSyncTimeoutRef = useRef<ReturnType<
typeof setTimeout
> | null>(null);
const [showModalCreateService, setShowModalCreateService] = useState(false);
const [showVolumesModal, setShowVolumesModal] = useState(false);
@ -102,6 +108,27 @@ export default function Project(props: IProjectProps) {
stateNetworksRef.current = networks;
stateProjectRef.current = data;
const suppressGraphToCodeSync = useCallback((durationMs = 1200) => {
suppressGraphToCodeSyncRef.current = true;
if (suppressGraphToCodeSyncTimeoutRef.current) {
clearTimeout(suppressGraphToCodeSyncTimeoutRef.current);
}
suppressGraphToCodeSyncTimeoutRef.current = setTimeout(() => {
suppressGraphToCodeSyncRef.current = false;
suppressGraphToCodeSyncTimeoutRef.current = null;
}, durationMs);
}, []);
useEffect(() => {
return () => {
if (suppressGraphToCodeSyncTimeoutRef.current) {
clearTimeout(suppressGraphToCodeSyncTimeoutRef.current);
}
};
}, []);
const onNodeUpdate = (positionData: IServiceNodePosition) => {
if (stateNodesRef.current) {
const node = {
@ -155,6 +182,10 @@ export default function Project(props: IProjectProps) {
};
const onGraphUpdate = (graphData: any) => {
if (suppressGraphToCodeSyncRef.current) {
return;
}
const data = { ...graphData };
data.networks = stateNetworksRef.current;
eventBus.dispatch("FETCH_CODE", {
@ -162,6 +193,31 @@ export default function Project(props: IProjectProps) {
});
};
const onCodeUpdate = useCallback(
(composeData: unknown): string => {
suppressGraphToCodeSync();
const nextGraph = composeToCanvasGraph(
composeData,
(stateNodesRef.current as Dictionary<
IServiceNodeItem | IVolumeNodeItem
>) || {},
(stateNetworksRef.current as Record<string, INetworkNodeItem>) || {}
);
stateNodesRef.current = nextGraph.nodes;
stateConnectionsRef.current = nextGraph.connections as [[string, string]];
stateNetworksRef.current = nextGraph.networks as any;
setNodes(nextGraph.nodes);
setConnections(nextGraph.connections as [[string, string]]);
setNetworks(nextGraph.networks);
return nextGraph.version;
},
[suppressGraphToCodeSync]
);
const onCanvasUpdate = (updatedCanvasPosition: any) => {
setCanvasPosition({ ...canvasPosition, ...updatedCanvasPosition });
};
@ -692,7 +748,7 @@ export default function Project(props: IProjectProps) {
</div>
<div className="group code-column w-1/2 md:w-1/3 absolute top-0 right-0 sm:relative z-40 md:z-30">
<CodeBox />
<CodeBox onCodeUpdate={onCodeUpdate} />
</div>
</div>
</div>

@ -3,30 +3,20 @@ const Logo = () => {
<svg
width="24"
height="24"
viewBox="0 0 270 270"
viewBox="0 0 92 148"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M26.0711 110.924C22.1658 107.019 22.1658 100.687 26.0711 96.7817L96.7817 26.071C100.687 22.1658 107.019 22.1658 110.924 26.071L144.866 60.0132C148.771 63.9184 148.771 70.2501 144.866 74.1553L74.1554 144.866C70.2501 148.771 63.9185 148.771 60.0132 144.866L26.0711 110.924Z"
fill="#2697EC"
d="M80.7266 47.1644C84.796 43.095 91.8316 46.1194 91.7899 51.9202L91.4054 105.37C91.3801 108.89 89.9848 112.24 87.5153 114.709L56.3589 145.866C52.2895 149.935 45.2539 146.911 45.2956 141.11L45.6802 87.6601C45.7055 84.1399 47.1007 80.7903 49.5702 78.3208L80.7266 47.1644Z"
fill="#6CBFFD"
/>
<rect
x="118.736"
y="109.654"
width="120"
height="68.0015"
rx="10"
transform="rotate(-45 118.736 109.654)"
fill="#F38432"
<path
d="M58.5779 23.2462C62.6472 19.1768 69.6828 22.2011 69.6412 28.0019L69.2623 80.6629C69.2335 84.689 67.6377 88.5202 64.8133 91.3445L34.2103 121.948C30.1409 126.017 23.1052 122.992 23.1468 117.192L23.5314 63.7418C23.5568 60.2216 24.952 56.872 27.4214 54.4026L58.5779 23.2462Z"
fill="#2697EC"
/>
<rect
x="27.3326"
y="202.095"
width="120"
height="68.0015"
rx="10"
transform="rotate(-45 27.3326 202.095)"
<path
d="M35.4312 1.86898C39.5006 -2.20041 46.5362 0.824 46.4945 6.62475L46.11 60.0746C46.0847 63.5947 44.6895 66.9444 42.22 69.4138L11.0635 100.57C6.99414 104.64 -0.0414986 101.615 0.000189715 95.8145L0.384779 42.3647C0.410123 38.8445 1.80533 35.4948 4.27482 33.0253L35.4312 1.86898Z"
fill="#2E6DE5"
/>
</svg>

@ -0,0 +1,388 @@
import { Dictionary } from "lodash";
import {
ensure,
flattenLibraries,
getClientNodeItem,
iconByNodeType,
attachUUID
} from "./index";
import { INetworkNodeItem, IServiceNodeItem, IVolumeNodeItem } from "../types";
import { nodeLibraries } from "./data/libraries";
type ComposeRecord = Record<string, unknown>;
type ComposeNodes = Dictionary<IServiceNodeItem | IVolumeNodeItem>;
interface IComposeGraphResult {
version: string;
nodes: ComposeNodes;
connections: Array<[string, string]>;
networks: Record<string, INetworkNodeItem>;
}
const COMPOSE_RESERVED_TOP_LEVEL_KEYS = new Set([
"version",
"services",
"volumes",
"networks",
"configs",
"secrets",
"name"
]);
const isRecord = (value: unknown): value is ComposeRecord => {
return Boolean(value) && value?.constructor === Object;
};
const toRecord = (value: unknown): ComposeRecord => {
if (isRecord(value)) {
return value;
}
return {};
};
const getDefaultNodePosition = (index: number) => {
const columns = 3;
const left = 100 + (index % columns) * 280;
const top = 100 + Math.floor(index / columns) * 180;
return { left, top };
};
const getDependsOnServiceNames = (dependsOn: unknown): string[] => {
if (Array.isArray(dependsOn)) {
return dependsOn.filter((item): item is string => typeof item === "string");
}
if (isRecord(dependsOn)) {
return Object.keys(dependsOn);
}
return [];
};
const getVolumeMountSourceName = (mount: unknown): string | null => {
if (typeof mount === "string") {
const source = mount.split(":", 1)[0].trim();
if (!source) {
return null;
}
// Ignore bind mounts and host paths.
if (
source.startsWith("/") ||
source.startsWith(".") ||
source.startsWith("~") ||
/^[a-zA-Z]:[\\/]/.test(source)
) {
return null;
}
return source;
}
if (isRecord(mount) && typeof mount.source === "string") {
const source = mount.source.trim();
const mountType =
typeof mount.type === "string" ? mount.type.toLowerCase() : "";
if (!source) {
return null;
}
// Long syntax can represent bind mounts as well.
if (mountType && mountType !== "volume") {
return null;
}
return source;
}
return null;
};
const getServiceVolumeNames = (volumes: unknown): string[] => {
if (!Array.isArray(volumes)) {
return [];
}
const names: string[] = [];
volumes.forEach((mount) => {
const sourceName = getVolumeMountSourceName(mount);
if (sourceName && !names.includes(sourceName)) {
names.push(sourceName);
}
});
return names;
};
const getLegacyV1Services = (composeData: ComposeRecord): ComposeRecord => {
const services: ComposeRecord = {};
Object.entries(composeData).forEach(([key, value]) => {
if (
COMPOSE_RESERVED_TOP_LEVEL_KEYS.has(key) ||
key.startsWith("x-") ||
!isRecord(value)
) {
return;
}
services[key] = value;
});
return services;
};
const normalizeVersion = (
composeData: ComposeRecord,
services: ComposeRecord
): string => {
const versionValue = composeData.version;
if (typeof versionValue === "number") {
const major = Math.trunc(versionValue).toString();
if (["1", "2", "3"].includes(major)) {
return major;
}
}
if (typeof versionValue === "string") {
const normalized = versionValue.trim().toLowerCase();
if (
["latest", "latest (spec)", "spec", "compose-spec"].includes(normalized)
) {
return "latest";
}
const major = normalized.split(".")[0];
if (["1", "2", "3"].includes(major)) {
return major;
}
}
// Legacy Compose v1 does not have top-level "version" and "services".
if (
!composeData.version &&
!composeData.services &&
Object.keys(services).length
) {
return "1";
}
return "latest";
};
const getExistingServicesByName = (
nodes: ComposeNodes
): Record<string, IServiceNodeItem> => {
const map: Record<string, IServiceNodeItem> = {};
Object.values(nodes).forEach((node) => {
if (node.type === "SERVICE") {
const serviceNode = node as IServiceNodeItem;
const serviceName = serviceNode.canvasConfig.node_name;
if (serviceName) {
map[serviceName] = serviceNode;
}
}
});
return map;
};
const getExistingVolumesByName = (
nodes: ComposeNodes
): Record<string, IVolumeNodeItem> => {
const map: Record<string, IVolumeNodeItem> = {};
Object.values(nodes).forEach((node) => {
if (node.type === "VOLUME") {
const volumeNode = node as IVolumeNodeItem;
const volumeName =
volumeNode.canvasConfig.node_name || volumeNode.volumeConfig.name;
if (volumeName) {
map[volumeName as string] = volumeNode;
}
}
});
return map;
};
const getExistingNetworksByName = (
networks: Record<string, INetworkNodeItem>
): Record<string, INetworkNodeItem> => {
const map: Record<string, INetworkNodeItem> = {};
Object.values(networks).forEach((networkNode) => {
const networkName = networkNode.canvasConfig.node_name;
if (networkName) {
map[networkName] = networkNode;
}
});
return map;
};
export const composeToCanvasGraph = (
composeInput: unknown,
existingNodes: ComposeNodes = {},
existingNetworks: Record<string, INetworkNodeItem> = {}
): IComposeGraphResult => {
const composeData = toRecord(composeInput);
const composeServices = composeData.services
? toRecord(composeData.services)
: getLegacyV1Services(composeData);
const composeVolumes = toRecord(composeData.volumes);
const composeNetworks = toRecord(composeData.networks);
const version = normalizeVersion(composeData, composeServices);
const libraries = flattenLibraries(nodeLibraries);
const serviceLibrary = ensure(libraries.find((l) => l.type === "SERVICE"));
const volumeLibrary = ensure(libraries.find((l) => l.type === "VOLUME"));
const existingServiceNodesByName = getExistingServicesByName(existingNodes);
const existingVolumeNodesByName = getExistingVolumesByName(existingNodes);
const existingNetworkNodesByName =
getExistingNetworksByName(existingNetworks);
const nextNodes: ComposeNodes = {};
const serviceNodesByName: Record<string, IServiceNodeItem> = {};
const volumeNodesByName: Record<string, IVolumeNodeItem> = {};
let autoPositionIndex = 0;
Object.entries(composeServices).forEach(([serviceName, rawConfig]) => {
const existingNode = existingServiceNodesByName[serviceName];
const node = {
key: existingNode?.key ?? attachUUID("service"),
type: "SERVICE",
position:
existingNode?.position ?? getDefaultNodePosition(autoPositionIndex++),
inputs: existingNode?.inputs ?? [],
outputs: existingNode?.outputs ?? [],
canvasConfig: {
node_name: serviceName,
node_icon: existingNode?.canvasConfig.node_icon ?? ""
},
serviceConfig: toRecord(rawConfig)
} as IServiceNodeItem;
const clientNode = getClientNodeItem(
node as IServiceNodeItem,
serviceLibrary
) as IServiceNodeItem;
serviceNodesByName[serviceName] = clientNode;
nextNodes[clientNode.key] = clientNode;
});
Object.entries(composeVolumes).forEach(([volumeName, rawConfig]) => {
const existingNode = existingVolumeNodesByName[volumeName];
const nextVolumeConfig = {
...toRecord(rawConfig),
name:
(toRecord(rawConfig).name as string | undefined) ??
existingNode?.volumeConfig.name ??
volumeName
};
const node = {
key: existingNode?.key ?? attachUUID("volume"),
type: "VOLUME",
position:
existingNode?.position ?? getDefaultNodePosition(autoPositionIndex++),
inputs: existingNode?.inputs ?? [],
outputs: existingNode?.outputs ?? [],
canvasConfig: {
node_name: volumeName,
node_icon:
existingNode?.canvasConfig.node_icon ?? iconByNodeType.volume ?? ""
},
volumeConfig: nextVolumeConfig
} as IVolumeNodeItem;
const clientNode = getClientNodeItem(
node as unknown as IServiceNodeItem,
volumeLibrary
) as unknown as IVolumeNodeItem;
volumeNodesByName[volumeName] = clientNode;
nextNodes[clientNode.key] = clientNode;
});
const connections: Array<[string, string]> = [];
const seenConnections = new Set<string>();
const addConnection = (source: string, target: string) => {
const connectionKey = `${source}->${target}`;
if (seenConnections.has(connectionKey)) {
return;
}
seenConnections.add(connectionKey);
connections.push([source, target]);
};
Object.entries(serviceNodesByName).forEach(([serviceName, serviceNode]) => {
const sourceService = serviceNodesByName[serviceName];
const dependsOnNames = getDependsOnServiceNames(
sourceService.serviceConfig.depends_on
);
dependsOnNames.forEach((dependsOnName) => {
const targetService = serviceNodesByName[dependsOnName];
if (targetService) {
addConnection(sourceService.key, targetService.key);
}
});
const volumeNames = getServiceVolumeNames(
sourceService.serviceConfig.volumes
);
volumeNames.forEach((volumeName) => {
const sourceVolume = volumeNodesByName[volumeName];
if (sourceVolume) {
addConnection(sourceVolume.key, serviceNode.key);
}
});
});
const nextNetworks: Record<string, INetworkNodeItem> = {};
Object.entries(composeNetworks).forEach(([networkName, rawConfig]) => {
const existingNode = existingNetworkNodesByName[networkName];
const nextNetworkConfig = {
...toRecord(rawConfig),
name:
(toRecord(rawConfig).name as string | undefined) ??
existingNode?.networkConfig.name ??
networkName
};
const networkNode: INetworkNodeItem = {
key: existingNode?.key ?? attachUUID("network"),
type: "NETWORK",
position: existingNode?.position ?? { left: 0, top: 0 },
inputs: existingNode?.inputs ?? [],
outputs: existingNode?.outputs ?? [],
canvasConfig: {
node_name: networkName,
node_icon:
existingNode?.canvasConfig.node_icon ?? iconByNodeType.network ?? ""
},
networkConfig: nextNetworkConfig
};
nextNetworks[networkNode.key] = networkNode;
});
return {
version,
nodes: nextNodes,
connections,
networks: nextNetworks
};
};
Loading…
Cancel
Save