diff --git a/README.md b/README.md index ace2efc..605c4b1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,63 @@ -# cameleer-deploy-demo +# Cameleer Deploy Demo -Demo prototype: upload Camel JARs, build containers with agent injection, deploy to K8s with full observability \ No newline at end of file +Demo prototype: upload Camel JARs, build containers with automatic agent injection, deploy to K8s with full observability via cameleer3-server. + +## Architecture + +``` +Browser (React + @cameleer/design-system) + └─ Deploy Service (Spring Boot 3, port 8082) + ├─ docker build (inject cameleer3 agent) + ├─ docker push (Gitea registry) + └─ kubectl apply (k3s cluster) + └─ Agent auto-registers → cameleer3-server +``` + +## Prerequisites + +- Java 21 +- Node.js 22+ +- Docker (for building images) +- kubectl configured for your k3s cluster +- cameleer3-server running (for agent registration) + +## Quick Start + +### Backend + +```bash +mvn clean package -DskipTests +java -jar target/cameleer-deploy-demo-0.1.0-SNAPSHOT.jar \ + --cameleer.deploy.server-url=http://cameleer3-server.cameleer.svc:8081 \ + --cameleer.deploy.bootstrap-token=YOUR_TOKEN \ + --cameleer.deploy.cameleer-server-ui=http://localhost:8081 +``` + +### Frontend + +```bash +cd ui +npm install +npm run dev +``` + +Open http://localhost:5174 + +## Configuration + +| Env Var | Default | Description | +|---------|---------|-------------| +| `CAMELEER_SERVER_URL` | `http://cameleer3-server.cameleer.svc:8081` | cameleer3-server URL for agent registration | +| `CAMELEER_BOOTSTRAP_TOKEN` | `changeme` | Bootstrap token for agent auth | +| `CAMELEER_REGISTRY` | `gitea.siegeln.net/cameleer/demo-apps` | Container registry prefix | +| `CAMELEER_AGENT_MAVEN_URL` | (Gitea Maven) | URL for cameleer3-agent JAR | +| `CAMELEER_DEMO_NAMESPACE` | `cameleer-demo` | K8s namespace for deployed apps | +| `CAMELEER_SERVER_UI` | `http://localhost:8081` | cameleer3-server UI URL (for links) | + +## Demo Flow + +1. Open the UI +2. Click "Deploy Application" +3. Upload a Camel JAR, configure resources and env vars +4. Watch the build log stream +5. Open cameleer3-server — your app appears with full observability diff --git a/ui/.npmrc b/ui/.npmrc new file mode 100644 index 0000000..df2d130 --- /dev/null +++ b/ui/.npmrc @@ -0,0 +1 @@ +@cameleer:registry=https://gitea.siegeln.net/api/packages/cameleer/npm/ diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..aa2c9cf --- /dev/null +++ b/ui/index.html @@ -0,0 +1,13 @@ + + + + + + + Cameleer Deploy + + +
+ + + diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 0000000..b6b8ad3 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,1093 @@ +{ + "name": "cameleer-deploy-demo-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cameleer-deploy-demo-ui", + "version": "0.1.0", + "dependencies": { + "@cameleer/design-system": "^0.1.26", + "@tanstack/react-query": "^5.90.21", + "lucide-react": "^1.7.0", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.0", + "typescript": "~5.9.3", + "vite": "^8.0.0" + } + }, + "node_modules/@cameleer/design-system": { + "version": "0.1.26", + "resolved": "https://gitea.siegeln.net/api/packages/cameleer/npm/%40cameleer%2Fdesign-system/-/0.1.26/design-system-0.1.26.tgz", + "integrity": "sha512-lu76c4F1Vz6fdXLjv434zh5jm61uSrkyNwmRbLtrs3tZbjovL8JWwj0Ao6akBJ84axzUt3GCbugVJwiLZvUKdA==", + "dependencies": { + "lucide-react": "^1.7.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.96.1.tgz", + "integrity": "sha512-u1yBgtavSy+N8wgtW3PiER6UpxcplMje65yXnnVgiHTqiMwLlxiw4WvQDrXyn+UD6lnn8kHaxmerJUzQcV/MMg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.96.1.tgz", + "integrity": "sha512-2X7KYK5KKWUKGeWCVcqxXAkYefJtrKB7tSKWgeG++b0H6BRHxQaLSSi8AxcgjmUnnosHuh9WsFZqvE16P1WCzA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.96.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-react": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", + "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-router": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", + "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz", + "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 0000000..997aa73 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,25 @@ +{ + "name": "cameleer-deploy-demo-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -p tsconfig.app.json --noEmit && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@cameleer/design-system": "^0.1.26", + "@tanstack/react-query": "^5.90.21", + "lucide-react": "^1.7.0", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.0", + "typescript": "~5.9.3", + "vite": "^8.0.0" + } +} diff --git a/ui/public/favicon.svg b/ui/public/favicon.svg new file mode 100644 index 0000000..a5e73c3 --- /dev/null +++ b/ui/public/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/App.module.css b/ui/src/App.module.css new file mode 100644 index 0000000..02b65da --- /dev/null +++ b/ui/src/App.module.css @@ -0,0 +1,43 @@ +.app { + display: flex; + flex-direction: column; + height: 100vh; + background: var(--bg-base); + color: var(--text-primary); +} + +.header { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1.5rem; + border-bottom: 1px solid var(--border-subtle); + background: var(--bg-surface); +} + +.logo { + width: 28px; + height: 24px; +} + +.title { + font-size: 1rem; + font-weight: 600; + margin: 0; +} + +.demoBadge { + font-size: 9px; + font-weight: 700; + letter-spacing: 1px; + padding: 2px 6px; + border-radius: var(--radius-sm); + background: var(--amber); + color: var(--bg-base); +} + +.main { + flex: 1; + overflow: auto; + padding: 1.5rem; +} diff --git a/ui/src/App.tsx b/ui/src/App.tsx new file mode 100644 index 0000000..be00543 --- /dev/null +++ b/ui/src/App.tsx @@ -0,0 +1,17 @@ +import { Dashboard } from './Dashboard'; +import styles from './App.module.css'; + +export function App() { + return ( +
+
+ +

Cameleer Deploy

+ DEMO +
+
+ +
+
+ ); +} diff --git a/ui/src/Dashboard.module.css b/ui/src/Dashboard.module.css new file mode 100644 index 0000000..04aa412 --- /dev/null +++ b/ui/src/Dashboard.module.css @@ -0,0 +1,113 @@ +.toolbar { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1rem; +} + +.heading { + font-size: 1.125rem; + font-weight: 600; + margin: 0; +} + +.center { + display: flex; + justify-content: center; + padding: 4rem; +} + +.empty { + text-align: center; + padding: 4rem 2rem; + color: var(--text-muted); + font-size: 0.875rem; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 0.8125rem; +} + +.table th { + text-align: left; + padding: 0.5rem 0.75rem; + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); + border-bottom: 1px solid var(--border-subtle); +} + +.table td { + padding: 0.625rem 0.75rem; + border-bottom: 1px solid var(--border-subtle); + vertical-align: middle; +} + +.table tr:hover td { + background: var(--bg-hover); +} + +.appLink { + display: inline-flex; + align-items: center; + gap: 4px; + color: var(--text-primary); + text-decoration: none; + font-weight: 500; +} + +.appLink:hover { + color: var(--accent); +} + +.resources { + display: flex; + flex-direction: column; + gap: 2px; + font-size: 0.75rem; + color: var(--text-secondary); +} + +.age { + color: var(--text-muted); + font-size: 0.75rem; + white-space: nowrap; +} + +.actions { + display: flex; + gap: 0.25rem; +} + +.iconBtn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + border-radius: var(--radius-sm); + background: none; + color: var(--text-muted); + cursor: pointer; +} + +.iconBtn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.errorMsg { + display: block; + font-size: 0.6875rem; + color: var(--error); + margin-top: 2px; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/ui/src/Dashboard.tsx b/ui/src/Dashboard.tsx new file mode 100644 index 0000000..64b0a35 --- /dev/null +++ b/ui/src/Dashboard.tsx @@ -0,0 +1,150 @@ +import { useState } from 'react'; +import { Button, Badge, MonoText, ConfirmDialog, Spinner, useToast } from '@cameleer/design-system'; +import { Upload, Trash2, ExternalLink, Terminal } from 'lucide-react'; +import { useApps, useUndeployApp, useConfig } from './api'; +import type { DeployedApp } from './api'; +import { DeployDialog } from './DeployDialog'; +import { LogDialog } from './LogDialog'; +import styles from './Dashboard.module.css'; + +type BadgeColor = 'primary' | 'success' | 'warning' | 'error' | 'running' | 'auto'; + +function statusColor(status: DeployedApp['status']): BadgeColor { + switch (status) { + case 'RUNNING': return 'success'; + case 'BUILDING': + case 'PUSHING': + case 'DEPLOYING': + case 'PENDING': return 'warning'; + case 'FAILED': return 'error'; + default: return 'auto'; + } +} + +function formatAge(iso: string): string { + const ms = Date.now() - new Date(iso).getTime(); + if (ms < 60_000) return 'just now'; + if (ms < 3600_000) return `${Math.floor(ms / 60_000)}m ago`; + if (ms < 86_400_000) return `${Math.floor(ms / 3600_000)}h ago`; + return `${Math.floor(ms / 86_400_000)}d ago`; +} + +export function Dashboard() { + const { data: apps, isLoading } = useApps(); + const { data: config } = useConfig(); + const undeploy = useUndeployApp(); + const { toast } = useToast(); + + const [deployOpen, setDeployOpen] = useState(false); + const [deleteTarget, setDeleteTarget] = useState(null); + const [logTarget, setLogTarget] = useState(null); + + const cameleerUi = config?.cameleerServerUi ?? 'http://localhost:8081'; + + return ( +
+
+

Deployed Applications

+ +
+ + {isLoading ? ( +
+ ) : !apps || apps.length === 0 ? ( +
+ No applications deployed yet. Click "Deploy Application" to get started. +
+ ) : ( + + + + + + + + + + + + + {apps.map((app) => ( + + + + + + + + + ))} + +
NameStatusImageResourcesAgeActions
+ + {app.name} + + + + + {app.statusMessage && app.status === 'FAILED' && ( + {app.statusMessage} + )} + {app.imageTag || '-'} + {app.resources.cpuLimit} CPU + {app.resources.memoryLimit} RAM + {formatAge(app.createdAt)} + + +
+ )} + + setDeployOpen(false)} /> + + setLogTarget(null)} /> + + setDeleteTarget(null)} + onConfirm={() => { + if (deleteTarget) { + undeploy.mutate(deleteTarget, { + onSuccess: () => { + toast({ title: 'App deleted', description: `${deleteTarget} has been undeployed`, variant: 'success' }); + setDeleteTarget(null); + }, + onError: (err) => { + toast({ title: 'Delete failed', description: err.message, variant: 'error' }); + setDeleteTarget(null); + }, + }); + } + }} + title="Delete application?" + message={`This will delete the deployment "${deleteTarget}" from the cluster. This cannot be undone.`} + confirmText={deleteTarget ?? ''} + confirmLabel="Delete" + variant="danger" + loading={undeploy.isPending} + /> +
+ ); +} diff --git a/ui/src/DeployDialog.module.css b/ui/src/DeployDialog.module.css new file mode 100644 index 0000000..d14bd94 --- /dev/null +++ b/ui/src/DeployDialog.module.css @@ -0,0 +1,165 @@ +.overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.dialog { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + width: 520px; + max-height: 85vh; + display: flex; + flex-direction: column; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +} + +.dialogHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--border-subtle); +} + +.dialogHeader h3 { + margin: 0; + font-size: 0.9375rem; + font-weight: 600; +} + +.closeBtn { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 4px; + border-radius: var(--radius-sm); +} + +.closeBtn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.body { + padding: 1.25rem; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.field { + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.label { + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); +} + +.hint { + font-size: 0.6875rem; + color: var(--error); +} + +.row { + display: flex; + gap: 0.5rem; +} + +.dropzone { + border: 2px dashed var(--border); + border-radius: var(--radius-md); + padding: 1.5rem; + text-align: center; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; +} + +.dropzone:hover { + border-color: var(--accent); + background: var(--bg-hover); +} + +.dropzoneHasFile { + border-style: solid; + border-color: var(--success); + background: var(--bg-hover); +} + +.dropText { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + color: var(--text-muted); + font-size: 0.8125rem; +} + +.fileName { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 0.8125rem; + color: var(--success); + font-weight: 500; +} + +.envRow { + display: flex; + gap: 0.375rem; + align-items: center; +} + +.removeBtn { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 4px; + flex-shrink: 0; +} + +.removeBtn:hover { + color: var(--error); +} + +.addEnvBtn { + display: inline-flex; + align-items: center; + gap: 4px; + background: none; + border: 1px dashed var(--border); + border-radius: var(--radius-sm); + padding: 0.375rem 0.75rem; + font-size: 0.75rem; + color: var(--text-muted); + cursor: pointer; + align-self: flex-start; +} + +.addEnvBtn:hover { + border-color: var(--accent); + color: var(--text-primary); +} + +.footer { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + padding: 1rem 1.25rem; + border-top: 1px solid var(--border-subtle); +} diff --git a/ui/src/DeployDialog.tsx b/ui/src/DeployDialog.tsx new file mode 100644 index 0000000..e060d84 --- /dev/null +++ b/ui/src/DeployDialog.tsx @@ -0,0 +1,193 @@ +import { useState, useCallback, useRef } from 'react'; +import { Button, Input, useToast } from '@cameleer/design-system'; +import { Upload, Plus, X } from 'lucide-react'; +import { useDeployApp } from './api'; +import styles from './DeployDialog.module.css'; + +interface DeployDialogProps { + open: boolean; + onClose: () => void; +} + +export function DeployDialog({ open, onClose }: DeployDialogProps) { + const { toast } = useToast(); + const deploy = useDeployApp(); + const fileRef = useRef(null); + + const [name, setName] = useState(''); + const [file, setFile] = useState(null); + const [cpuRequest, setCpuRequest] = useState('250m'); + const [memoryRequest, setMemoryRequest] = useState('256Mi'); + const [cpuLimit, setCpuLimit] = useState('500m'); + const [memoryLimit, setMemoryLimit] = useState('512Mi'); + const [envVars, setEnvVars] = useState<{ key: string; value: string }[]>([]); + + const reset = useCallback(() => { + setName(''); + setFile(null); + setCpuRequest('250m'); + setMemoryRequest('256Mi'); + setCpuLimit('500m'); + setMemoryLimit('512Mi'); + setEnvVars([]); + }, []); + + const handleClose = useCallback(() => { + reset(); + onClose(); + }, [reset, onClose]); + + const nameValid = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(name); + + const handleDeploy = useCallback(() => { + if (!file || !nameValid) return; + + const envMap: Record = {}; + for (const { key, value } of envVars) { + if (key.trim()) envMap[key.trim()] = value; + } + + deploy.mutate( + { name, jar: file, cpuRequest, memoryRequest, cpuLimit, memoryLimit, envVars: envMap }, + { + onSuccess: () => { + toast({ title: 'Deploy started', description: `${name} is building...`, variant: 'success' }); + handleClose(); + }, + onError: (err) => { + toast({ title: 'Deploy failed', description: err.message, variant: 'error', duration: 86_400_000 }); + }, + }, + ); + }, [name, file, nameValid, cpuRequest, memoryRequest, cpuLimit, memoryLimit, envVars, deploy, toast, handleClose]); + + const handleDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + const f = e.dataTransfer.files[0]; + if (f && f.name.endsWith('.jar')) setFile(f); + }, []); + + if (!open) return null; + + return ( +
+
e.stopPropagation()}> +
+

Deploy Application

+ +
+ +
+ {/* App name */} +
+ + setName(e.target.value.toLowerCase())} + placeholder="my-camel-app" + /> + {name && !nameValid && ( + Lowercase letters, numbers, and hyphens only + )} +
+ + {/* JAR upload */} +
+ +
e.preventDefault()} + onDrop={handleDrop} + onClick={() => fileRef.current?.click()} + > + { + const f = e.target.files?.[0]; + if (f) setFile(f); + }} + /> + {file ? ( + + {file.name} ({(file.size / 1024 / 1024).toFixed(1)} MB) + + ) : ( + + Drop JAR file here or click to browse + + )} +
+
+ + {/* Resource limits */} +
+ +
+ setCpuRequest(e.target.value)} placeholder="CPU" /> + setMemoryRequest(e.target.value)} placeholder="Memory" /> +
+
+
+ +
+ setCpuLimit(e.target.value)} placeholder="CPU" /> + setMemoryLimit(e.target.value)} placeholder="Memory" /> +
+
+ + {/* Env vars */} +
+ + {envVars.map((env, i) => ( +
+ { + const next = [...envVars]; + next[i] = { ...next[i], key: e.target.value }; + setEnvVars(next); + }} + placeholder="KEY" + /> + { + const next = [...envVars]; + next[i] = { ...next[i], value: e.target.value }; + setEnvVars(next); + }} + placeholder="value" + /> + +
+ ))} + +
+
+ +
+ + +
+
+
+ ); +} diff --git a/ui/src/LogDialog.module.css b/ui/src/LogDialog.module.css new file mode 100644 index 0000000..f211b91 --- /dev/null +++ b/ui/src/LogDialog.module.css @@ -0,0 +1,78 @@ +.overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.dialog { + background: var(--bg-base); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + width: 700px; + max-height: 80vh; + display: flex; + flex-direction: column; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--border-subtle); + background: var(--bg-surface); + border-radius: var(--radius-lg) var(--radius-lg) 0 0; +} + +.header h3 { + margin: 0; + font-size: 0.875rem; + font-weight: 600; +} + +.closeBtn { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 4px; + border-radius: var(--radius-sm); +} + +.closeBtn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} + +.logArea { + flex: 1; + overflow-y: auto; + padding: 0.75rem 1rem; + font-family: var(--font-mono, 'JetBrains Mono', monospace); + font-size: 0.6875rem; + line-height: 1.5; +} + +.logLine { + white-space: pre-wrap; + word-break: break-all; + color: var(--text-secondary); + padding: 1px 0; +} + +.center { + display: flex; + justify-content: center; + padding: 2rem; +} + +.empty { + text-align: center; + color: var(--text-muted); + padding: 2rem; +} diff --git a/ui/src/LogDialog.tsx b/ui/src/LogDialog.tsx new file mode 100644 index 0000000..7bc10b5 --- /dev/null +++ b/ui/src/LogDialog.tsx @@ -0,0 +1,46 @@ +import { useEffect, useRef } from 'react'; +import { X } from 'lucide-react'; +import { Spinner } from '@cameleer/design-system'; +import { useAppLogs } from './api'; +import styles from './LogDialog.module.css'; + +interface LogDialogProps { + appName: string | null; + onClose: () => void; +} + +export function LogDialog({ appName, onClose }: LogDialogProps) { + const { data: logs, isLoading } = useAppLogs(appName); + const endRef = useRef(null); + + useEffect(() => { + endRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [logs]); + + if (!appName) return null; + + return ( +
+
e.stopPropagation()}> +
+

Build Log: {appName}

+ +
+
+ {isLoading ? ( +
+ ) : !logs || logs.length === 0 ? ( +
No build logs available
+ ) : ( + <> + {logs.map((line, i) => ( +
{line}
+ ))} +
+ + )} +
+
+
+ ); +} diff --git a/ui/src/api.ts b/ui/src/api.ts new file mode 100644 index 0000000..3f219df --- /dev/null +++ b/ui/src/api.ts @@ -0,0 +1,85 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; + +export interface DeployedApp { + name: string; + imageName: string; + imageTag: string; + status: 'BUILDING' | 'PUSHING' | 'DEPLOYING' | 'RUNNING' | 'PENDING' | 'FAILED' | 'DELETED'; + statusMessage: string | null; + resources: { + cpuRequest: string; + memoryRequest: string; + cpuLimit: string; + memoryLimit: string; + }; + envVars: Record; + createdAt: string; +} + +async function apiFetch(path: string, init?: RequestInit): Promise { + const res = await fetch(`/api/apps${path}`, init); + if (!res.ok) throw new Error(`API error: ${res.status}`); + if (res.status === 204) return undefined as T; + return res.json(); +} + +export function useApps() { + return useQuery({ + queryKey: ['apps'], + queryFn: () => apiFetch(''), + refetchInterval: 5000, + }); +} + +export function useAppLogs(name: string | null) { + return useQuery({ + queryKey: ['apps', name, 'logs'], + queryFn: () => apiFetch(`/${name}/logs`), + enabled: !!name, + refetchInterval: 2000, + }); +} + +export function useDeployApp() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (data: { + name: string; + jar: File; + cpuRequest: string; + memoryRequest: string; + cpuLimit: string; + memoryLimit: string; + envVars: Record; + }) => { + const form = new FormData(); + form.append('name', data.name); + form.append('jar', data.jar); + form.append('cpuRequest', data.cpuRequest); + form.append('memoryRequest', data.memoryRequest); + form.append('cpuLimit', data.cpuLimit); + form.append('memoryLimit', data.memoryLimit); + if (Object.keys(data.envVars).length > 0) { + form.append('envVars', JSON.stringify(data.envVars)); + } + return apiFetch('', { method: 'POST', body: form }); + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ['apps'] }), + }); +} + +export function useUndeployApp() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (name: string) => apiFetch(`/${name}`, { method: 'DELETE' }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['apps'] }), + }); +} + +export function useConfig() { + return useQuery({ + queryKey: ['config'], + queryFn: () => apiFetch<{ cameleerServerUi: string }>('/config'), + staleTime: Infinity, + }); +} diff --git a/ui/src/main.tsx b/ui/src/main.tsx new file mode 100644 index 0000000..b3d7bf2 --- /dev/null +++ b/ui/src/main.tsx @@ -0,0 +1,22 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ToastProvider } from '@cameleer/design-system'; +import '@cameleer/design-system/style.css'; +import { App } from './App'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { refetchOnWindowFocus: false, retry: 1 }, + }, +}); + +createRoot(document.getElementById('root')!).render( + + + + + + + , +); diff --git a/ui/src/vite-env.d.ts b/ui/src/vite-env.d.ts new file mode 100644 index 0000000..40abd8c --- /dev/null +++ b/ui/src/vite-env.d.ts @@ -0,0 +1,6 @@ +/// + +declare module '*.module.css' { + const classes: { readonly [key: string]: string }; + export default classes; +} diff --git a/ui/tsconfig.app.json b/ui/tsconfig.app.json new file mode 100644 index 0000000..55f42a8 --- /dev/null +++ b/ui/tsconfig.app.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2023", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 0000000..27d2052 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,6 @@ +{ + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/ui/tsconfig.node.json b/ui/tsconfig.node.json new file mode 100644 index 0000000..eff9257 --- /dev/null +++ b/ui/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true + }, + "include": ["vite.config.ts"] +} diff --git a/ui/vite.config.ts b/ui/vite.config.ts new file mode 100644 index 0000000..ff9ec3a --- /dev/null +++ b/ui/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +const apiTarget = process.env.VITE_API_TARGET || 'http://localhost:8082'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5174, + proxy: { + '/api/': { + target: apiTarget, + changeOrigin: true, + secure: false, + }, + }, + }, + build: { + outDir: 'dist', + }, +});