Cavnas: Implement basic service discovery logic
Change-Id: I71b25076dba94d6491ad4db748b259870991c526
diff --git a/apps/canvas/front/package-lock.json b/apps/canvas/front/package-lock.json
index 94ca1b2..cb422f6 100644
--- a/apps/canvas/front/package-lock.json
+++ b/apps/canvas/front/package-lock.json
@@ -13,15 +13,16 @@
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-collapsible": "^1.1.1",
- "@radix-ui/react-dialog": "^1.1.2",
+ "@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-icons": "^1.3.1",
- "@radix-ui/react-label": "^2.1.0",
+ "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.13",
"@radix-ui/react-tooltip": "^1.1.4",
@@ -1403,24 +1404,25 @@
}
},
"node_modules/@radix-ui/react-dialog": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz",
- "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==",
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
+ "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
+ "license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.0",
- "@radix-ui/react-compose-refs": "1.1.0",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-dismissable-layer": "1.1.1",
- "@radix-ui/react-focus-guards": "1.1.1",
- "@radix-ui/react-focus-scope": "1.1.0",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-portal": "1.1.2",
- "@radix-ui/react-presence": "1.1.1",
- "@radix-ui/react-primitive": "2.0.0",
- "@radix-ui/react-slot": "1.1.0",
- "@radix-ui/react-use-controllable-state": "1.1.0",
- "aria-hidden": "^1.1.1",
- "react-remove-scroll": "2.6.0"
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -1437,6 +1439,308 @@
}
}
},
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
+ "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
+ "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+ "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.0.tgz",
+ "integrity": "sha512-sGsQtcjMqdQyijAHytfGEELB8FufGbfXIsvUTe+NLx1GDRJCXtCFLBLUI1eyZCKXXvbEU2C6gai0PZKoIE9Vbg==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-direction": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
@@ -1699,11 +2003,12 @@
}
},
"node_modules/@radix-ui/react-label": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.0.tgz",
- "integrity": "sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==",
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
+ "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
+ "license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.0.0"
+ "@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -1720,6 +2025,62 @@
}
}
},
+ "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-menu": {
"version": "2.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.14.tgz",
@@ -2535,6 +2896,179 @@
}
}
},
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz",
+ "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/primitive": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz",
diff --git a/apps/canvas/front/package.json b/apps/canvas/front/package.json
index b25d17b..43d1232 100644
--- a/apps/canvas/front/package.json
+++ b/apps/canvas/front/package.json
@@ -21,15 +21,16 @@
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-collapsible": "^1.1.1",
- "@radix-ui/react-dialog": "^1.1.2",
+ "@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-icons": "^1.3.1",
- "@radix-ui/react-label": "^2.1.0",
+ "@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.13",
"@radix-ui/react-tooltip": "^1.1.4",
diff --git a/apps/canvas/front/src/ProjectSelect.tsx b/apps/canvas/front/src/ProjectSelect.tsx
index 278db67..312e6aa 100644
--- a/apps/canvas/front/src/ProjectSelect.tsx
+++ b/apps/canvas/front/src/ProjectSelect.tsx
@@ -117,7 +117,6 @@
});
});
}, [name, setCreateNewOpen, toast, refreshProjects]);
- console.log("asd", projectId);
return (
<>
<Select onValueChange={onSelect} value={projectId}>
diff --git a/apps/canvas/front/src/Tools.tsx b/apps/canvas/front/src/Tools.tsx
index f355d3e..4c9b19e 100644
--- a/apps/canvas/front/src/Tools.tsx
+++ b/apps/canvas/front/src/Tools.tsx
@@ -27,7 +27,7 @@
<TabsContent value="gateways">
<Gateways />
</TabsContent>
- <TabsContent value="deployKeys">{env.deployKey && <>{env.deployKey}</>}</TabsContent>
+ <TabsContent value="deployKeys">{env.deployKeyPublic && <>{env.deployKeyPublic}</>}</TabsContent>
</div>
</Tabs>
);
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index 24198cb..dbb5ea6 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -169,6 +169,10 @@
}
const resp = await fetch(`/api/project/${projectId}`, {
method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ state: JSON.stringify(instance.toObject()) }),
});
if (resp.ok) {
clear();
@@ -177,7 +181,7 @@
} else {
error("Failed to delete project", await resp.text());
}
- }, [store, clear, projectId, info, error]);
+ }, [store, clear, projectId, info, error, instance]);
const reload = useCallback(async () => {
if (projectId == null) {
return;
diff --git a/apps/canvas/front/src/components/node-github.tsx b/apps/canvas/front/src/components/node-github.tsx
index 4c065be..dd7c68a 100644
--- a/apps/canvas/front/src/components/node-github.tsx
+++ b/apps/canvas/front/src/components/node-github.tsx
@@ -1,6 +1,19 @@
import { NodeRect } from "./node-rect";
-import { GithubNode, nodeIsConnectable, nodeLabel, useStateStore, useGithubService } from "@/lib/state";
-import { useEffect, useMemo, useState } from "react";
+import {
+ GithubNode,
+ nodeIsConnectable,
+ nodeLabel,
+ serviceAnalyzisSchema,
+ useStateStore,
+ useGithubService,
+ ServiceType,
+ ServiceData,
+ useGithubRepositories,
+ useGithubRepositoriesLoading,
+ useGithubRepositoriesError,
+ useFetchGithubRepositories,
+} from "@/lib/state";
+import { useCallback, useEffect, useMemo, useState } from "react";
import { z } from "zod";
import { DeepPartial, EventType, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -10,7 +23,12 @@
import { GitHubRepository } from "../lib/github";
import { useProjectId } from "@/lib/state";
import { Alert, AlertDescription } from "./ui/alert";
-import { AlertCircle } from "lucide-react";
+import { AlertCircle, LoaderCircle, RefreshCw } from "lucide-react";
+import { Button } from "./ui/button";
+import { v4 as uuidv4 } from "uuid";
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
+import { Switch } from "./ui/switch";
+import { Label } from "./ui/label";
export function NodeGithub(node: GithubNode) {
const { id, selected } = node;
@@ -39,34 +57,42 @@
export function NodeGithubDetails({ id, data, disabled }: GithubNode & { disabled?: boolean }) {
const store = useStateStore();
const projectId = useProjectId();
- const [repos, setRepos] = useState<GitHubRepository[]>([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string | null>(null);
const githubService = useGithubService();
+ const storeRepos = useGithubRepositories();
+ const isLoadingRepos = useGithubRepositoriesLoading();
+ const repoError = useGithubRepositoriesError();
+ const fetchStoreRepositories = useFetchGithubRepositories();
+
+ const [displayRepos, setDisplayRepos] = useState<GitHubRepository[]>([]);
+
+ const [isAnalyzing, setIsAnalyzing] = useState(false);
+ const [showModal, setShowModal] = useState(false);
+ const [discoveredServices, setDiscoveredServices] = useState<z.infer<typeof serviceAnalyzisSchema>[]>([]);
+ const [selectedServices, setSelectedServices] = useState<Record<string, boolean>>({});
+
useEffect(() => {
+ let currentRepoInStore = false;
if (data.repository) {
- const { id, sshURL } = data.repository;
- setRepos((prevRepos) => {
- if (!prevRepos.some((r) => r.id === id)) {
- return [
- ...prevRepos,
- {
- id,
- name: sshURL.split("/").pop() || "",
- full_name: sshURL.split("/").slice(-2).join("/"),
- html_url: "",
- ssh_url: sshURL,
- description: null,
- private: false,
- default_branch: "main",
- },
- ];
- }
- return prevRepos;
- });
+ currentRepoInStore = storeRepos.some((r) => r.id === data.repository!.id);
}
- }, [data.repository]);
+
+ if (data.repository && !currentRepoInStore) {
+ const currentRepoForDisplay: GitHubRepository = {
+ id: data.repository.id,
+ name: data.repository.sshURL.split("/").pop() || "",
+ full_name: data.repository.fullName || data.repository.sshURL.split("/").slice(-2).join("/"),
+ html_url: "",
+ ssh_url: data.repository.sshURL,
+ description: null,
+ private: false,
+ default_branch: "main",
+ };
+ setDisplayRepos([currentRepoForDisplay, ...storeRepos.filter((r) => r.id !== data.repository!.id)]);
+ } else {
+ setDisplayRepos(storeRepos);
+ }
+ }, [data.repository, storeRepos]);
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
@@ -77,6 +103,10 @@
});
useEffect(() => {
+ form.reset({ repositoryId: data.repository?.id });
+ }, [data.repository?.id, form]);
+
+ useEffect(() => {
const sub = form.watch(
(
value: DeepPartial<z.infer<typeof schema>>,
@@ -88,7 +118,7 @@
switch (name) {
case "repositoryId":
if (value.repositoryId) {
- const repo = repos.find((r) => r.id === value.repositoryId);
+ const repo = displayRepos.find((r) => r.id === value.repositoryId);
if (repo) {
store.updateNodeData<"github">(id, {
repository: {
@@ -104,26 +134,87 @@
},
);
return () => sub.unsubscribe();
- }, [form, store, id, repos]);
+ }, [form, store, id, displayRepos]);
- useEffect(() => {
- const fetchRepositories = async () => {
- if (!githubService) return;
+ const analyze = useCallback(async () => {
+ if (!data.repository?.sshURL) return;
- setLoading(true);
- setError(null);
- try {
- const repositories = await githubService.getRepositories();
- setRepos(repositories);
- } catch (err) {
- setError(err instanceof Error ? err.message : "Failed to fetch repositories");
- } finally {
- setLoading(false);
+ setIsAnalyzing(true);
+ try {
+ const resp = await fetch(`/api/project/${projectId}/analyze`, {
+ method: "POST",
+ body: JSON.stringify({
+ address: data.repository?.sshURL,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const servicesResult = z.array(serviceAnalyzisSchema).safeParse(await resp.json());
+ if (!servicesResult.success) {
+ console.error(servicesResult.error);
+ setIsAnalyzing(false);
+ return;
}
- };
- fetchRepositories();
- }, [githubService]);
+ setDiscoveredServices(servicesResult.data);
+ const initialSelectedServices: Record<string, boolean> = {};
+ servicesResult.data.forEach((service) => {
+ initialSelectedServices[service.name] = true;
+ });
+ setSelectedServices(initialSelectedServices);
+ setShowModal(true);
+ } catch (err) {
+ console.error("Analysis failed:", err);
+ } finally {
+ setIsAnalyzing(false);
+ }
+ }, [projectId, data.repository?.sshURL]);
+
+ const handleImportServices = () => {
+ discoveredServices.forEach((service) => {
+ if (selectedServices[service.name]) {
+ const newNodeData: Omit<ServiceData, "activeField" | "state"> = {
+ label: service.name,
+ repository: {
+ id: id,
+ },
+ info: service,
+ type: "nodejs:24.0.2" as ServiceType,
+ env: [],
+ volume: [],
+ preBuildCommands: "",
+ isChoosingPortToConnect: false,
+ envVars: [],
+ ports: [],
+ };
+ const newNodeId = uuidv4();
+ store.addNode({
+ id: newNodeId,
+ type: "app",
+ data: newNodeData,
+ });
+ let edges = store.edges;
+ edges = edges.concat({
+ id: uuidv4(),
+ source: id,
+ sourceHandle: "repository",
+ target: newNodeId,
+ targetHandle: "repository",
+ });
+ store.setEdges(edges);
+ }
+ });
+ setShowModal(false);
+ setDiscoveredServices([]);
+ setSelectedServices({});
+ };
+
+ const handleCancelModal = () => {
+ setShowModal(false);
+ setDiscoveredServices([]);
+ setSelectedServices({});
+ };
return (
<>
@@ -134,32 +225,57 @@
name="repositoryId"
render={({ field }) => (
<FormItem>
- <Select
- onValueChange={(value) => field.onChange(Number(value))}
- value={field.value?.toString()}
- disabled={loading || !projectId || !githubService || disabled}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue
- placeholder={
- githubService ? "Select a repository" : "GitHub not configured"
- }
- />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {repos.map((repo) => (
- <SelectItem key={repo.id} value={repo.id.toString()}>
- {repo.full_name}
- {repo.description && ` - ${repo.description}`}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
+ <div className="flex items-center gap-2 w-full">
+ <div className="flex-grow">
+ <Select
+ onValueChange={(value) => field.onChange(Number(value))}
+ value={field.value?.toString()}
+ disabled={isLoadingRepos || !projectId || !githubService || disabled}
+ >
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue
+ placeholder={
+ githubService
+ ? isLoadingRepos
+ ? "Loading..."
+ : displayRepos.length === 0
+ ? "No repositories found"
+ : "Select a repository"
+ : "GitHub not configured"
+ }
+ />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {displayRepos.map((repo) => (
+ <SelectItem key={repo.id} value={repo.id.toString()}>
+ {repo.full_name}
+ {repo.description && ` - ${repo.description}`}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ {isLoadingRepos && (
+ <Button variant="ghost" size="icon" disabled>
+ <LoaderCircle className="h-5 w-5 animate-spin text-muted-foreground" />
+ </Button>
+ )}
+ {!isLoadingRepos && githubService && (
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={fetchStoreRepositories}
+ disabled={disabled}
+ aria-label="Refresh repositories"
+ >
+ <RefreshCw className="h-5 w-5 text-muted-foreground" />
+ </Button>
+ )}
+ </div>
<FormMessage />
- {error && <p className="text-sm text-red-500">{error}</p>}
- {loading && <p className="text-sm text-gray-500">Loading repositories...</p>}
+ {repoError && <p className="text-sm text-red-500 mt-1">{repoError}</p>}
{!githubService && (
<Alert variant="destructive" className="mt-2">
<AlertCircle className="h-4 w-4" />
@@ -173,6 +289,62 @@
/>
</form>
</Form>
+ <Button disabled={!data.repository?.sshURL || isAnalyzing || !githubService || disabled} onClick={analyze}>
+ {isAnalyzing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
+ Scan for services
+ </Button>
+ {showModal && (
+ <Dialog open={showModal} onOpenChange={setShowModal}>
+ <DialogContent className="sm:max-w-[425px]">
+ <DialogHeader>
+ <DialogTitle>Discovered Services</DialogTitle>
+ <DialogDescription>Select the services you want to import.</DialogDescription>
+ </DialogHeader>
+ <div className="grid gap-4 py-4">
+ {discoveredServices.map((service) => (
+ <div key={service.name} className="flex flex-col space-y-2 p-2 border rounded-md">
+ <div className="flex items-center space-x-2">
+ <Switch
+ id={service.name}
+ checked={selectedServices[service.name]}
+ onCheckedChange={(checked: boolean) =>
+ setSelectedServices((prev) => ({
+ ...prev,
+ [service.name]: checked,
+ }))
+ }
+ />
+ <Label htmlFor={service.name} className="font-semibold">
+ {service.name}
+ </Label>
+ </div>
+ <div className="pl-6 text-sm text-gray-600">
+ <p>
+ <span className="font-medium">Location:</span> {service.location}
+ </p>
+ {service.configVars && service.configVars.length > 0 && (
+ <div className="mt-1">
+ <p className="font-medium">Environment Variables:</p>
+ <ul className="list-disc list-inside pl-4">
+ {service.configVars.map((envVar) => (
+ <li key={envVar.name}>{envVar.name}</li>
+ ))}
+ </ul>
+ </div>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ <DialogFooter>
+ <Button variant="outline" onClick={handleCancelModal}>
+ Cancel
+ </Button>
+ <Button onClick={handleImportServices}>Import</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )}
</>
);
}
diff --git a/apps/canvas/front/src/components/ui/switch.tsx b/apps/canvas/front/src/components/ui/switch.tsx
new file mode 100644
index 0000000..5139bf2
--- /dev/null
+++ b/apps/canvas/front/src/components/ui/switch.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import * as React from "react";
+import * as SwitchPrimitives from "@radix-ui/react-switch";
+
+import { cn } from "@/lib/utils";
+
+const Switch = React.forwardRef<
+ React.ElementRef<typeof SwitchPrimitives.Root>,
+ React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
+>(({ className, ...props }, ref) => (
+ <SwitchPrimitives.Root
+ className={cn(
+ "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
+ className,
+ )}
+ {...props}
+ ref={ref}
+ >
+ <SwitchPrimitives.Thumb
+ className={cn(
+ "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
+ )}
+ />
+ </SwitchPrimitives.Root>
+));
+Switch.displayName = SwitchPrimitives.Root.displayName;
+
+export { Switch };
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index 572fc78..513f1f0 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -309,6 +309,7 @@
EmptyValidator,
GitRepositoryValidator,
ServiceValidator,
+ ServiceAnalyzisValidator,
GatewayHTTPSValidator,
GatewayTCPValidator,
),
@@ -488,6 +489,56 @@
return noName.concat(noSource).concat(noRuntime).concat(noPorts).concat(noIngress).concat(multipleIngress);
}
+function ServiceAnalyzisValidator(nodes: AppNode[]): Message[] {
+ const apps = nodes.filter((n) => n.type === "app");
+ return apps
+ .filter((n) => n.data.info != null)
+ .flatMap((n) => {
+ return n.data
+ .info!.configVars.map((cv): Message | undefined => {
+ if (cv.semanticType === "PORT") {
+ if (
+ !(n.data.envVars || []).some(
+ (p) => ("name" in p && p.name === cv.name) || ("alias" in p && p.alias === cv.name),
+ )
+ ) {
+ return {
+ id: `${n.id}-missing-port-${cv.name}`,
+ type: "WARNING",
+ nodeId: n.id,
+ message: `Service requires port env variable ${cv.name}`,
+ };
+ }
+ }
+ if (cv.category === "EnvironmentVariable") {
+ if (
+ !(n.data.envVars || []).some(
+ (p) => ("name" in p && p.name === cv.name) || ("alias" in p && p.alias === cv.name),
+ )
+ ) {
+ if (cv.semanticType === "FILESYSTEM_PATH") {
+ return {
+ id: `${n.id}-missing-env-${cv.name}`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: `Service requires env variable ${cv.name}, representing filesystem path`,
+ };
+ } else if (cv.semanticType === "POSTGRES_URL") {
+ return {
+ id: `${n.id}-missing-env-${cv.name}`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: `Service requires env variable ${cv.name}, representing postgres connection URL`,
+ };
+ }
+ }
+ }
+ return undefined;
+ })
+ .filter((m) => m !== undefined);
+ });
+}
+
function GatewayHTTPSValidator(nodes: AppNode[]): Message[] {
const ing = nodes.filter((n) => n.type === "gateway-https");
const noNetwork: Message[] = ing
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index b7413d9..5d8013d 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -1,6 +1,6 @@
import { Category, defaultCategories } from "./categories";
import { CreateValidators, Validator } from "./config";
-import { GitHubService, GitHubServiceImpl } from "./github";
+import { GitHubService, GitHubServiceImpl, GitHubRepository } from "./github";
import type { Edge, Node, OnConnect, OnEdgesChange, OnNodesChange, Viewport as ReactFlowViewport } from "@xyflow/react";
import {
addEdge,
@@ -16,6 +16,41 @@
import { z } from "zod";
import { create } from "zustand";
+export const serviceAnalyzisSchema = z.object({
+ name: z.string(),
+ location: z.string(),
+ configVars: z.array(
+ z.object({
+ name: z.string(),
+ category: z.enum(["CommandLineFlag", "EnvironmentVariable"]),
+ type: z.optional(z.enum(["String", "Number", "Boolean"])),
+ semanticType: z.optional(
+ z.enum([
+ "EXPANDED_ENV_VAR",
+ "PORT",
+ "FILESYSTEM_PATH",
+ "DATABASE_URL",
+ "SQLITE_PATH",
+ "POSTGRES_URL",
+ "POSTGRES_PASSWORD",
+ "POSTGRES_USER",
+ "POSTGRES_DB",
+ "POSTGRES_PORT",
+ "POSTGRES_HOST",
+ "POSTGRES_SSL",
+ "MONGO_URL",
+ "MONGO_PASSWORD",
+ "MONGO_USER",
+ "MONGO_DB",
+ "MONGO_PORT",
+ "MONGO_HOST",
+ "MONGO_SSL",
+ ]),
+ ),
+ }),
+ ),
+});
+
export type InitData = {
label: string;
envVars: BoundEnvVar[];
@@ -125,6 +160,7 @@
codeServerNodeId: string;
sshNodeId: string;
};
+ info?: z.infer<typeof serviceAnalyzisSchema>;
};
export type ServiceNode = Node<ServiceData> & {
@@ -425,7 +461,8 @@
export const envSchema = z.object({
managerAddr: z.optional(z.string().min(1)),
- deployKey: z.optional(z.nullable(z.string().min(1))),
+ instanceId: z.optional(z.string().min(1)),
+ deployKeyPublic: z.optional(z.nullable(z.string().min(1))),
networks: z
.array(
z.object({
@@ -451,7 +488,8 @@
const defaultEnv: Env = {
managerAddr: undefined,
- deployKey: undefined,
+ deployKeyPublic: undefined,
+ instanceId: undefined,
networks: [],
integrations: {
github: false,
@@ -499,6 +537,9 @@
viewport: Viewport;
setViewport: (viewport: Viewport) => void;
githubService: GitHubService | null;
+ githubRepositories: GitHubRepository[];
+ githubRepositoriesLoading: boolean;
+ githubRepositoriesError: string | null;
setHighlightCategory: (name: string, active: boolean) => void;
onNodesChange: OnNodesChange<AppNode>;
onEdgesChange: OnEdgesChange;
@@ -512,6 +553,7 @@
updateNodeData: <T extends NodeType>(id: string, data: NodeDataUpdate<T>) => void;
replaceEdge: (c: Connection, id?: string) => void;
refreshEnv: () => Promise<void>;
+ fetchGithubRepositories: () => Promise<void>;
};
const projectIdSelector = (state: AppState) => state.projectId;
@@ -520,6 +562,9 @@
const githubServiceSelector = (state: AppState) => state.githubService;
const envSelector = (state: AppState) => state.env;
const zoomSelector = (state: AppState) => state.zoom;
+const githubRepositoriesSelector = (state: AppState) => state.githubRepositories;
+const githubRepositoriesLoadingSelector = (state: AppState) => state.githubRepositoriesLoading;
+const githubRepositoriesErrorSelector = (state: AppState) => state.githubRepositoriesError;
export function useZoom(): ReactFlowViewport {
return useStateStore(zoomSelector);
@@ -561,6 +606,22 @@
return useStateStore(githubServiceSelector);
}
+export function useGithubRepositories(): GitHubRepository[] {
+ return useStateStore(githubRepositoriesSelector);
+}
+
+export function useGithubRepositoriesLoading(): boolean {
+ return useStateStore(githubRepositoriesLoadingSelector);
+}
+
+export function useGithubRepositoriesError(): string | null {
+ return useStateStore(githubRepositoriesErrorSelector);
+}
+
+export function useFetchGithubRepositories(): () => Promise<void> {
+ return useStateStore((state) => state.fetchGithubRepositories);
+}
+
export function useMode(): "edit" | "deploy" {
return useStateStore((state) => state.mode);
}
@@ -851,6 +912,29 @@
}
}
}
+
+ const fetchGithubRepositories = async () => {
+ const { githubService, projectId } = get();
+ if (!githubService || !projectId) {
+ set({
+ githubRepositories: [],
+ githubRepositoriesError: "GitHub service or Project ID not available.",
+ githubRepositoriesLoading: false,
+ });
+ return;
+ }
+
+ set({ githubRepositoriesLoading: true, githubRepositoriesError: null });
+ try {
+ const repos = await githubService.getRepositories();
+ set({ githubRepositories: repos, githubRepositoriesLoading: false });
+ } catch (error) {
+ console.error("Failed to fetch GitHub repositories in store:", error);
+ const errorMessage = error instanceof Error ? error.message : "Unknown error fetching repositories";
+ set({ githubRepositories: [], githubRepositoriesError: errorMessage, githubRepositoriesLoading: false });
+ }
+ };
+
return {
projectId: undefined,
mode: "edit",
@@ -873,6 +957,9 @@
zoom: 1,
},
githubService: null,
+ githubRepositories: [],
+ githubRepositoriesLoading: false,
+ githubRepositoriesError: null,
setViewport: (viewport) => {
const { viewport: vp } = get();
if (
@@ -970,13 +1057,30 @@
} catch (error) {
console.error("Failed to fetch integrations:", error);
} finally {
- if (JSON.stringify(get().env) !== JSON.stringify(env)) {
+ const oldEnv = get().env;
+ const oldGithubIntegrationStatus = oldEnv.integrations.github;
+ if (JSON.stringify(oldEnv) !== JSON.stringify(env)) {
set({ env });
injectNetworkNodes();
+ let ghService = null;
if (env.integrations.github) {
- set({ githubService: new GitHubServiceImpl(projectId!) });
- } else {
- set({ githubService: null });
+ ghService = new GitHubServiceImpl(projectId!);
+ }
+ if (get().githubService !== ghService || (ghService && !get().githubService)) {
+ set({ githubService: ghService });
+ }
+ if (
+ ghService &&
+ (oldGithubIntegrationStatus !== env.integrations.github || !oldEnv.integrations.github)
+ ) {
+ get().fetchGithubRepositories();
+ }
+ if (!env.integrations.github) {
+ set({
+ githubRepositories: [],
+ githubRepositoriesError: null,
+ githubRepositoriesLoading: false,
+ });
}
}
}
@@ -992,10 +1096,13 @@
stopRefreshEnvInterval();
set({
projectId,
+ githubRepositories: [],
+ githubRepositoriesLoading: false,
+ githubRepositoriesError: null,
});
if (projectId) {
await get().refreshEnv();
- if (get().env.deployKey) {
+ if (get().env.instanceId) {
set({ mode: "deploy" });
} else {
set({ mode: "edit" });
@@ -1008,8 +1115,12 @@
edges: [],
env: defaultEnv,
githubService: null,
+ githubRepositories: [],
+ githubRepositoriesLoading: false,
+ githubRepositoriesError: null,
});
}
},
+ fetchGithubRepositories: fetchGithubRepositories,
};
});