From 90f9e24caa4f162392767ad4f98be64248051f15 Mon Sep 17 00:00:00 2001 From: Sergey Pronin Date: Wed, 23 Sep 2026 11:48:38 +0300 Subject: [PATCH] Plugin UI alignment Signed-off-by: Sergey Pronin --- package-lock.json | 1010 +++++++++++++++++++++++++++++++++++++++++++- package.json | 7 +- src/api.ts | 170 ++++++++ src/components.tsx | 877 ++++++++++++++++++++++++++++++++++++++ src/main.tsx | 987 +------------------------------------------ vite.config.ts | 15 +- 6 files changed, 2085 insertions(+), 981 deletions(-) create mode 100644 src/api.ts create mode 100644 src/components.tsx diff --git a/package-lock.json b/package-lock.json index 1914084..7f3c704 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,17 +8,215 @@ "name": "@openeverest/plugin-mongodb-explorer", "version": "0.1.0", "dependencies": { + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.11.0", + "@emotion/styled": "^11.11.0", + "@mui/material": "^7.3.0", + "@openeverest/ui-lib": "file:../openeverest/ui/packages/plugin-ui-lib", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@openeverest/plugin-sdk": "^0.3.0", + "@openeverest/plugin-sdk": "file:../openeverest/ui/packages/plugin-sdk", "@types/react": "^18.3.28", "@vitejs/plugin-react-swc": "^4.3.3", "typescript": "^5.2.2", "vite": "^8.2.0" } }, + "../openeverest/ui/packages/plugin-sdk": { + "name": "@openeverest/plugin-sdk", + "version": "0.3.0", + "dev": true, + "devDependencies": { + "@percona/tsconfig": "workspace:*", + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^11.0.0", + "@types/react": "^18.2.0", + "react": "^18.2.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.0", + "rollup-plugin-peer-deps-external": "^2.2.4", + "typescript": "^5.2.2" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "../openeverest/ui/packages/plugin-ui-lib": { + "name": "@openeverest/ui-lib", + "version": "0.1.2", + "dependencies": { + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.11.0", + "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^7.3.0", + "@mui/material": "^7.3.0" + }, + "devDependencies": { + "@percona/tsconfig": "workspace:*", + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^11.0.0", + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.0.0", + "@types/react": "^18.2.0", + "@vitejs/plugin-react-swc": "^4.2.3", + "jsdom": "^28.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rollup": "^4.22.4", + "rollup-plugin-dts": "^6.1.0", + "typescript": "^5.2.2", + "vitest": "^4.0.18" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "2.0.0-alpha.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", @@ -53,6 +251,394 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.11.tgz", + "integrity": "sha512-a7I/b/nBTdXYz2cOSlEmkQ9WWE1x8FHpqMhFPp+Y1VPFxcOw91G5ELOHARQAGSPy5V+UCgJua6K/1x70bAtQPw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/material": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.11.tgz", + "integrity": "sha512-yq8bPc3LxOwKRWpcjRgDkYFmpM6aKlARfESTmOQcvLYFeJwtHte2tw6hJDrb8sk8wcvpDprHEHVaoUU0MslIkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/core-downloads-tracker": "^7.3.11", + "@mui/system": "^7.3.11", + "@mui/types": "^7.4.12", + "@mui/utils": "^7.3.11", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.3", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.3.11", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.11.tgz", + "integrity": "sha512-9B+YKms0fRHbNrqp9tOT/DNbNnU5gyvJ1o3qAGXfq8GmZcbJnE3At9x07Zr/o0pkhzg4aDdwXVqe4+AcgtOCPA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/utils": "^7.3.11", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.3.10", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.10.tgz", + "integrity": "sha512-WxE9SiF8xskAQqGjsp0poXCkCqsoXFEsSr0HBXfApmGHR+DBnXRp+z46Vsltg4gpPM4Z96DeAQRpeAOnhNg7Ng==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.11.tgz", + "integrity": "sha512-7izwGWdNawAKpBKcRlx7f2gFnAAjmASBWvMcyX4YYEeLOFsbfGRbUYGInvnAcUeql3rPxI7F9Ft4oY2OLRz44g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/private-theming": "^7.3.11", + "@mui/styled-engine": "^7.3.10", + "@mui/types": "^7.4.12", + "@mui/utils": "^7.3.11", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.12", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.12.tgz", + "integrity": "sha512-iKNAF2u9PzSIj40CjvKJWxFXJo122jXVdrmdh0hMYd+FR+NuJMkr/L88XwWLCRiJ5P1j+uyac25+Kp6YC4hu6w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.3.11", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.11.tgz", + "integrity": "sha512-XTjGnifwteg71/ij+0e7Y7d+hwyntMYP5wPoA/g2drdGH+Flkvjwy0OfrVpKBbaOvofq4zU/LIyUZyKgmWu18g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "@mui/types": "^7.4.12", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", @@ -76,13 +662,12 @@ } }, "node_modules/@openeverest/plugin-sdk": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@openeverest/plugin-sdk/-/plugin-sdk-0.3.0.tgz", - "integrity": "sha512-4A0b1SABKR7kj+2jz5fTaBWgsDrRxwNJ0npBZC4ZDrSieiUYYb4K8+HyqgfgcWvMc0niwzv6skk3FxDQAqQ0Qw==", - "dev": true, - "peerDependencies": { - "react": "^18.0.0" - } + "resolved": "../openeverest/ui/packages/plugin-sdk", + "link": true + }, + "node_modules/@openeverest/ui-lib": { + "resolved": "../openeverest/ui/packages/plugin-ui-lib", + "link": true }, "node_modules/@oxc-project/types": { "version": "0.142.0", @@ -94,6 +679,16 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz", @@ -664,24 +1259,37 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", - "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@vitejs/plugin-react-swc": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.3.3.tgz", @@ -699,13 +1307,93 @@ "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -716,6 +1404,46 @@ "node": ">=8" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -734,6 +1462,12 @@ } } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -749,12 +1483,103 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -1028,6 +1853,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1040,6 +1871,12 @@ "loose-envify": "cli.js" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.18", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", @@ -1059,11 +1896,64 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "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": { @@ -1108,6 +1998,23 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -1133,6 +2040,58 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.0.tgz", + "integrity": "sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA==", + "license": "MIT" + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/rolldown": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", @@ -1176,6 +2135,15 @@ "loose-envify": "^1.1.0" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1186,6 +2154,24 @@ "node": ">=0.10.0" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", diff --git a/package.json b/package.json index 0cd2799..21e413d 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,16 @@ "preview": "vite preview --port 3001" }, "dependencies": { + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.11.0", + "@emotion/styled": "^11.11.0", + "@mui/material": "^7.3.0", + "@openeverest/ui-lib": "file:../openeverest/ui/packages/plugin-ui-lib", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { - "@openeverest/plugin-sdk": "^0.3.0", + "@openeverest/plugin-sdk": "file:../openeverest/ui/packages/plugin-sdk", "@types/react": "^18.3.28", "@vitejs/plugin-react-swc": "^4.3.3", "typescript": "^5.2.2", diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..825702a --- /dev/null +++ b/src/api.ts @@ -0,0 +1,170 @@ +import type { PluginApi } from '@openeverest/plugin-sdk'; + +// The host provides fetch (cluster-scoped, auth-attached) and the CSP nonce at +// register() time; both are captured here so the rest of the plugin stays +// framework-agnostic. +let pluginFetch: PluginApi['fetch']; +let cssNonce = ''; + +export function configurePlugin(api: PluginApi): void { + pluginFetch = api.fetch.bind(api); + cssNonce = api.cssNonce; +} + +export function getCssNonce(): string { + return cssNonce; +} + +// Unwraps the backend's nested error envelopes (`{"error":"…"}` possibly +// wrapping a downstream `{"message":"…"}`) into a single human-readable +// sentence, so the UI never shows raw JSON. +function extractErrorMessage(raw: string): string { + let msg = (raw ?? '').trim(); + if (!msg) return ''; + + const unwrap = (s: string): string => { + try { + const parsed = JSON.parse(s); + if (parsed && typeof parsed === 'object') { + const inner = + (parsed as Record).message ?? + (parsed as Record).error; + if (typeof inner === 'string') return inner.trim(); + } + } catch { + // Not JSON — fall through. + } + return s; + }; + + for (let i = 0; i < 3; i++) { + const next = unwrap(msg); + if (next === msg) break; + msg = next; + } + const embedded = msg.match(/\{[^{}]*"message"\s*:\s*"([^"]+)"[^{}]*\}/); + if (embedded) return embedded[1].trim(); + + return msg; +} + +// Uses the host proxy so the backend receives a valid session and the +// X-Everest-User JWT. +async function apiFetch(path: string, opts?: RequestInit) { + const res = await pluginFetch(`/api${path}`, opts); + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(extractErrorMessage(text) || `HTTP ${res.status}`); + } + return res.json(); +} + +export interface MongoInstanceTarget { + k8sCluster: string; + namespace: string; + name: string; + provider: string; +} + +export interface OverviewMember { + name: string; + stateStr: string; + health: number; + uptimeSeconds: number; + lagSeconds: number; +} + +export interface ClusterOverview { + sharded: boolean; + version?: string; + replicaSet?: { set: string; members: OverviewMember[] }; + connections?: { current?: number; available?: number }; + opcounters?: Record; + shards?: unknown[]; + balancer?: { mode?: string; inBalancerRound?: boolean }; +} + +export function instanceKey(instance: MongoInstanceTarget): string { + return `${instance.k8sCluster}/${instance.namespace}/${instance.name}`; +} + +export async function fetchMongoInstances(): Promise { + const data = await apiFetch('/instances'); + return Array.isArray(data.instances) ? data.instances : []; +} + +export async function fetchDatabases( + k8sCluster: string, + instance: string, + namespace: string +): Promise { + const data = await apiFetch( + `/databases?k8sCluster=${encodeURIComponent(k8sCluster)}&cluster=${encodeURIComponent(instance)}&namespace=${encodeURIComponent(namespace)}` + ); + return data.databases ?? []; +} + +export async function fetchCollections( + k8sCluster: string, + instance: string, + namespace: string, + db: string +): Promise { + const data = await apiFetch( + `/databases/${encodeURIComponent(db)}/collections?k8sCluster=${encodeURIComponent(k8sCluster)}&cluster=${encodeURIComponent(instance)}&namespace=${encodeURIComponent(namespace)}` + ); + return data.collections ?? []; +} + +export async function runQuery( + k8sCluster: string, + cluster: string, + namespace: string, + db: string, + collection: string, + filterStr: string, + projectionStr: string, + limit: number +): Promise[]> { + let filter: unknown; + try { + filter = JSON.parse(filterStr || '{}'); + } catch { + throw new Error('Filter is not valid JSON.'); + } + + let projection: unknown = undefined; + if (projectionStr.trim()) { + try { + projection = JSON.parse(projectionStr); + } catch { + throw new Error('Projection is not valid JSON.'); + } + } + + const data = await apiFetch('/query', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + k8sCluster, + cluster, + namespace, + db, + collection, + filter, + projection, + limit, + }), + }); + return data.documents ?? []; +} + +export async function fetchOverview( + k8sCluster: string, + cluster: string, + namespace: string +): Promise { + return apiFetch( + `/overview?k8sCluster=${encodeURIComponent(k8sCluster)}&cluster=${encodeURIComponent(cluster)}&namespace=${encodeURIComponent(namespace)}` + ); +} diff --git a/src/components.tsx b/src/components.tsx new file mode 100644 index 0000000..582e6fb --- /dev/null +++ b/src/components.tsx @@ -0,0 +1,877 @@ +import { useEffect, useMemo, useState } from 'react'; +import type { + ClusterDetailTabProps, + PluginRouteProps, +} from '@openeverest/plugin-sdk'; +import { + Alert, + Box, + Button, + Chip, + CircularProgress, + Collapse, + Dialog, + DialogContent, + DialogTitle, + Divider, + IconButton, + Link, + List, + ListItemButton, + ListItemText, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Tab, + Tabs, + TextField, + Tooltip, + Typography, +} from '@openeverest/ui-lib'; +import { + ClusterOverview, + MongoInstanceTarget, + fetchCollections, + fetchDatabases, + fetchMongoInstances, + fetchOverview, + instanceKey, + runQuery, +} from './api'; + +const MONO = "'Roboto Mono', ui-monospace, SFMono-Regular, Menlo, monospace"; + +function errText(e: unknown): string { + return e instanceof Error ? e.message : String(e); +} + +function formatCellValue(v: unknown): string { + if (v === null || v === undefined) return '—'; + if (typeof v === 'object') return JSON.stringify(v); + return String(v); +} + +// --------------------------------------------------------------------------- +// DatabaseTree — left-panel collection browser +// --------------------------------------------------------------------------- + +interface DatabaseTreeProps { + k8sCluster: string; + cluster: string; + namespace: string; + onSelectCollection: (db: string, collection: string) => void; +} + +const DatabaseTree = ({ + k8sCluster, + cluster, + namespace, + onSelectCollection, +}: DatabaseTreeProps) => { + const [databases, setDatabases] = useState([]); + const [expanded, setExpanded] = useState>(new Set()); + const [collections, setCollections] = useState>({}); + const [loading, setLoading] = useState(true); + const [colLoading, setColLoading] = useState>({}); + const [error, setError] = useState(null); + + useEffect(() => { + setLoading(true); + setError(null); + fetchDatabases(k8sCluster, cluster, namespace) + .then(setDatabases) + .catch((e: unknown) => setError(errText(e))) + .finally(() => setLoading(false)); + }, [k8sCluster, cluster, namespace]); + + const toggleDb = (db: string) => { + const next = new Set(expanded); + if (next.has(db)) { + next.delete(db); + setExpanded(next); + return; + } + next.add(db); + setExpanded(next); + if (!collections[db]) { + setColLoading((prev) => ({ ...prev, [db]: true })); + fetchCollections(k8sCluster, cluster, namespace, db) + .then((cols) => setCollections((prev) => ({ ...prev, [db]: cols }))) + .catch((e: unknown) => setError(errText(e))) + .finally(() => setColLoading((prev) => ({ ...prev, [db]: false }))); + } + }; + + if (loading) { + return ( + + + + Loading databases… + + + ); + } + if (error) { + return ( + + {error} + + ); + } + if (databases.length === 0) { + return ( + + No databases found. + + ); + } + + return ( + + {databases.map((db) => { + const isOpen = expanded.has(db); + const cols = collections[db]; + return ( + + toggleDb(db)} sx={{ borderRadius: 1 }}> + + {isOpen ? '▾' : '▸'} {db} + + + + {colLoading[db] ? ( + + + + ) : (cols ?? []).length === 0 ? ( + + (empty) + + ) : ( + (cols ?? []).map((coll) => ( + onSelectCollection(db, coll)} + sx={{ pl: 4, borderRadius: 1 }} + > + + + )) + )} + + + ); + })} + + ); +}; + +// --------------------------------------------------------------------------- +// QueryPanel — query editor + results viewer +// --------------------------------------------------------------------------- + +interface QueryPanelProps { + k8sCluster: string; + cluster: string; + namespace: string; + initialDb: string | null; + initialCollection: string | null; +} + +const QueryPanel = ({ + k8sCluster, + cluster, + namespace, + initialDb, + initialCollection, +}: QueryPanelProps) => { + const [db, setDb] = useState(initialDb ?? ''); + const [collection, setCollection] = useState(initialCollection ?? ''); + const [filter, setFilter] = useState('{}'); + const [projection, setProjection] = useState(''); + const [limit, setLimit] = useState('20'); + const [results, setResults] = useState[] | null>( + null + ); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [viewMode, setViewMode] = useState<'table' | 'json'>('table'); + const [cellDetail, setCellDetail] = useState<{ + column: string; + value: unknown; + } | null>(null); + + useEffect(() => { + if (initialDb) setDb(initialDb); + }, [initialDb]); + useEffect(() => { + if (initialCollection) setCollection(initialCollection); + }, [initialCollection]); + + const handleRun = () => { + if (!db.trim() || !collection.trim()) { + setError('Enter a database and collection name.'); + return; + } + setLoading(true); + setError(null); + runQuery( + k8sCluster, + cluster, + namespace, + db, + collection, + filter, + projection, + parseInt(limit, 10) || 20 + ) + .then((docs) => setResults(docs)) + .catch((e: unknown) => { + setError(errText(e)); + setResults(null); + }) + .finally(() => setLoading(false)); + }; + + const columns = useMemo( + () => + results && results.length > 0 + ? Array.from( + new Set(results.flatMap((doc) => Object.keys(doc))) + ).slice(0, 12) + : [], + [results] + ); + + return ( + + + setDb(e.target.value)} + /> + setCollection(e.target.value)} + /> + + setFilter(e.target.value)} + sx={{ mb: 1.5, '& textarea': { fontFamily: MONO } }} + /> + + setProjection(e.target.value)} + sx={{ '& input': { fontFamily: MONO } }} + /> + setLimit(e.target.value)} + inputProps={{ min: 1, max: 1000 }} + /> + + + + {error && ( + + {error} + + )} + + {results !== null && ( + + + + {results.length} document(s) returned + + setViewMode(v)} + sx={{ minHeight: 0 }} + > + + + + + + {viewMode === 'json' ? ( + + {JSON.stringify(results, null, 2)} + + ) : results.length === 0 ? ( + + No documents found. + + ) : ( + + + + + {columns.map((col) => ( + + {col} + + ))} + + + + {results.map((doc, i) => ( + + {columns.map((col) => ( + + + setCellDetail({ column: col, value: doc[col] }) + } + sx={{ + fontFamily: MONO, + maxWidth: 220, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + cursor: 'pointer', + }} + > + {formatCellValue(doc[col])} + + + ))} + + ))} + +
+
+ )} +
+ )} + + setCellDetail(null)} + maxWidth="md" + fullWidth + > + + {cellDetail?.column} + + + + {cellDetail && + typeof cellDetail.value === 'object' && + cellDetail.value !== null + ? JSON.stringify(cellDetail.value, null, 2) + : formatCellValue(cellDetail?.value)} + + + +
+ ); +}; + +// --------------------------------------------------------------------------- +// ClusterOverviewStrip — collapsible instance health summary +// --------------------------------------------------------------------------- + +interface ClusterOverviewStripProps { + k8sCluster: string; + cluster: string; + namespace: string; +} + +const ClusterOverviewStrip = ({ + k8sCluster, + cluster, + namespace, +}: ClusterOverviewStripProps) => { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [open, setOpen] = useState(false); + + useEffect(() => { + let active = true; + setData(null); + setError(null); + fetchOverview(k8sCluster, cluster, namespace) + .then((d) => { + if (active) setData(d); + }) + .catch((e: unknown) => { + if (active) setError(errText(e)); + }); + return () => { + active = false; + }; + }, [k8sCluster, cluster, namespace]); + + if (error) { + const pending = /connection details are not yet available|not yet available/i.test( + error + ); + return ( + + {pending + ? 'Cluster status will appear once the instance finishes provisioning.' + : `Cluster status unavailable: ${error}`} + + ); + } + if (!data) { + return ( + + + + Loading cluster status… + + + ); + } + + const rs = data.replicaSet; + const primary = rs?.members.find((m) => m.stateStr === 'PRIMARY'); + const maxLag = + rs && rs.members.length > 0 + ? Math.max(0, ...rs.members.map((m) => m.lagSeconds)) + : 0; + const allHealthy = rs ? rs.members.every((m) => m.health === 1) : true; + + const summaryLine = rs + ? `${rs.set} · ${primary?.stateStr ?? 'no primary'} · ${rs.members.length} members · lag ${maxLag.toFixed(1)}s` + : data.sharded + ? 'sharded cluster' + : 'standalone'; + + return ( + + setOpen((v) => !v)} + > + + {typeof data.connections?.current === 'number' && ( + + · {data.connections.current} conns + + )} + {data.sharded && ( + + · {Array.isArray(data.shards) ? data.shards.length : 0} shards + + )} + + + + {open ? '▾' : '▸'} + + + + + + + + {rs ? ( + + + + {['Member', 'State', 'Health', 'Lag', 'Uptime'].map((h) => ( + {h} + ))} + + + + {rs.members.map((m) => ( + + {m.name} + + {m.stateStr} + + + {m.health === 1 ? 'up' : 'down'} + + + {m.stateStr === 'PRIMARY' + ? '—' + : `${m.lagSeconds.toFixed(1)}s`} + + {`${Math.floor(m.uptimeSeconds / 3600)}h`} + + ))} + +
+ ) : ( + + No replica set information available. + + )} + {(data.opcounters || data.version) && ( + + {data.version && ( + + MongoDB {data.version} + + )} + {data.opcounters && ( + + ops:{' '} + {['insert', 'query', 'update', 'delete'] + .map((k) => `${k[0]}${data.opcounters?.[k] ?? 0}`) + .join(' ')} + + )} + + )} +
+
+
+ ); +}; + +// --------------------------------------------------------------------------- +// MongoExplorer — reusable database/collection explorer +// --------------------------------------------------------------------------- + +interface MongoExplorerProps { + target: MongoInstanceTarget; +} + +const MongoExplorer = ({ target }: MongoExplorerProps) => { + const [selectedDb, setSelectedDb] = useState(null); + const [selectedCollection, setSelectedCollection] = useState( + null + ); + + return ( + + + + Databases + + { + setSelectedDb(db); + setSelectedCollection(coll); + }} + /> + + + + + Query + + + + + ); +}; + +function targetFromClusterDetailProps( + props: ClusterDetailTabProps +): MongoInstanceTarget { + const clusterObj = props.cluster as { + engine?: string; + clusterName?: string; + spec?: { provider?: string; clusterName?: string }; + [k: string]: unknown; + }; + return { + k8sCluster: clusterObj.clusterName ?? clusterObj.spec?.clusterName ?? 'main', + namespace: props.namespace, + name: props.instanceName, + provider: clusterObj.spec?.provider ?? 'percona-server-mongodb', + }; +} + +// --------------------------------------------------------------------------- +// MongoExplorerTab — clusterDetailTab entry point +// --------------------------------------------------------------------------- + +export const MongoExplorerTab = (props: ClusterDetailTabProps) => { + const target = targetFromClusterDetailProps(props); + const clusterObj = props.cluster as { + engine?: string; + spec?: { provider?: string }; + [k: string]: unknown; + }; + const provider = clusterObj.spec?.provider ?? clusterObj.engine; + if (provider && provider !== 'psmdb' && provider !== 'percona-server-mongodb') { + return ( + + MongoDB Explorer is only available for PSMDB (MongoDB) clusters. + + ); + } + return ; +}; + +// --------------------------------------------------------------------------- +// MongoExplorerPage — standalone route page +// --------------------------------------------------------------------------- + +export const MongoExplorerPage = (_props: PluginRouteProps) => { + const [instances, setInstances] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const loadInstances = () => { + setLoading(true); + setError(null); + fetchMongoInstances() + .then((found) => { + const mongoInstances = found + .filter((i) => i.provider === 'percona-server-mongodb') + .sort((a, b) => instanceKey(a).localeCompare(instanceKey(b))); + setInstances(mongoInstances); + }) + .catch((e: unknown) => setError(errText(e))) + .finally(() => setLoading(false)); + }; + + useEffect(() => { + loadInstances(); + }, []); + + const navigateToInstance = (instance: MongoInstanceTarget) => { + window.location.assign( + `/databases/${encodeURIComponent(instance.namespace)}/${encodeURIComponent(instance.name)}/mongodb-explorer` + ); + }; + + return ( + + + MongoDB Explorer + + + Discover and query OpenEverest-managed MongoDB instances. Select an + instance below to browse databases, view collections, and run find + queries. + + + {loading ? ( + + + + Loading MongoDB instances… + + + ) : error ? ( + + {error} + + + ) : instances.length === 0 ? ( + + No OpenEverest MongoDB instances are available. Deploy a MongoDB + instance or ask your administrator for access. + + ) : ( + + + + + Name + Namespace + K8s Cluster + Action + + + + {instances.map((instance) => ( + navigateToInstance(instance)} + > + + { + e.stopPropagation(); + navigateToInstance(instance); + }} + > + {instance.name} + + + {instance.namespace} + {instance.k8sCluster} + + + + + ))} + +
+
+ )} +
+ ); +}; diff --git a/src/main.tsx b/src/main.tsx index 85c73a7..4aa6e91 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,972 +4,27 @@ import type { PluginRouteProps, ClusterDetailTabProps, } from '@openeverest/plugin-sdk'; - -// React and fetch are provided by the host at runtime. -let React: PluginApi['React']; -let pluginFetch: PluginApi['fetch']; - -// --------------------------------------------------------------------------- -// API helpers -// --------------------------------------------------------------------------- - -// Uses api.fetch() so the host proxy receives a valid session, generates the -// X-Everest-User JWT, and forwards it to the backend. -async function apiFetch(path: string, opts?: RequestInit) { - const res = await pluginFetch(`/api${path}`, opts); - if (!res.ok) { - const text = await res.text().catch(() => ''); - throw new Error(extractErrorMessage(text) || `HTTP ${res.status}`); - } - return res.json(); -} - -// extractErrorMessage unwraps the backend's nested error envelopes -// (`{"error":"…"}` possibly wrapping a downstream `{"message":"…"}`) into a -// single human-readable sentence, so the UI never shows raw JSON. -function extractErrorMessage(raw: string): string { - let msg = (raw ?? '').trim(); - if (!msg) return ''; - - const unwrap = (s: string): string => { - try { - const parsed = JSON.parse(s); - if (parsed && typeof parsed === 'object') { - const inner = (parsed as Record).message ?? (parsed as Record).error; - if (typeof inner === 'string') return inner.trim(); - } - } catch { - // Not JSON — fall through. - } - return s; - }; - - // Peel envelopes until stable, then grab the deepest embedded JSON message. - for (let i = 0; i < 3; i++) { - const next = unwrap(msg); - if (next === msg) break; - msg = next; - } - const embedded = msg.match(/\{[^{}]*"message"\s*:\s*"([^"]+)"[^{}]*\}/); - if (embedded) return embedded[1].trim(); - - return msg; -} - -interface MongoInstanceTarget { - k8sCluster: string; - namespace: string; - name: string; - provider: string; -} - -async function fetchMongoInstances(): Promise { - const data = await apiFetch('/instances'); - return Array.isArray(data.instances) ? data.instances : []; -} - -async function fetchDatabases(k8sCluster: string, instance: string, namespace: string): Promise { - const data = await apiFetch( - `/databases?k8sCluster=${encodeURIComponent(k8sCluster)}&cluster=${encodeURIComponent(instance)}&namespace=${encodeURIComponent(namespace)}` - ); - return data.databases ?? []; -} - -async function fetchCollections(k8sCluster: string, instance: string, namespace: string, db: string): Promise { - const data = await apiFetch( - `/databases/${encodeURIComponent(db)}/collections?k8sCluster=${encodeURIComponent(k8sCluster)}&cluster=${encodeURIComponent(instance)}&namespace=${encodeURIComponent(namespace)}` - ); - return data.collections ?? []; -} - -async function runQuery( - k8sCluster: string, - cluster: string, - namespace: string, - db: string, - collection: string, - filterStr: string, - projectionStr: string, - limit: number -): Promise[]> { - let filter: unknown; - try { - filter = JSON.parse(filterStr || '{}'); - } catch { - throw new Error('Filter is not valid JSON.'); - } - - let projection: unknown = undefined; - if (projectionStr.trim()) { - try { - projection = JSON.parse(projectionStr); - } catch { - throw new Error('Projection is not valid JSON.'); - } - } - - const data = await apiFetch('/query', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ k8sCluster, cluster, namespace, db, collection, filter, projection, limit }), - }); - return data.documents ?? []; -} - -interface OverviewMember { - name: string; - stateStr: string; - health: number; - uptimeSeconds: number; - lagSeconds: number; -} - -interface ClusterOverview { - sharded: boolean; - version?: string; - replicaSet?: { set: string; members: OverviewMember[] }; - connections?: { current?: number; available?: number }; - opcounters?: Record; - shards?: unknown[]; - balancer?: { mode?: string; inBalancerRound?: boolean }; -} - -async function fetchOverview(k8sCluster: string, cluster: string, namespace: string): Promise { - return apiFetch( - `/overview?k8sCluster=${encodeURIComponent(k8sCluster)}&cluster=${encodeURIComponent(cluster)}&namespace=${encodeURIComponent(namespace)}` - ); -} - -// --------------------------------------------------------------------------- -// Styles -// --------------------------------------------------------------------------- - -const styles = { - panel: { padding: '1rem' } as React.CSSProperties, - label: { fontSize: '0.75rem', color: '#666', marginBottom: '0.25rem', display: 'block' } as React.CSSProperties, - input: { - padding: '0.4rem 0.5rem', - border: '1px solid #ccc', - borderRadius: '4px', - fontFamily: 'monospace', - fontSize: '0.875rem', - width: '100%', - boxSizing: 'border-box', - } as React.CSSProperties, - btn: (primary: boolean, disabled?: boolean): React.CSSProperties => ({ - padding: '0.4rem 1rem', - background: disabled ? '#ccc' : primary ? '#1565c0' : '#e3e8f0', - color: disabled ? '#888' : primary ? '#fff' : '#333', - border: 'none', - borderRadius: '4px', - cursor: disabled ? 'not-allowed' : 'pointer', - fontWeight: 'bold', - fontSize: '0.875rem', - }), - treeItem: (depth: number, clickable: boolean): React.CSSProperties => ({ - padding: `0.2rem 0.25rem 0.2rem ${0.5 + depth * 1}rem`, - cursor: clickable ? 'pointer' : 'default', - fontSize: '0.875rem', - fontFamily: 'monospace', - borderRadius: '3px', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }), - error: { color: '#c62828', fontSize: '0.875rem', margin: '0.5rem 0' } as React.CSSProperties, - muted: { color: '#888', fontSize: '0.875rem', fontStyle: 'italic' } as React.CSSProperties, -}; - -// --------------------------------------------------------------------------- -// DatabaseTree — left-panel collection browser -// --------------------------------------------------------------------------- - -interface DatabaseTreeProps { - k8sCluster: string; - cluster: string; - namespace: string; - onSelectCollection: (db: string, collection: string) => void; -} - -const DatabaseTree = ({ k8sCluster, cluster, namespace, onSelectCollection }: DatabaseTreeProps) => { - const [databases, setDatabases] = React.useState([]); - const [expanded, setExpanded] = React.useState>(new Set()); - const [collections, setCollections] = React.useState>({}); - const [loading, setLoading] = React.useState(true); - const [colLoading, setColLoading] = React.useState>({}); - const [error, setError] = React.useState(null); - - React.useEffect(() => { - setLoading(true); - setError(null); - fetchDatabases(k8sCluster, cluster, namespace) - .then(setDatabases) - .catch((e: unknown) => setError(e instanceof Error ? e.message : String(e))) - .finally(() => setLoading(false)); - }, [k8sCluster, cluster, namespace]); - - const toggleDb = (db: string) => { - const next = new Set(expanded); - if (next.has(db)) { - next.delete(db); - setExpanded(next); - return; - } - next.add(db); - setExpanded(next); - if (!collections[db]) { - setColLoading((prev) => ({ ...prev, [db]: true })); - fetchCollections(k8sCluster, cluster, namespace, db) - .then((cols) => setCollections((prev) => ({ ...prev, [db]: cols }))) - .catch((e: unknown) => setError(e instanceof Error ? e.message : String(e))) - .finally(() => setColLoading((prev) => ({ ...prev, [db]: false }))); - } - }; - - if (loading) { - return React.createElement('p', { style: styles.muted }, 'Loading databases…'); - } - if (error) { - return React.createElement('p', { style: styles.error }, error); - } - if (databases.length === 0) { - return React.createElement('p', { style: styles.muted }, 'No databases found.'); - } - - return React.createElement( - 'div', - null, - ...databases.map((db) => { - const isOpen = expanded.has(db); - const cols = collections[db]; - return React.createElement( - 'div', - { key: db }, - React.createElement( - 'div', - { - style: { ...styles.treeItem(0, true), fontWeight: '600', color: '#333' }, - onClick: () => toggleDb(db), - }, - `${isOpen ? '▾' : '▸'} ${db}` - ), - isOpen && - React.createElement( - 'div', - null, - colLoading[db] - ? React.createElement('div', { style: styles.treeItem(1, false) }, '…') - : (cols ?? []).length === 0 - ? React.createElement('div', { style: { ...styles.treeItem(1, false), ...styles.muted } }, '(empty)') - : (cols ?? []).map((coll) => - React.createElement( - 'div', - { - key: coll, - style: { ...styles.treeItem(1, true), color: '#1565c0' }, - onClick: () => onSelectCollection(db, coll), - title: coll, - }, - `⊡ ${coll}` - ) - ) - ) - ); - }) - ); -}; - -// --------------------------------------------------------------------------- -// QueryPanel — query editor + results viewer -// --------------------------------------------------------------------------- - -function formatCellValue(v: unknown): string { - if (v === null || v === undefined) return '—'; - if (typeof v === 'object') return JSON.stringify(v); - return String(v); -} - -interface QueryPanelProps { - k8sCluster: string; - cluster: string; - namespace: string; - initialDb: string | null; - initialCollection: string | null; -} - -const QueryPanel = ({ k8sCluster, cluster, namespace, initialDb, initialCollection }: QueryPanelProps) => { - const [db, setDb] = React.useState(initialDb ?? ''); - const [collection, setCollection] = React.useState(initialCollection ?? ''); - const [filter, setFilter] = React.useState('{}'); - const [projection, setProjection] = React.useState(''); - const [limit, setLimit] = React.useState('20'); - const [results, setResults] = React.useState[] | null>(null); - const [loading, setLoading] = React.useState(false); - const [error, setError] = React.useState(null); - const [viewMode, setViewMode] = React.useState<'table' | 'json'>('table'); - const [cellDetail, setCellDetail] = React.useState<{ column: string; value: unknown } | null>(null); - - // Sync selection from the tree. - React.useEffect(() => { if (initialDb) setDb(initialDb); }, [initialDb]); - React.useEffect(() => { if (initialCollection) setCollection(initialCollection); }, [initialCollection]); - - const handleRun = () => { - if (!db.trim() || !collection.trim()) { - setError('Enter a database and collection name.'); - return; - } - setLoading(true); - setError(null); - runQuery(k8sCluster, cluster, namespace, db, collection, filter, projection, parseInt(limit, 10) || 20) - .then((docs) => { - setResults(docs); - }) - .catch((e: unknown) => { - setError(e instanceof Error ? e.message : String(e)); - setResults(null); - }) - .finally(() => setLoading(false)); - }; - - // Derive columns from first 12 distinct keys across results. - const columns = - results && results.length > 0 - ? Array.from(new Set(results.flatMap((doc) => Object.keys(doc)))).slice(0, 12) - : []; - - return React.createElement( - 'div', - null, - // ── Inputs row 1: db + collection ────────────────────────────────────── - React.createElement( - 'div', - { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem', marginBottom: '0.75rem' } }, - React.createElement( - 'div', - null, - React.createElement('label', { style: styles.label }, 'Database'), - React.createElement('input', { - value: db, - placeholder: 'mydb', - style: styles.input, - onChange: (e: { target: { value: string } }) => setDb(e.target.value), - }) - ), - React.createElement( - 'div', - null, - React.createElement('label', { style: styles.label }, 'Collection'), - React.createElement('input', { - value: collection, - placeholder: 'users', - style: styles.input, - onChange: (e: { target: { value: string } }) => setCollection(e.target.value), - }) - ) - ), - // ── Filter ──────────────────────────────────────────────────────────── - React.createElement( - 'div', - { style: { marginBottom: '0.75rem' } }, - React.createElement('label', { style: styles.label }, 'Filter (JSON)'), - React.createElement('textarea', { - value: filter, - placeholder: '{ "status": "active" }', - style: { ...styles.input, height: '64px', resize: 'vertical' }, - onChange: (e: { target: { value: string } }) => setFilter(e.target.value), - }) - ), - // ── Projection + Limit + Run ────────────────────────────────────────── - React.createElement( - 'div', - { - style: { - display: 'grid', - gridTemplateColumns: '1fr 80px auto', - gap: '0.75rem', - alignItems: 'end', - marginBottom: '1rem', - }, - }, - React.createElement( - 'div', - null, - React.createElement('label', { style: styles.label }, 'Projection (optional)'), - React.createElement('input', { - value: projection, - placeholder: '{ "name": 1, "_id": 0 }', - style: styles.input, - onChange: (e: { target: { value: string } }) => setProjection(e.target.value), - }) - ), - React.createElement( - 'div', - null, - React.createElement('label', { style: styles.label }, 'Limit'), - React.createElement('input', { - value: limit, - type: 'number', - min: '1', - max: '1000', - style: { ...styles.input, width: '80px' }, - onChange: (e: { target: { value: string } }) => setLimit(e.target.value), - }) - ), - React.createElement( - 'button', - { onClick: handleRun, disabled: loading, style: styles.btn(true, loading) }, - loading ? 'Running…' : 'Run Query' - ) - ), - // ── Error ───────────────────────────────────────────────────────────── - error && React.createElement('p', { style: styles.error }, error), - // ── Results ─────────────────────────────────────────────────────────── - results !== null && - React.createElement( - 'div', - null, - React.createElement( - 'div', - { style: { display: 'flex', gap: '0.5rem', alignItems: 'center', marginBottom: '0.5rem' } }, - React.createElement( - 'span', - { style: { ...styles.muted, fontStyle: 'normal' } }, - `${results.length} document(s) returned` - ), - React.createElement( - 'button', - { - onClick: () => setViewMode('table'), - style: { ...styles.btn(viewMode === 'table'), padding: '0.2rem 0.6rem' }, - }, - 'Table' - ), - React.createElement( - 'button', - { - onClick: () => setViewMode('json'), - style: { ...styles.btn(viewMode === 'json'), padding: '0.2rem 0.6rem' }, - }, - 'JSON' - ) - ), - viewMode === 'json' - ? React.createElement( - 'pre', - { - style: { - background: '#f5f5f5', - padding: '0.75rem', - borderRadius: '4px', - fontSize: '0.8rem', - maxHeight: '420px', - overflow: 'auto', - margin: 0, - }, - }, - JSON.stringify(results, null, 2) - ) - : results.length === 0 - ? React.createElement('p', { style: styles.muted }, 'No documents found.') - : React.createElement( - 'div', - { style: { overflowX: 'auto', maxHeight: '420px', overflowY: 'auto' } }, - React.createElement( - 'table', - { style: { width: '100%', borderCollapse: 'collapse', fontSize: '0.8rem' } }, - React.createElement( - 'thead', - null, - React.createElement( - 'tr', - null, - ...columns.map((col) => - React.createElement( - 'th', - { - key: col, - style: { - padding: '0.4rem 0.6rem', - background: '#f0f4fb', - borderBottom: '2px solid #c5d0e0', - textAlign: 'left', - fontFamily: 'monospace', - whiteSpace: 'nowrap', - position: 'sticky', - top: 0, - }, - }, - col - ) - ) - ) - ), - React.createElement( - 'tbody', - null, - ...results.map((doc, i) => - React.createElement( - 'tr', - { - key: i, - style: { borderBottom: '1px solid #eee', background: i % 2 === 0 ? '#fff' : '#fafafa' }, - }, - ...columns.map((col) => - React.createElement( - 'td', - { - key: col, - title: 'Click to view full value', - onClick: () => setCellDetail({ column: col, value: doc[col] }), - style: { - padding: '0.35rem 0.6rem', - fontFamily: 'monospace', - maxWidth: '220px', - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - cursor: 'pointer', - }, - }, - formatCellValue(doc[col]) - ) - ) - ) - ) - ) - ) - ) - ), - // ── Cell detail modal ───────────────────────────────────────────────── - cellDetail && - React.createElement( - 'div', - { - onClick: () => setCellDetail(null), - style: { - position: 'fixed', - inset: 0, - background: 'rgba(0,0,0,0.4)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - zIndex: 1000, - }, - }, - React.createElement( - 'div', - { - onClick: (e: { stopPropagation: () => void }) => e.stopPropagation(), - style: { - background: '#fff', - borderRadius: '6px', - padding: '1rem', - maxWidth: '80vw', - maxHeight: '80vh', - display: 'flex', - flexDirection: 'column', - boxShadow: '0 8px 32px rgba(0,0,0,0.25)', - }, - }, - React.createElement( - 'div', - { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '1rem', marginBottom: '0.5rem' } }, - React.createElement('span', { style: { fontFamily: 'monospace', fontWeight: '700', fontSize: '0.85rem' } }, cellDetail.column), - React.createElement( - 'button', - { onClick: () => setCellDetail(null), style: { ...styles.btn(false), padding: '0.2rem 0.6rem' } }, - 'Close' - ) - ), - React.createElement( - 'pre', - { - style: { - background: '#f5f5f5', - padding: '0.75rem', - borderRadius: '4px', - fontSize: '0.8rem', - overflow: 'auto', - margin: 0, - whiteSpace: 'pre-wrap', - wordBreak: 'break-word', - }, - }, - typeof cellDetail.value === 'object' && cellDetail.value !== null - ? JSON.stringify(cellDetail.value, null, 2) - : formatCellValue(cellDetail.value) - ) - ) - ) - ); -}; - -// --------------------------------------------------------------------------- -// ClusterOverviewStrip — collapsible instance health summary -// --------------------------------------------------------------------------- - -interface ClusterOverviewStripProps { - k8sCluster: string; - cluster: string; - namespace: string; -} - -const overviewHeaderCell: React.CSSProperties = { - textAlign: 'left', - padding: '0.35rem 0.6rem', - color: '#666', - fontWeight: '600', -}; - -const ClusterOverviewStrip = ({ k8sCluster, cluster, namespace }: ClusterOverviewStripProps) => { - const [data, setData] = React.useState(null); - const [error, setError] = React.useState(null); - const [open, setOpen] = React.useState(false); - - React.useEffect(() => { - let active = true; - setData(null); - setError(null); - fetchOverview(k8sCluster, cluster, namespace) - .then((d) => { if (active) setData(d); }) - .catch((e: unknown) => { if (active) setError(e instanceof Error ? e.message : String(e)); }); - return () => { active = false; }; - }, [k8sCluster, cluster, namespace]); - - if (error) { - const pending = /connection details are not yet available|not yet available/i.test(error); - if (pending) { - return React.createElement( - 'div', - { style: { ...styles.muted, margin: '0 0 0.75rem' } }, - 'Cluster status will appear once the instance finishes provisioning.' - ); - } - return React.createElement('div', { style: { ...styles.error, margin: '0 0 0.75rem' } }, `Cluster status unavailable: ${error}`); - } - if (!data) { - return React.createElement('div', { style: { ...styles.muted, margin: '0 0 0.75rem' } }, 'Loading cluster status…'); - } - - const rs = data.replicaSet; - const primary = rs?.members.find((m) => m.stateStr === 'PRIMARY'); - const maxLag = rs && rs.members.length > 0 ? Math.max(0, ...rs.members.map((m) => m.lagSeconds)) : 0; - const allHealthy = rs ? rs.members.every((m) => m.health === 1) : true; - - const summaryLine = rs - ? `${allHealthy ? '●' : '▲'} ${rs.set} · ${primary?.stateStr ?? 'no primary'} · ${rs.members.length} members · lag ${maxLag.toFixed(1)}s` - : data.sharded - ? '● sharded cluster' - : '● standalone'; - - return React.createElement( - 'div', - { style: { border: '1px solid #e0e0e0', borderRadius: '6px', marginBottom: '0.75rem', background: '#fff' } }, - React.createElement( - 'div', - { - onClick: () => setOpen((v) => !v), - style: { display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.5rem 0.75rem', cursor: 'pointer', fontSize: '0.85rem' }, - }, - React.createElement('span', { style: { color: allHealthy ? '#2e7d32' : '#e65100', fontWeight: '700' } }, summaryLine), - typeof data.connections?.current === 'number' && - React.createElement('span', { style: styles.muted }, `· ${data.connections.current} conns`), - data.sharded && - React.createElement('span', { style: styles.muted }, `· ${Array.isArray(data.shards) ? data.shards.length : 0} shards`), - React.createElement('span', { style: { marginLeft: 'auto', color: '#888' } }, open ? '▾' : '▸') - ), - open && - React.createElement( - 'div', - { style: { borderTop: '1px solid #eee', padding: '0.25rem 0.25rem 0.5rem' } }, - rs - ? React.createElement( - 'table', - { style: { width: '100%', borderCollapse: 'collapse', fontSize: '0.8rem' } }, - React.createElement( - 'thead', - null, - React.createElement( - 'tr', - null, - ...['Member', 'State', 'Health', 'Lag', 'Uptime'].map((h) => - React.createElement('th', { key: h, style: overviewHeaderCell }, h) - ) - ) - ), - React.createElement( - 'tbody', - null, - ...rs.members.map((m) => - React.createElement( - 'tr', - { key: m.name, style: { borderTop: '1px solid #f0f0f0' } }, - React.createElement('td', { style: { padding: '0.35rem 0.6rem', fontFamily: 'monospace' } }, m.name), - React.createElement('td', { style: { padding: '0.35rem 0.6rem', fontWeight: m.stateStr === 'PRIMARY' ? '700' : '400' } }, m.stateStr), - React.createElement('td', { style: { padding: '0.35rem 0.6rem', color: m.health === 1 ? '#2e7d32' : '#c62828' } }, m.health === 1 ? 'up' : 'down'), - React.createElement('td', { style: { padding: '0.35rem 0.6rem' } }, m.stateStr === 'PRIMARY' ? '—' : `${m.lagSeconds.toFixed(1)}s`), - React.createElement('td', { style: { padding: '0.35rem 0.6rem' } }, `${Math.floor(m.uptimeSeconds / 3600)}h`) - ) - ) - ) - ) - : React.createElement('div', { style: { ...styles.muted, padding: '0.35rem 0.6rem' } }, 'No replica set information available.'), - (data.opcounters || data.version) && - React.createElement( - 'div', - { style: { ...styles.muted, padding: '0.5rem 0.6rem 0', display: 'flex', gap: '1rem', flexWrap: 'wrap' } }, - data.version && React.createElement('span', null, `MongoDB ${data.version}`), - data.opcounters && - React.createElement( - 'span', - null, - `ops: ${['insert', 'query', 'update', 'delete'] - .map((k) => `${k[0]}${data.opcounters?.[k] ?? 0}`) - .join(' ')}` - ) - ) - ) - ); -}; - -// --------------------------------------------------------------------------- -// MongoExplorer — reusable database/collection explorer -// --------------------------------------------------------------------------- - -interface MongoExplorerProps { target: MongoInstanceTarget } -const MongoExplorer = ({ target }: MongoExplorerProps) => { - const [selectedDb, setSelectedDb] = React.useState(null); - const [selectedCollection, setSelectedCollection] = React.useState(null); - const handleSelectCollection = (db: string, coll: string) => { - setSelectedDb(db); - setSelectedCollection(coll); - }; - - return React.createElement( - 'div', - { style: { display: 'flex', height: '100%', minHeight: '520px', gap: 0 } }, - React.createElement( - 'div', - { style: { width: '220px', flexShrink: 0, borderRight: '1px solid #e0e0e0', padding: '1rem 0.75rem 1rem 1rem', overflowY: 'auto', background: '#fafafa' } }, - React.createElement('div', { style: { fontSize: '0.7rem', fontWeight: '700', textTransform: 'uppercase', letterSpacing: '0.08em', color: '#888', marginBottom: '0.5rem' } }, 'Databases'), - React.createElement(DatabaseTree, { k8sCluster: target.k8sCluster, cluster: target.name, namespace: target.namespace, onSelectCollection: handleSelectCollection }) - ), - React.createElement( - 'div', - { style: { flex: 1, padding: '1rem', overflowY: 'auto' } }, - React.createElement(ClusterOverviewStrip, { k8sCluster: target.k8sCluster, cluster: target.name, namespace: target.namespace }), - React.createElement('div', { style: { fontSize: '0.7rem', fontWeight: '700', textTransform: 'uppercase', letterSpacing: '0.08em', color: '#888', marginBottom: '0.75rem' } }, 'Query'), - React.createElement(QueryPanel, { k8sCluster: target.k8sCluster, cluster: target.name, namespace: target.namespace, initialDb: selectedDb, initialCollection: selectedCollection }) - ) - ); -}; - -function instanceKey(instance: MongoInstanceTarget): string { - return `${instance.k8sCluster}/${instance.namespace}/${instance.name}`; -} - -function targetFromClusterDetailProps(props: ClusterDetailTabProps): MongoInstanceTarget { - const clusterObj = props.cluster as { engine?: string; clusterName?: string; spec?: { provider?: string; clusterName?: string }; [k: string]: unknown }; - return { - k8sCluster: clusterObj.clusterName ?? clusterObj.spec?.clusterName ?? 'main', - namespace: props.namespace, - name: props.instanceName, - provider: clusterObj.spec?.provider ?? 'percona-server-mongodb', - }; -} - -// --------------------------------------------------------------------------- -// MongoExplorerTab — clusterDetailTab entry point -// --------------------------------------------------------------------------- - -const MongoExplorerTab = (props: ClusterDetailTabProps) => { - const target = targetFromClusterDetailProps(props); - const clusterObj = props.cluster as { engine?: string; spec?: { provider?: string }; [k: string]: unknown }; - const provider = clusterObj.spec?.provider ?? clusterObj.engine; - if (provider && provider !== 'psmdb' && provider !== 'percona-server-mongodb') { - return React.createElement('div', { style: { padding: '2rem', color: '#666' } }, 'MongoDB Explorer is only available for PSMDB (MongoDB) clusters.'); - } - return React.createElement(MongoExplorer, { key: instanceKey(target), target }); -}; - -// --------------------------------------------------------------------------- -// MongoExplorerPage — standalone route page -// --------------------------------------------------------------------------- - -const MongoExplorerPage = (_props: PluginRouteProps) => { - const [instances, setInstances] = React.useState([]); - const [loading, setLoading] = React.useState(true); - const [error, setError] = React.useState(null); - - const loadInstances = () => { - setLoading(true); - setError(null); - fetchMongoInstances().then((found) => { - const mongoInstances = found.filter((i) => i.provider === 'percona-server-mongodb').sort((a, b) => instanceKey(a).localeCompare(instanceKey(b))); - setInstances(mongoInstances); - }).catch((e: unknown) => setError(e instanceof Error ? e.message : String(e))).finally(() => setLoading(false)); - }; - - React.useEffect(() => { loadInstances(); }, []); - - const navigateToInstance = (instance: MongoInstanceTarget) => { - window.location.assign(`/databases/${encodeURIComponent(instance.namespace)}/${encodeURIComponent(instance.name)}/mongodb-explorer`); - }; - - return React.createElement( - 'div', - { style: { padding: '1.5rem', height: '100%', boxSizing: 'border-box', overflowY: 'auto' } }, - React.createElement('h1', { style: { margin: '0 0 0.5rem' } }, 'MongoDB Explorer'), - React.createElement( - 'p', - { style: { margin: '0 0 1.5rem', color: '#555', fontSize: '0.9rem', lineHeight: '1.4' } }, - 'Discover and query OpenEverest-managed MongoDB instances. Select an instance below to browse databases, view collections, and run find queries.' - ), - loading - ? React.createElement('p', { style: styles.muted }, 'Loading MongoDB instances…') - : error - ? React.createElement( - 'div', - null, - React.createElement('p', { style: styles.error }, error), - React.createElement('button', { style: styles.btn(true), onClick: loadInstances }, 'Retry') - ) - : instances.length === 0 - ? React.createElement( - 'p', - { style: styles.muted }, - 'No OpenEverest MongoDB instances are available. Deploy a MongoDB instance or ask your administrator for access.' - ) - : React.createElement( - 'div', - { style: { background: '#fff', border: '1px solid #e0e0e0', borderRadius: '6px', overflow: 'hidden', maxWidth: '900px' } }, - React.createElement( - 'table', - { style: { width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' } }, - React.createElement( - 'thead', - null, - React.createElement( - 'tr', - null, - React.createElement( - 'th', - { - style: { - padding: '0.6rem 0.85rem', - background: '#f0f4fb', - borderBottom: '2px solid #c5d0e0', - textAlign: 'left', - fontWeight: '600', - color: '#333', - }, - }, - 'Name' - ), - React.createElement( - 'th', - { - style: { - padding: '0.6rem 0.85rem', - background: '#f0f4fb', - borderBottom: '2px solid #c5d0e0', - textAlign: 'left', - fontWeight: '600', - color: '#333', - }, - }, - 'Namespace' - ), - React.createElement( - 'th', - { - style: { - padding: '0.6rem 0.85rem', - background: '#f0f4fb', - borderBottom: '2px solid #c5d0e0', - textAlign: 'left', - fontWeight: '600', - color: '#333', - }, - }, - 'K8s Cluster' - ), - React.createElement( - 'th', - { - style: { - padding: '0.6rem 0.85rem', - background: '#f0f4fb', - borderBottom: '2px solid #c5d0e0', - textAlign: 'right', - fontWeight: '600', - color: '#333', - }, - }, - 'Action' - ) - ) - ), - React.createElement( - 'tbody', - null, - ...instances.map((instance, i) => - React.createElement( - 'tr', - { - key: instanceKey(instance), - style: { - borderBottom: i === instances.length - 1 ? 'none' : '1px solid #eee', - background: i % 2 === 0 ? '#fff' : '#fafafa', - cursor: 'pointer', - }, - onClick: () => navigateToInstance(instance), - }, - React.createElement( - 'td', - { style: { padding: '0.6rem 0.85rem', fontWeight: '600', color: '#1565c0' } }, - instance.name - ), - React.createElement( - 'td', - { style: { padding: '0.6rem 0.85rem', color: '#555' } }, - instance.namespace - ), - React.createElement( - 'td', - { style: { padding: '0.6rem 0.85rem', color: '#555' } }, - instance.k8sCluster - ), - React.createElement( - 'td', - { style: { padding: '0.6rem 0.85rem', textAlign: 'right' } }, - React.createElement( - 'button', - { - style: { ...styles.btn(true), padding: '0.3rem 0.75rem', fontSize: '0.8rem' }, - onClick: (e: { stopPropagation: () => void }) => { - e.stopPropagation(); - navigateToInstance(instance); - }, - }, - 'Explore' - ) - ) - ) - ) - ) - ) - ) - ); -}; - -// --------------------------------------------------------------------------- -// Plugin registration -// --------------------------------------------------------------------------- +import { PluginThemeProvider } from '@openeverest/ui-lib'; +import { configurePlugin, getCssNonce } from './api'; +import { MongoExplorerPage, MongoExplorerTab } from './components'; + +// Every host-rendered entry point is wrapped so its MUI subtree inherits the +// host design tokens (palette, typography, dark mode) through a namespaced +// Emotion cache — see @openeverest/ui-lib PluginThemeProvider. +const ThemedPage = (props: PluginRouteProps) => ( + + + +); + +const ThemedTab = (props: ClusterDetailTabProps) => ( + + + +); const register: PluginRegisterFn = (api: PluginApi) => { - React = api.React; - pluginFetch = api.fetch.bind(api); + configurePlugin(api); api.registerExtension({ type: 'sidebarItem', @@ -979,7 +34,7 @@ const register: PluginRegisterFn = (api: PluginApi) => { api.registerExtension({ type: 'route', label: 'MongoDB Explorer', - component: MongoExplorerPage, + component: ThemedPage, }); api.registerExtension({ @@ -987,7 +42,7 @@ const register: PluginRegisterFn = (api: PluginApi) => { label: 'MongoDB Explorer', path: 'mongodb-explorer', providers: ['percona-server-mongodb'], - component: MongoExplorerTab, + component: ThemedTab, }); }; diff --git a/vite.config.ts b/vite.config.ts index 093f01d..e671444 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,14 +3,25 @@ import react from '@vitejs/plugin-react-swc'; export default defineConfig({ plugins: [react()], + // Vite lib mode (unlike app builds) does not substitute this, but the bundled + // MUI/Emotion/React read it at init — leaving it unreplaced throws + // "process is not defined" in the browser and the plugin fails to load. + define: { + 'process.env.NODE_ENV': JSON.stringify('production'), + }, build: { lib: { entry: 'src/main.tsx', formats: ['es'], fileName: () => 'main.js', }, - // No externals — React is passed at runtime via the register() API, - // so there are no bare `import 'react'` statements in the bundle. + rollupOptions: { + // React (incl. the automatic JSX runtime) is the host's shared singleton, + // resolved at runtime via the host import map. MUI/Emotion ship inside + // @openeverest/ui-lib and stay bundled so the plugin owns its design-system + // version (Model B). + external: [/^react($|\/)/, /^react-dom($|\/)/], + }, }, server: { port: 3001,