diff --git a/README.md b/README.md index 140f149..6e26cae 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ or remote Trino cluster. > production workloads. Treat the current release as an early-stage demo; > production-ready builds and documentation are planned. -![Trino Query UI Demo](demos.gif "Trino Query UI Demo") +![Trino Query UI](screenshot.png "Trino Query UI") Implementation details: * React TypeScript project with Vite @@ -29,7 +29,7 @@ import { QueryEditor } from 'trino-query-ui' import 'trino-query-ui/dist/index.css' function MyTrinoApp() { - return + return } export default MyTrinoApp @@ -51,8 +51,10 @@ npm run build ### Copying into Trino +``` mkdir -p $TRINO_HOME/core/trino-main/src/main/resources/query_ui_webapp/ cp -r dist/* $TRINO_HOME/core/trino-main/src/main/resources/query_ui_webapp/ +``` ### Modifying Trino to respond to /query/ @@ -126,7 +128,7 @@ npm run dev The local URL is displayed, and you can open it in your browser. -### Set Up proxying to a local Trino instance +### Set up proxying to a local Trino instance Update `vite.config.ts` with the following so that queries can be proxied to Trino's query endpoint running on `http://localhost:8080` (or any @@ -182,19 +184,19 @@ cases are: The approach: 1. Direct integration into the Trino UI - - No need for an additional authentication hop (although it could be added - in the future) - - Authenticates as the user executing the query when using OAuth2 - - Trino does the heavy lifting + - No need for an additional authentication hop (although it could be added + in the future) + - Authenticates as the user executing the query when using OAuth2 + - Trino does the heavy lifting 2. Remove friction so you can simply write a query - - Autocomplete understands the Trino language, tables, and columns - - Provides syntax highlighting and validation - - Offers a comprehensive catalog explorer + - Autocomplete understands the Trino language, tables, and columns + - Provides syntax highlighting and validation + - Offers a comprehensive catalog explorer 3. Avoid black-box query execution - - Show progress and execution details. People ask "why is my query slow?" - mostly because they only see a spinner for minutes. - - Link to the Trino Query UI to drill into query performance - - Show stages and split counts like the Trino console client + - Show progress and execution details. People ask "why is my query slow?" + mostly because they only see a spinner for minutes. + - Link to the Trino Query UI to drill into query performance + - Show stages and split counts like the Trino console client 4. Keep the experience easy to navigate ### Gaps and future direction diff --git a/precise/index.html b/precise/index.html index 9f6fd79..9faea4e 100644 --- a/precise/index.html +++ b/precise/index.html @@ -4,7 +4,7 @@ - Trino Query Editor - Example app + Trino query editor - Example app
diff --git a/precise/package-lock.json b/precise/package-lock.json index 6729dda..f952c17 100644 --- a/precise/package-lock.json +++ b/precise/package-lock.json @@ -9,13 +9,15 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", "@monaco-editor/react": "^4.7.0", + "@mui/icons-material": "^7.3.2", + "@mui/material": "^7.3.2", + "@mui/x-data-grid": "^8.14.0", + "@mui/x-tree-view": "^8.13.1", "antlr4-c3": "^3.4.4", "antlr4ng": "^3.0.16", - "lucide-react": "^0.544.0", - "prettier": "^3.6.2", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", "sql-formatter": "^15.6.9", "uuidv4": "^6.2.13" }, @@ -29,6 +31,7 @@ "eslint": "^9.36.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.21", + "prettier": "^3.6.2", "typescript": "^5.9.2", "vite": "^7.1.7" }, @@ -41,7 +44,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -68,6 +70,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -107,7 +110,6 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.3", @@ -151,7 +153,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -161,7 +162,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -203,7 +203,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -213,7 +212,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -247,7 +245,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.4" @@ -304,7 +301,6 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -319,7 +315,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -338,7 +333,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -348,6 +342,182 @@ "node": ">=6.9.0" } }, + "node_modules/@base-ui-components/utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@base-ui-components/utils/-/utils-0.1.1.tgz", + "integrity": "sha512-HWXZA8upEKgrdL1rQqxWu1H+2tB2cXzY2jCxvgnpUv3eoWN2jldhXxMZnXIjZF7jahGxSWXfSIM/qskiTWFFxA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.3", + "@floating-ui/utils": "^0.2.10", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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/babel-plugin/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/@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", + "peer": true, + "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", + "peer": true, + "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/@esbuild/aix-ppc64": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", @@ -976,6 +1146,12 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1032,7 +1208,6 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1054,7 +1229,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1064,14 +1238,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "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==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1101,6 +1273,418 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.2.tgz", + "integrity": "sha512-AOyfHjyDKVPGJJFtxOlept3EYEdLoar/RvssBTWVAvDJGIE676dLi2oT/Kx+FoVXFoA/JdV7DEMq/BVWV3KHRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.2.tgz", + "integrity": "sha512-TZWazBjWXBjR6iGcNkbKklnwodcwj0SrChCNHc9BhD9rBgET22J1eFhHsEmvSvru9+opDy3umqAimQjokhfJlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.3.2", + "@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/material": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.2.tgz", + "integrity": "sha512-qXvbnawQhqUVfH1LMgMaiytP+ZpGoYhnGl7yYq2x57GYzcFL/iPzSZ3L30tlbwEjSVKNYcbiKO8tANR1tadjUg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.28.3", + "@mui/core-downloads-tracker": "^7.3.2", + "@mui/system": "^7.3.2", + "@mui/types": "^7.4.6", + "@mui/utils": "^7.3.2", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.1.1", + "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.2", + "@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/material/node_modules/react-is": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", + "license": "MIT" + }, + "node_modules/@mui/private-theming": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.2.tgz", + "integrity": "sha512-ha7mFoOyZGJr75xeiO9lugS3joRROjc8tG1u4P50dH0KR7bwhHznVMcYg7MouochUy0OxooJm/OOSpJ7gKcMvg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.3", + "@mui/utils": "^7.3.2", + "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.2", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.2.tgz", + "integrity": "sha512-PkJzW+mTaek4e0nPYZ6qLnW5RGa0KN+eRTf5FA2nc7cFZTeM+qebmGibaTLrgQBy3UpcpemaqfzToBNkzuxqew==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.3", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.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.2", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.2.tgz", + "integrity": "sha512-9d8JEvZW+H6cVkaZ+FK56R53vkJe3HsTpcjMUtH8v1xK6Y1TjzHdZ7Jck02mGXJsE6MQGWVs3ogRHTQmS9Q/rA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.28.3", + "@mui/private-theming": "^7.3.2", + "@mui/styled-engine": "^7.3.2", + "@mui/types": "^7.4.6", + "@mui/utils": "^7.3.2", + "clsx": "^2.1.1", + "csstype": "^3.1.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.7", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.7.tgz", + "integrity": "sha512-8vVje9rdEr1rY8oIkYgP+Su5Kwl6ik7O3jQ0wl78JGSmiZhRHV+vkjooGdKD8pbtZbutXFVTWQYshu2b3sG9zw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.3.tgz", + "integrity": "sha512-kwNAUh7bLZ7mRz9JZ+6qfRnnxbE4Zuc+RzXnhSpRSxjTlSTj7b4JxRLXpG+MVtPVtqks5k/XC8No1Vs3x4Z2gg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/types": "^7.4.7", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.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/utils/node_modules/react-is": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", + "license": "MIT" + }, + "node_modules/@mui/x-data-grid": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.14.0.tgz", + "integrity": "sha512-bzUpD83Wx4mawkgquDQUUbLLnpF+JP7Pe7YQx1ixS6W/AlUwXAVagPTOijwchHvlx0Ky11dJvOQAfrnWu6an/Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.3", + "@mui/x-internals": "8.14.0", + "@mui/x-virtualizer": "0.2.3", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.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 + } + } + }, + "node_modules/@mui/x-data-grid/node_modules/@mui/x-internals": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.14.0.tgz", + "integrity": "sha512-esYyl61nuuFXiN631TWuPh2tqdoyTdBI/4UXgwH3rytF8jiWvy6prPBPRHEH1nvW3fgw9FoBI48FlOO+yEI8xg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.3", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-internals": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.13.1.tgz", + "integrity": "sha512-OKQyCJ9uxtMpjBZCOEQGOR5MhgL1f9HjI4qZHuaLxxtDATK5rcBbVjBF67hI8FzXeF1wrcZP2wsjc4AgGpAo9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.2", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-tree-view": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-8.13.1.tgz", + "integrity": "sha512-i5GrIMbNZRGEIYpJDC+d8TStANmvQpwIGYbsGTczrM+6x3KCkASahURgSraZVBfAAxJ2sm55rbrG+6jtkbq5HQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@base-ui-components/utils": "0.1.1", + "@mui/utils": "^7.3.2", + "@mui/x-internals": "8.13.1", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.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 + } + } + }, + "node_modules/@mui/x-virtualizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.2.3.tgz", + "integrity": "sha512-CZ+VxFmeJaTduAOlSyo5cVek0PV5Y8gm4coyaHEpCvms207J9AoMUKqWIcdwsVGlTH1Y71j35xT/MwHKutZiNw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.3", + "@mui/x-internals": "8.14.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-virtualizer/node_modules/@mui/x-internals": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.14.0.tgz", + "integrity": "sha512-esYyl61nuuFXiN631TWuPh2tqdoyTdBI/4UXgwH3rytF8jiWvy6prPBPRHEH1nvW3fgw9FoBI48FlOO+yEI8xg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.3", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1139,23 +1723,15 @@ "node": ">= 8" } }, - "node_modules/@react-dnd/asap": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", - "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==", - "license": "MIT" - }, - "node_modules/@react-dnd/invariant": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", - "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==", - "license": "MIT" - }, - "node_modules/@react-dnd/shallowequal": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", - "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", - "license": "MIT" + "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/pluginutils": { "version": "1.0.0-beta.35", @@ -1531,12 +2107,24 @@ "dev": true, "license": "MIT" }, + "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==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.1.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", - "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1551,12 +2139,20 @@ "@types/react": "^19.0.0" } }, + "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/@types/trusted-types": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz", "integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/uuid": { "version": "8.3.4", @@ -1600,6 +2196,7 @@ "integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.44.1", "@typescript-eslint/types": "8.44.1", @@ -1826,6 +2423,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1908,6 +2506,21 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "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/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1968,6 +2581,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -1986,7 +2600,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2030,6 +2643,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "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/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2070,6 +2692,31 @@ "dev": true, "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.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2089,14 +2736,12 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": 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==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2123,15 +2768,14 @@ "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==", "license": "MIT" }, - "node_modules/dnd-core": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", - "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "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": { - "@react-dnd/asap": "^5.0.1", - "@react-dnd/invariant": "^4.0.1", - "redux": "^4.2.0" + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" } }, "node_modules/electron-to-chromium": { @@ -2141,6 +2785,15 @@ "dev": true, "license": "ISC" }, + "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/esbuild": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", @@ -2197,7 +2850,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2212,6 +2864,7 @@ "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2448,6 +3101,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -2530,6 +3184,12 @@ "node": ">=8" } }, + "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/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2583,6 +3243,15 @@ "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/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2636,6 +3305,18 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "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", @@ -2659,7 +3340,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2682,6 +3362,27 @@ "node": ">=0.8.19" } }, + "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.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2726,7 +3427,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -2746,7 +3446,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -2762,6 +3461,12 @@ "dev": true, "license": "MIT" }, + "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/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2813,6 +3518,12 @@ "node": ">= 0.8.0" } }, + "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/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2836,6 +3547,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2846,15 +3569,6 @@ "yallist": "^3.0.2" } }, - "node_modules/lucide-react": { - "version": "0.544.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", - "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2915,7 +3629,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2973,6 +3686,15 @@ "dev": true, "license": "MIT" }, + "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/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3027,7 +3749,6 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -3036,6 +3757,24 @@ "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-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3056,11 +3795,25 @@ "node": ">=8" } }, + "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": { @@ -3119,6 +3872,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -3130,6 +3884,17 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "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/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3190,45 +3955,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-dnd": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", - "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", - "license": "MIT", - "dependencies": { - "@react-dnd/invariant": "^4.0.1", - "@react-dnd/shallowequal": "^4.0.1", - "dnd-core": "^16.0.1", - "fast-deep-equal": "^3.1.3", - "hoist-non-react-statics": "^3.3.2" - }, - "peerDependencies": { - "@types/hoist-non-react-statics": ">= 3.3.1", - "@types/node": ">= 12", - "@types/react": ">= 16", - "react": ">= 16.14" - }, - "peerDependenciesMeta": { - "@types/hoist-non-react-statics": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-dnd-html5-backend": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", - "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", - "license": "MIT", - "dependencies": { - "dnd-core": "^16.0.1" - } - }, "node_modules/react-dom": { "version": "19.1.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", @@ -3258,20 +3984,52 @@ "node": ">=0.10.0" } }, - "node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "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/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.9.2" + "is-core-module": "^2.16.0", + "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==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -3367,8 +4125,7 @@ "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/semver": { "version": "7.7.2", @@ -3406,6 +4163,15 @@ "node": ">=8" } }, + "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", @@ -3448,6 +4214,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3461,6 +4233,18 @@ "node": ">=8" } }, + "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.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3502,6 +4286,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3554,6 +4339,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3603,6 +4389,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -3629,6 +4424,7 @@ "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -3722,6 +4518,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/precise/package.json b/precise/package.json index 3467958..8e61b1c 100644 --- a/precise/package.json +++ b/precise/package.json @@ -1,6 +1,6 @@ { "name": "trino-query-ui", - "description": "Trino Query Editor react component", + "description": "Trino query editor React component", "version": "0.0.1", "author": { "name": "Trino contributors", @@ -40,13 +40,15 @@ "antlr4ng": "antlr4ng -visitor -listener -Dlanguage=TypeScript -o src/generated/lexer/SqlBase.g4 trino/SqlBase.g4" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", "@monaco-editor/react": "^4.7.0", + "@mui/icons-material": "^7.3.2", + "@mui/material": "^7.3.2", + "@mui/x-data-grid": "^8.14.0", + "@mui/x-tree-view": "^8.13.1", "antlr4-c3": "^3.4.4", "antlr4ng": "^3.0.16", - "lucide-react": "^0.544.0", - "prettier": "^3.6.2", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", "sql-formatter": "^15.6.9", "uuidv4": "^6.2.13" }, @@ -60,6 +62,7 @@ "eslint": "^9.36.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.21", + "prettier": "^3.6.2", "typescript": "^5.9.2", "vite": "^7.1.7" }, diff --git a/precise/src/QueryCell.tsx b/precise/src/QueryCell.tsx index c8329a7..e1fb7e8 100644 --- a/precise/src/QueryCell.tsx +++ b/precise/src/QueryCell.tsx @@ -1,12 +1,19 @@ -import React from 'react' +import React, { ReactNode } from 'react' +import { Box, Divider, IconButton, Stack, TextField, Toolbar, Typography } from '@mui/material' +import type { TextFieldProps } from '@mui/material/TextField' +import type { TypographyProps } from '@mui/material/Typography' +import MenuIcon from '@mui/icons-material/Menu' +import PlayCircleOutlinedIcon from '@mui/icons-material/PlayCircleOutlined' +import StopCircleOutlinedIcon from '@mui/icons-material/StopCircleOutlined' +import UnfoldLessIcon from '@mui/icons-material/UnfoldLess' +import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore' import QueryEditorPane from './QueryEditorPane' import ResultSet from './ResultSet' import Queries from './schema/Queries' import QueryInfo from './schema/QueryInfo' import AsyncTrinoClient from './AsyncTrinoClient' -import { Play, StopCircle, Link, Plus, FileEdit, MinusSquare, PlusSquare } from 'lucide-react' -import './style/components.css' -import './style/query-editor.css' + +const TOOLBAR_HEIGHT = 64 interface QueryCellState { results: any[] @@ -15,15 +22,22 @@ interface QueryCellState { errorMessage: string currentQuery: QueryInfo runningQuery: QueryInfo | undefined + editingTitle: boolean + editingCatalog: boolean + editingSchema: boolean + editorCollapsed: boolean } interface QueryCellProps { queries: Queries + drawerOpen: boolean + height: number + onDrawerToggle: () => void + theme?: string } class QueryCell extends React.Component { private queryRunner: AsyncTrinoClient - private isQueryCollapsed: boolean = false constructor(props: QueryCellProps) { super(props) @@ -34,6 +48,10 @@ class QueryCell extends React.Component { errorMessage: '', currentQuery: this.props.queries.getCurrentQuery(), runningQuery: undefined, + editingTitle: false, + editingCatalog: false, + editingSchema: false, + editorCollapsed: false, } this.queryRunner = new AsyncTrinoClient() this.setupQueryRunner() @@ -50,13 +68,19 @@ class QueryCell extends React.Component { shouldComponentUpdate(nextProps: QueryCellProps, nextState: QueryCellState) { // Only update if the ResultSet-related props have changed return ( + this.props.drawerOpen !== nextProps.drawerOpen || + this.props.height !== nextProps.height || this.state.results !== nextState.results || this.state.columns !== nextState.columns || this.state.response !== nextState.response || this.state.errorMessage !== nextState.errorMessage || this.state.runningQuery !== nextState.runningQuery || this.state.currentQuery !== nextState.currentQuery || - this.state.currentQuery.title !== nextState.currentQuery.title + this.state.currentQuery.title !== nextState.currentQuery.title || + this.state.editingTitle !== nextState.editingTitle || + this.state.editingCatalog !== nextState.editingCatalog || + this.state.editingSchema !== nextState.editingSchema || + this.state.editorCollapsed !== nextState.editorCollapsed ) } @@ -109,6 +133,14 @@ class QueryCell extends React.Component { this.props.queries.updateQuery(this.state.currentQuery.id, { title: title }) } + handleCatalogChange = (catalog: string) => { + this.props.queries.updateQuery(this.state.currentQuery.id, { catalog: catalog }) + } + + handleSchemaChange = (schema: string) => { + this.props.queries.updateQuery(this.state.currentQuery.id, { schema: schema }) + } + ClearResults() { this.setState({ results: [], columns: [], errorMessage: '' }) } @@ -132,11 +164,63 @@ class QueryCell extends React.Component { } toggleQueryCollapse = () => { - const queryEditor = document.getElementById('query-editor') - if (queryEditor) { - this.isQueryCollapsed = !this.isQueryCollapsed - queryEditor.style.display = this.isQueryCollapsed ? 'none' : 'block' + this.setState({ editorCollapsed: !this.state.editorCollapsed }) + } + + private renderEditableTextField( + key: 'editingTitle' | 'editingCatalog' | 'editingSchema', + value: string | undefined, + options: { + typographyProps?: TypographyProps + textFieldProps?: TextFieldProps + displayContent?: ReactNode + } = {} + ) { + const { typographyProps = {}, textFieldProps = {}, displayContent } = options + const isEditing = this.state[key] + + if (isEditing) { + const { onChange, onKeyDown, onBlur, autoFocus, ...restTextFieldProps } = textFieldProps + + return ( + { + onChange?.(event) + }} + onKeyDown={(event) => { + onKeyDown?.(event) + if (!event.defaultPrevented && (event.key === 'Enter' || event.key === 'Escape')) { + this.setState({ [key]: false } as Pick) + } + }} + onBlur={(event) => { + onBlur?.(event) + this.setState({ [key]: false } as Pick) + }} + autoFocus={autoFocus ?? true} + /> + ) } + + const { onClick, ...restTypographyProps } = typographyProps + + return ( + { + onClick?.(event) + if (!event.defaultPrevented) { + this.setState({ [key]: true } as Pick) + } + }} + > + {displayContent ?? value} + + ) } render() { @@ -146,103 +230,100 @@ class QueryCell extends React.Component { response.stats !== undefined && (response.stats.state === 'RUNNING' || response.stats.state === 'QUEUED') + const availablePanelHeight = Math.max(this.props.height - TOOLBAR_HEIGHT, 0) + const resultSetHeight = this.state.editorCollapsed ? availablePanelHeight : availablePanelHeight / 2 + return ( - <> -
-
- - this.handleTitleChange(e.target.value)} - value={currentQuery.title} - /> - - this.props.queries.updateQuery(this.state.currentQuery.id, { catalog: e.target.value }) - } - value={currentQuery.catalog ?? ''} - /> - - this.props.queries.updateQuery(this.state.currentQuery.id, { schema: e.target.value }) - } - value={currentQuery.schema ?? ''} - /> - -
- -
- -
- -
-
-
+ + + + + + this.Execute()} + > + {!isQueryRunning ? : } + + {this.renderEditableTextField('editingTitle', currentQuery.title, { + typographyProps: { + variant: 'h6', + sx: { ml: 2 }, + }, + textFieldProps: { + sx: { maxWidth: 200 }, + onChange: (event) => this.handleTitleChange(event.target.value), + }, + })} + + + + + Catalog: + + {this.renderEditableTextField('editingCatalog', currentQuery.catalog ?? '', { + typographyProps: { + sx: { ml: 2, maxWidth: 200, fontFamily: 'monospace' }, + noWrap: true, + }, + textFieldProps: { + sx: { + maxWidth: 200, + '& .MuiInputBase-input': { fontFamily: 'monospace' }, + }, + onChange: (event) => this.handleCatalogChange(event.target.value), + }, + displayContent: + currentQuery.catalog && currentQuery.catalog.length > 0 ? ( + currentQuery.catalog + ) : ( + + <no-catalog> + + ), + })} + + + + + Schema: + + {this.renderEditableTextField('editingSchema', currentQuery.schema ?? '', { + typographyProps: { + sx: { ml: 2, maxWidth: 200, fontFamily: 'monospace' }, + noWrap: true, + }, + textFieldProps: { + sx: { + maxWidth: 200, + '& .MuiInputBase-input': { fontFamily: 'monospace' }, + }, + onChange: (event) => this.handleSchemaChange(event.target.value), + }, + displayContent: + currentQuery.schema && currentQuery.schema.length > 0 ? ( + currentQuery.schema + ) : ( + + <no-schema> + + ), + })} + + + + {this.state.editorCollapsed ? : } + + + + {}} @@ -250,19 +331,21 @@ class QueryCell extends React.Component { queries={this.props.queries} catalog={currentQuery.catalog} schema={currentQuery.schema} + theme={this.props.theme} + maxHeight={availablePanelHeight} /> -
-
- this.ClearResults()} - /> -
- + {this.props.theme != 'dark' && } + + this.ClearResults()} + /> + ) } } diff --git a/precise/src/QueryEditor.tsx b/precise/src/QueryEditor.tsx index 328b012..2bad7a7 100644 --- a/precise/src/QueryEditor.tsx +++ b/precise/src/QueryEditor.tsx @@ -1,33 +1,109 @@ -import React from 'react' +import React, { useRef, useState } from 'react' +import { styled } from '@mui/material/styles' +import { Box, Drawer, useMediaQuery } from '@mui/material' +import CssBaseline from '@mui/material/CssBaseline' +import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar' +import { ThemeProvider } from '@mui/material/styles' import QueryCell from './QueryCell' +import { darkTheme, lightTheme } from './theme' import Queries from './schema/Queries' +import QueryInfo from './schema/QueryInfo' import CatalogViewer from './controls/catalog_viewer/CatalogViewer' -import './style/components.css' -import './style/control.css' -import './style/layout.css' -import './style/normalize.css' -import './style/query-editor.css' -import './style/results.css' -import './style/theme.css' - -type QueryEditorProps = Record - -interface QueryEditorState { - queries: Queries + +interface IQueryEditor { + height: number + theme?: 'dark' | 'light' + enableCatalogSearchColumns?: boolean } -class QueryEditor extends React.Component { - constructor(props: QueryEditorProps) { - super(props) +const DRAWER_WIDTH = 260 + +const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{ + open?: boolean +}>(({ theme }) => ({ + flexGrow: 1, + padding: theme.spacing(3), + transition: theme.transitions.create('margin', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + marginLeft: 0, + variants: [ + { + props: ({ open }) => open, + style: { + transition: theme.transitions.create('margin', { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + marginLeft: `${DRAWER_WIDTH}px`, + }, + }, + ], +})) + +interface AppBarProps extends MuiAppBarProps { + open?: boolean +} - this.state = { - queries: new Queries(), +const AppBar = styled(MuiAppBar, { + shouldForwardProp: (prop) => prop !== 'open', +})(({ theme }) => ({ + position: 'absolute', + boxShadow: 'none', + borderBottom: `1px solid ${theme.palette.divider}`, + transition: theme.transitions.create(['margin', 'width'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + variants: [ + { + props: ({ open }) => open, + style: { + width: `calc(100% - ${DRAWER_WIDTH}px)`, + marginLeft: `${DRAWER_WIDTH}px`, + transition: theme.transitions.create(['margin', 'width'], { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + }, + }, + ], +})) + +export const QueryEditor = ({ height, theme, enableCatalogSearchColumns }: IQueryEditor) => { + const [queries, setQueries] = useState(() => new Queries()) + const [drawerOpen, setDrawerOpen] = useState(true) + const [queryRunning, setQueryRunning] = useState(false) + const [currentQuery, setCurrentQuery] = useState(queries.getCurrentQuery()) + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)') + const containerRef = useRef(null) + + const muiThemeToUse = () => { + if (theme === 'dark') { + return darkTheme + } else if (theme === 'light') { + return lightTheme + } else if (prefersDarkMode) { + return darkTheme + } else { + return lightTheme } } - setQueryContent = (query: string, catalog?: string, schema?: string) => { - const currentQuery = this.state.queries.getCurrentQuery() - const updates: any = {} + const applyQueryUpdates = (updates: Partial) => { + const activeQuery = queries.getCurrentQuery() + + if (!activeQuery) { + return + } + + queries.updateQuery(activeQuery.id, updates) + setCurrentQuery((prev) => ({ ...prev, ...updates })) + } + + const setQueryContent = (query: string, catalog?: string, schema?: string) => { + const updates: Partial = {} if (query) { updates.query = query @@ -41,79 +117,92 @@ class QueryEditor extends React.Component { updates.schema = schema } - this.state.queries.updateQuery(currentQuery.id, updates) + applyQueryUpdates(updates) } - appendQueryContent = (query: string, catalog?: string, schema?: string) => { - const currentQuery = this.state.queries.getCurrentQuery() - const updates: any = {} + const appendQueryContent = (query: string, catalog?: string, schema?: string) => { + const activeQuery = queries.getCurrentQuery() + const updates: Partial = {} - if (query) { - // Append to existing query, adding newlines as needed - const existingQuery = currentQuery.query || '' - const separator = existingQuery.trim() === '' ? '' : '\n\n' + if (query !== undefined) { + const existingQuery = activeQuery.query || '' + const separator = existingQuery.trim() === '' || query.trim() === '' ? '' : '\n\n' updates.query = existingQuery + separator + query } - if (catalog) { + if (catalog !== undefined) { updates.catalog = catalog } - if (schema) { + if (schema !== undefined) { updates.schema = schema } - this.state.queries.updateQuery(currentQuery.id, updates) + applyQueryUpdates(updates) } - render() { - return ( -
-
-
-
-
-
- -
-
- - - -
-
-
- -
-
-
-
- ) - } + return ( + + + + + + + setDrawerOpen(false)} + enableSearchColumns={enableCatalogSearchColumns} + /> + + +
+ setDrawerOpen(true)} + theme={theme} + /> +
+
+
+ ) } export default QueryEditor diff --git a/precise/src/QueryEditorPane.tsx b/precise/src/QueryEditorPane.tsx index 356c7df..3679fec 100644 --- a/precise/src/QueryEditorPane.tsx +++ b/precise/src/QueryEditorPane.tsx @@ -1,4 +1,8 @@ import React from 'react' +import { Box, Stack, Tooltip, IconButton } from '@mui/material' +import CodeIcon from '@mui/icons-material/Code' +import Maximize from '@mui/icons-material/Maximize' +import Minimize from '@mui/icons-material/Minimize' import Editor from '@monaco-editor/react' import * as monaco from 'monaco-editor/esm/vs/editor/editor.api' import Queries from './schema/Queries' @@ -20,26 +24,27 @@ import Column from './schema/Column' import NamedQuery from './sql/NamedQuery' import { tokenMap } from './sql/TokenMap' import SubstitutionEditor from './SubstitutionEditor' -import './style/query-editor.css' import { format } from 'sql-formatter' -import { Code, Maximize2, Minimize2 } from 'lucide-react' const TRINO_SQL_LANGUAGE = 'trinosql' +const TABS_HEIGHT = 64 interface QueryEditorPaneProps { queries: Queries + maxHeight: number onQueryChange: (query: string) => void onSelectChange: (selectedText: string) => void onExecute: () => void catalog?: string schema?: string + theme?: string } interface QueryEditorPaneState { currentQuery: QueryInfo | null substitutions: Record isMaximized: boolean - height: string + height: number width: string } @@ -106,7 +111,7 @@ class QueryEditorPane extends React.Component { - const newQuery = this.props.queries.addQuery(false, 'New Query') + const newQuery = this.props.queries.addQuery(false, 'New query') monaco.editor.createModel('', TRINO_SQL_LANGUAGE, monaco.Uri.parse(`file:///${newQuery.id}`)) this.props.queries.setCurrentQuery(newQuery.id) return newQuery.id @@ -177,8 +188,7 @@ class QueryEditorPane extends React.Component { this.setState((prevState) => ({ isMaximized: !prevState.isMaximized, - // 2.5 em for the tab bar, 3em for the substitution editor, 3em for the brand bar - height: !prevState.isMaximized ? 'calc(100vh - 2.5em - 3em - 3.5em)' : '40vh', + height: !prevState.isMaximized ? this.props.maxHeight : this.props.maxHeight / 2, width: '100%', })) } @@ -851,8 +861,8 @@ class QueryEditorPane extends React.Component { - this.setState({ height: newHeight }) + handleHeightChange = (maxHeight: number) => { + this.setState({ height: this.state.isMaximized ? maxHeight : maxHeight / 2 }) } render() { @@ -876,36 +886,51 @@ class QueryEditorPane extends React.Component -
-
-
- - -
+ + + + -
-
+ + ) } diff --git a/precise/src/ResultSet.tsx b/precise/src/ResultSet.tsx index 46da4ac..fbc4c6a 100644 --- a/precise/src/ResultSet.tsx +++ b/precise/src/ResultSet.tsx @@ -1,17 +1,33 @@ import React from 'react' -import QueryInfo from './schema/QueryInfo' +import { + Alert, + Box, + CircularProgress, + LinearProgress, + Link, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableFooter, + TableHead, + TableRow, + Typography, +} from '@mui/material' +import { DataGrid, GridColDef } from '@mui/x-data-grid' +import Chip, { ChipProps } from '@mui/material/Chip' import ReactDOMServer from 'react-dom/server' -import ErrorBox from './utils/ErrorBoxProvider' -import ProgressBar from './utils/ProgressBar' import CopyLink from './utils/CopyLink' import ClearButton from './utils/ClearButton' -import './style/results.css' interface ResultSetProps { - queryInfo: QueryInfo | undefined + queryId: string | undefined results: any[] columns: any[] response: any + height: number errorMessage: string onClearResults: (queryId: string | undefined) => void } @@ -21,6 +37,38 @@ class ResultSet extends React.Component { statsHistory: any[] = [] lastQueryId: string | undefined = undefined + static readonly STATE_COLOR_MAP: Record = { + QUEUED: 'default', + RUNNING: 'info', + PLANNING: 'info', + FINISHED: 'success', + BLOCKED: 'secondary', + USER_ERROR: 'error', + CANCELED: 'warning', + INSUFFICIENT_RESOURCES: 'error', + EXTERNAL_ERROR: 'error', + UNKNOWN_ERROR: 'error', + } + + getQueryStateColor(queryState: string): ChipProps['color'] { + switch (queryState) { + case 'QUEUED': + return ResultSet.STATE_COLOR_MAP.QUEUED + case 'PLANNING': + return ResultSet.STATE_COLOR_MAP.PLANNING + case 'STARTING': + case 'FINISHING': + case 'RUNNING': + return ResultSet.STATE_COLOR_MAP.RUNNING + case 'FAILED': + return ResultSet.STATE_COLOR_MAP.UNKNOWN_ERROR + case 'FINISHED': + return ResultSet.STATE_COLOR_MAP.FINISHED + default: + return ResultSet.STATE_COLOR_MAP.QUEUED + } + } + renderHeader(columns: any) { return ( @@ -60,11 +108,27 @@ class ResultSet extends React.Component { ) } - renderTable = (results: any[], response: any, columns: any) => { + renderTable = (results: any[], columns: any) => { + const muiColumns: GridColDef[] = columns.map((column: any) => ({ field: column.name, minWidth: 150 })) + const muiRows = results + .flat() + .map((row: any[], i: number) => + Object.fromEntries([ + ['mui-row-id', `row-${i + 1}`], + ...columns.map((c: any, j: number) => [c.name, row[j]]), + ]) + ) + return ( -
30 ? 'scrollable' : 'result-table-container'}> - {this.renderInnerTable(results, response, columns)} -
+ String(row['mui-row-id'])} + density="compact" + /> ) } @@ -214,14 +278,14 @@ class ResultSet extends React.Component { } render() { - const { queryInfo, results, columns, response, errorMessage } = this.props + const { queryId, results, columns, response, height, errorMessage } = this.props // if the query ID has changed, reset the last processed rows and elapsed time - if (queryInfo == null || this.lastQueryId !== queryInfo.id) { + if (this.lastQueryId !== queryId) { this.reset() } - this.lastQueryId = queryInfo?.id + this.lastQueryId = queryId // new implementation, look over 10 second window in stats history let processedRowsSinceLast = 0 @@ -271,239 +335,289 @@ class ResultSet extends React.Component { // Ensure the 'result-set' class is applied to the container return ( -
- { - // only return if there are columns - columns && columns.length ? ( -
- {response.stats && this.isFinishedFailedOrCancelled(response.stats.state) && ( - this.props.onClearResults(queryInfo?.id)} /> - )} - this.copy()} /> - {/* if row count > 30 place in scrollable div */} - {this.renderTable(results, response, columns)} -
- ) : null - } + {response && response.id ? ( -
- {this.getRowCount()} rows:{' '} - - {response.id} - -
+ + {errorMessage ? ( + + {errorMessage} + + ) : null} + + + {this.getRowCount()} rows: + + + {response.id} + + {columns && columns.length ? ( + response.stats && this.isFinishedFailedOrCancelled(response.stats.state) ? ( + <> + this.props.onClearResults(queryId)} /> + this.copy()} /> + + ) : null + ) : null} + + ) : null} - {errorMessage ? : null} {/* if the status is not finished, show spinner */} {response && response.stats && response.stats.state !== 'FINISHED' && response.stats.state !== 'FAILED' && response.stats.state !== 'CANCELLED' ? ( -
-
-
-
-
- {response && response.stats && response.stats.runningPercentage - ? Math.floor(response.stats.runningPercentage) - : 0} - % -
-
- {response.stats.state} -
+ <> + + + + {response && response.stats && response.stats.progressPercentage + ? Math.floor(response.stats.progressPercentage) + : 0} + % + + + + Workers: {response.stats.nodes}, Running splits: {response.stats.runningSplits}, Total splits: {response.stats.totalSplits}, Run time:{' '} {Math.floor(response.stats.elapsedTimeMillis / 1000)}s -
-
-
-
-
- -
- {this.formatMillisAsHHMMSS(response.stats.elapsedTimeMillis)} -
-
-
- {/* if response.stats.subStages */} - {response.stats.rootStage && response.stats.rootStage.subStages ? ( -
- - - - {' '} - {/* groupings for subcategories of metrics */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {/* look at all the substages in subStages */} - {stages.map((subStageInfo: any) => { - return ( - + + + + + + {this.formatMillisAsHHMMSS(response.stats.elapsedTimeMillis)} + + + + {response.stats.rootStage && response.stats.rootStage.subStages ? ( + +
- Rows - - Bytes - - Splits -
StageNodesProcessedRateCurrent RateProcessedRateCurrent RateInput ProcessedQueuedRunningDone
0 (root){response.stats.rootStage.nodes} - {this.rowCountToCorrectScale(response.stats.rootStage.processedRows)} - - {this.rowCountToCorrectScale( - response.stats.rootStage.processedRows / - (response.stats.rootStage.wallTimeMillis / 1000) - )} - - {this.bytesToCorrectScale(response.stats.rootStage.processedBytes)} - - {this.bytesToCorrectScale( - response.stats.rootStage.processedBytes / - (response.stats.wallTimeMillis / 1000) - )} - - {this.bytesToCorrectScale(response.stats.rootStage.physicalInputBytes)} - - {response.stats.rootStage.queuedSplits} - - {response.stats.rootStage.runningSplits} - - {response.stats.rootStage.completedSplits} -
*': { + borderRight: (theme) => `1px solid ${theme.palette.divider}`, + }, + '& .MuiTableRow-root > *:last-of-type': { + borderRight: 'none', + }, + }} + size="small" + stickyHeader + > + + + + Query run metrics + + + Rows + + + Bytes + + + Splits + + + + Stage + Nodes + Processed + Rate + Current Rate + Processed + Rate + Current Rate + Input Processed + Queued + Running + Done + + + + + 0 (root) + {response.stats.rootStage.nodes} + + {this.rowCountToCorrectScale( + response.stats.rootStage.processedRows + )} + + + {this.rowCountToCorrectScale( + response.stats.rootStage.processedRows / + (response.stats.rootStage.wallTimeMillis / 1000) + )} + + + + {this.bytesToCorrectScale(response.stats.rootStage.processedBytes)} + + + {this.bytesToCorrectScale( + response.stats.rootStage.processedBytes / + (response.stats.wallTimeMillis / 1000) + )} + + + + {this.bytesToCorrectScale( + response.stats.rootStage.physicalInputBytes + )} + + + {response.stats.rootStage.queuedSplits} + + + {response.stats.rootStage.runningSplits} + + + {response.stats.rootStage.completedSplits} + + + + {/* Sub-stages */} + {stages.map((subStageInfo: any) => ( + - - - - - - - - - - - - - - ) - })} - - - - - - - - - - - - - - - -
- {subStageInfo.stage.stageId} - {subStageInfo.stage.nodes} + {subStageInfo.stage.stageId} + {subStageInfo.stage.nodes} + {this.rowCountToCorrectScale(subStageInfo.stage.processedRows)} - + + {this.rowCountToCorrectScale( subStageInfo.stage.processedRows / (subStageInfo.stage.wallTimeMillis / 1000) )} - + + + {this.bytesToCorrectScale(response.stats.processedBytes)} - + + {this.bytesToCorrectScale( subStageInfo.stage.processedBytes / (subStageInfo.stage.wallTimeMillis / 1000) )} - + + + {this.bytesToCorrectScale(response.stats.physicalInputBytes)} - + + {subStageInfo.stage.queuedSplits} - + + {subStageInfo.stage.runningSplits} - + + {subStageInfo.stage.completedSplits} -
Total{response.stats.nodes} - {this.rowCountToCorrectScale(response.stats.processedRows)} - - {this.rowCountToCorrectScale( - response.stats.processedRows / - (response.stats.elapsedTimeMillis / 1000) - )} - - {this.rowCountToCorrectScale(processedRowsSinceLast)} - - {this.bytesToCorrectScale(response.stats.processedBytes)} - - {this.bytesToCorrectScale( - response.stats.processedBytes / - (response.stats.elapsedTimeMillis / 1000) - )} - - {this.bytesToCorrectScale(response.stats.physicalInputBytes)} - {response.stats.queuedSplits}{response.stats.runningSplits}{response.stats.completedSplits}
-
- ) : null} -
+ + + ))} + + + theme.typography.fontWeightBold, + color: 'text.primary', + }, + }} + > + {/* Totals */} + + Total + {response.stats.nodes} + + {this.rowCountToCorrectScale(response.stats.processedRows)} + + + {this.rowCountToCorrectScale( + response.stats.processedRows / + (response.stats.elapsedTimeMillis / 1000) + )} + + + {this.rowCountToCorrectScale(processedRowsSinceLast)} + + + {this.bytesToCorrectScale(response.stats.processedBytes)} + + + {this.bytesToCorrectScale( + response.stats.processedBytes / + (response.stats.elapsedTimeMillis / 1000) + )} + + + + {this.bytesToCorrectScale(response.stats.physicalInputBytes)} + + {response.stats.queuedSplits} + {response.stats.runningSplits} + {response.stats.completedSplits} + + + + + ) : null} +
+ + ) : columns && columns.length ? ( + + {this.renderTable(results, columns)} + ) : null} -
+ ) } } diff --git a/precise/src/SubstitutionEditor.tsx b/precise/src/SubstitutionEditor.tsx index da81681..c5e1467 100644 --- a/precise/src/SubstitutionEditor.tsx +++ b/precise/src/SubstitutionEditor.tsx @@ -1,4 +1,6 @@ import React, { useState, useEffect } from 'react' +import { Box, Stack, Typography, TextField, InputAdornment, IconButton } from '@mui/material' +import ClearIcon from '@mui/icons-material/Clear' interface SubstitutionField { name: string @@ -60,21 +62,52 @@ const SubstitutionEditor: React.FC = ({ query, onSubsti } return ( -
-

Query Parameters

- {fields.map((field) => ( -
- - handleInputChange(field.name, e.target.value)} - placeholder={field.defaultValue} - /> -
- ))} -
+ + Query Parameters + + {fields.map((field) => { + const isNumber = field.type !== 'varchar' + const val = values[field.name] ?? '' + + return ( + handleInputChange(field.name, e.target.value)} + placeholder={field.defaultValue !== undefined ? String(field.defaultValue) : ''} + slotProps={{ + input: { + // helpful for numeric types + inputProps: isNumber ? { step: field.type === 'double' ? 'any' : 1 } : undefined, + // optional clear button (keeps width stable when empty) + endAdornment: ( + + handleInputChange(field.name, '')} + sx={{ + visibility: val ? 'visible' : 'hidden', + '& .MuiSvgIcon-root': { fontSize: 16 }, + }} + > + + + + ), + }, + }} + /> + ) + })} + + ) } diff --git a/precise/src/assets/close.png b/precise/src/assets/close.png deleted file mode 100644 index cbd90be..0000000 Binary files a/precise/src/assets/close.png and /dev/null differ diff --git a/precise/src/assets/pin_down.png b/precise/src/assets/pin_down.png deleted file mode 100644 index 1039779..0000000 Binary files a/precise/src/assets/pin_down.png and /dev/null differ diff --git a/precise/src/assets/pin_up.png b/precise/src/assets/pin_up.png deleted file mode 100644 index 1d3ce9b..0000000 Binary files a/precise/src/assets/pin_up.png and /dev/null differ diff --git a/precise/src/controls/catalog_viewer/CatalogViewer.tsx b/precise/src/controls/catalog_viewer/CatalogViewer.tsx index fecee1b..32aac0b 100644 --- a/precise/src/controls/catalog_viewer/CatalogViewer.tsx +++ b/precise/src/controls/catalog_viewer/CatalogViewer.tsx @@ -1,21 +1,45 @@ import React, { useState, useEffect, useCallback, useRef } from 'react' +import { + Alert, + AlertTitle, + Box, + Checkbox, + Chip, + CircularProgress, + Grid, + FormControlLabel, + IconButton, + LinearProgress, + TextField, + Typography, + Divider, +} from '@mui/material' +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' +import ChevronRightIcon from '@mui/icons-material/ChevronRight' +import RefreshIcon from '@mui/icons-material/Refresh' +import StorageOutlinedIcon from '@mui/icons-material/StorageOutlined' +import { SimpleTreeView, TreeItem } from '@mui/x-tree-view' +import CatalogViewerSchema from './CatalogViewerSchema' import SchemaProvider from './../../sql/SchemaProvider' import Catalog from './../../schema/Catalog' import TableReference from '../../schema/TableReference' -import CatalogViewerSchema from './CatalogViewerSchema' -import ErrorBox from './../../utils/ErrorBoxProvider' -import CloseIcon from '../../assets/close.png' import { ViewerStateManager, buildPath } from './ViewerState' -import './catalogviewer.css' -import { Loader2, ChevronRight } from 'lucide-react' interface CatalogViewerProps { initialFilterText?: string onGenerateQuery?: (query: string, catalog?: string, schema?: string) => void onAppendQuery?: (query: string, catalog?: string, schema?: string) => void + onDrawerToggle?: () => void + enableSearchColumns?: boolean } -const CatalogViewer: React.FC = ({ initialFilterText = '', onGenerateQuery, onAppendQuery }) => { +const CatalogViewer: React.FC = ({ + initialFilterText = '', + onGenerateQuery, + onAppendQuery, + onDrawerToggle, + enableSearchColumns, +}) => { // Basic state const [catalogs, setCatalogs] = useState>(new Map()) const [errorMessage, setErrorMessage] = useState() @@ -70,8 +94,8 @@ const CatalogViewer: React.FC = ({ initialFilterText = '', o try { await SchemaProvider.populateCatalogsAndRefreshTableList( - () => { - setCatalogs(SchemaProvider.catalogs) + (nextCatalogs) => { + setCatalogs(nextCatalogs) setIsLoading(false) }, (error: string) => { @@ -153,91 +177,184 @@ const CatalogViewer: React.FC = ({ initialFilterText = '', o } return ( -
-
-
- + `calc(${theme.mixins.toolbar.minHeight}px + ${theme.spacing(1)})`, + px: 0, + py: 0, + }} + > + + setFilterText(e.target.value)} - className="filter-input" + fullWidth /> - {filterText && ( -
setFilterText('')} - role="button" - aria-label="Clear search" - > - Clear -
- )} -
- - - -
- - {isLoading &&
Loading catalogs...
} - - {errorMessage && } - -
- {Array.from(catalogs.values()) - .sort((a, b) => a.getName().localeCompare(b.getName())) - .map((catalog: Catalog) => { - const catalogName = catalog.getName() - const catalogPath = buildPath.catalog(catalogName) - - if (filterText && !isVisible(catalogPath)) { - return null - } - - return ( -
-
handleToggle(catalogPath)}> -
- {catalogName} - {catalog.getType()} catalog -
- + + + + + + + {enableSearchColumns && ( + + setSearchColumns(e.target.checked)} + /> + } + label={ + + - {isExpanded(catalogPath) ? 'â–¼' : 'â–¶'} - -
handleGenerateCatalogQuery(e, catalogName)} - title="Set this catalog as default catalog" - > - -
-
- - {isExpanded(catalogPath) && ( -
+ Search columns + + {isLoadingColumns && } + + } + /> + + )} + + + + {errorMessage && ( + + Catalog Viewer + {errorMessage} + + )} + + { + handleToggle(itemId) + }} + > + {Array.from(catalogs.values()) + .sort((a, b) => a.getName().localeCompare(b.getName())) + .map((catalog: Catalog) => { + const catalogName = catalog.getName() + const catalogPath = buildPath.catalog(catalogName) + + if (filterText && !isVisible(catalogPath)) { + return null + } + + return ( + + {catalogName} + {catalog.getType() === 'system' && ( + + )} + + handleGenerateCatalogQuery(e, catalogName)} + disabled={isLoading} + > + + + + } + slotProps={{ + label: { + style: { + overflow: 'visible', + }, + }, + }} + > + {catalog.getError() && ( - + + Catalog Viewer + {catalog.getError()} + )} {Array.from(catalog.getSchemas().values()) .sort((a, b) => a.getName().localeCompare(b.getName())) .map((schema) => { - const schemaPath = buildPath.schema(catalogName, schema.getName()) + const schemaName = schema.getName() + const schemaPath = buildPath.schema(catalogName, schemaName) return ( = ({ initialFilterText = '', o catalogName={catalogName} schema={schema} filterText={filterText} - isExpanded={isExpanded(schemaPath)} isVisible={isVisible} + isLoading={isLoading} hasMatchingChildren={hasMatchingChildren} - onToggle={handleToggle} onGenerateQuery={handleGenerateQuery} /> ) })} -
- )} -
- ) - })} -
-
+ + + ) + })} + + + ) } diff --git a/precise/src/controls/catalog_viewer/CatalogViewerColumn.tsx b/precise/src/controls/catalog_viewer/CatalogViewerColumn.tsx index 201510b..0b8fcad 100644 --- a/precise/src/controls/catalog_viewer/CatalogViewerColumn.tsx +++ b/precise/src/controls/catalog_viewer/CatalogViewerColumn.tsx @@ -1,123 +1,63 @@ -import React, { useState, useEffect } from 'react' +import React from 'react' +import { Box, Stack, Typography } from '@mui/material' +import { TreeItem } from '@mui/x-tree-view' import Column from '../../schema/Column' import TableReference from './../../schema/TableReference' -import CopyLink from './../../utils/CopyLink' import { buildPath } from './ViewerState' -import './catalogviewer.css' interface CatalogViewerColumnProps { tableRef: TableReference column: Column - isExpanded: boolean isVisible: (path: string) => boolean - onToggle: (path: string) => Promise } -const CatalogViewerColumn: React.FC = ({ - tableRef, - column, - isExpanded, - isVisible, - onToggle, -}) => { - const [sampleValues, setSampleValues] = useState([]) - const [isLoadingSamples, setIsLoadingSamples] = useState(false) - const [showSamples, setShowSamples] = useState(false) - +const CatalogViewerColumn: React.FC = ({ tableRef, column, isVisible }) => { const columnPath = buildPath.column(tableRef.catalogName, tableRef.schemaName, tableRef.tableName, column.getName()) - // Load sample values when samples are shown - useEffect(() => { - if (showSamples && sampleValues.length === 0 && !isLoadingSamples) { - setIsLoadingSamples(true) - column.getSampleValues(tableRef, (newSampleValues: string[]) => { - setSampleValues(newSampleValues) - setIsLoadingSamples(false) - }) - } - }, [showSamples, column, tableRef, sampleValues.length, isLoadingSamples]) - - const handleRefresh = (e: React.MouseEvent) => { - e.stopPropagation() - setIsLoadingSamples(true) - column.getSampleValues(tableRef, (newSampleValues: string[]) => { - setSampleValues(newSampleValues) - setIsLoadingSamples(false) - }) - } - - const handleToggle = async () => { - // Toggle samples if already expanded - if (isExpanded) { - setShowSamples(!showSamples) - } else { - // Just expand the column initially - await onToggle(columnPath) - setShowSamples(true) - } - } - - const renderSampleValue = (value: string, index: number) => { - if (value === null) { - return ( -
- NULL -
- ) - } - - const displayValue = value.length > 50 ? value.substring(0, 50) + '...' : value - - return ( -
- {displayValue} - navigator.clipboard.writeText(value)} /> -
- ) - } - // Check visibility using the passed down helper if (!isVisible(columnPath)) { return null } return ( -
-
- {showSamples && ( -
-
-
- )} -
- {column.getName()} - {column.getType()} - {showSamples ? 'â–¼' : 'â–¶'} -
- {column.getExtraOrComment() && ( -
- {column.getExtraOrComment()} -
- )} -
- - {showSamples && ( -
-
- Sample values: -
-
- {isLoadingSamples ? ( -
Loading samples...
- ) : sampleValues.length === 0 ? ( -
No sample values available
- ) : ( - sampleValues.map((value, index) => renderSampleValue(value, index)) - )} -
-
- )} -
+ + + + + {column.getName()} + + + {column.getType()} + + + + + } + slotProps={{ + label: { + style: { + overflow: 'visible', + }, + }, + }} + /> ) } diff --git a/precise/src/controls/catalog_viewer/CatalogViewerSchema.tsx b/precise/src/controls/catalog_viewer/CatalogViewerSchema.tsx index 90857b4..d348217 100644 --- a/precise/src/controls/catalog_viewer/CatalogViewerSchema.tsx +++ b/precise/src/controls/catalog_viewer/CatalogViewerSchema.tsx @@ -1,20 +1,21 @@ import React from 'react' +import { Box, IconButton, Typography } from '@mui/material' +import { TreeItem } from '@mui/x-tree-view' +import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined' +import ChevronRightIcon from '@mui/icons-material/ChevronRight' import Schema from '../../schema/Schema' import Table from '../../schema/Table' import CatalogViewerTable from './CatalogViewerTable' import TableReference from '../../schema/TableReference' import { buildPath } from './ViewerState' -import './catalogviewer.css' -import { ChevronRight } from 'lucide-react' interface SchemaProps { catalogName: string schema: Schema filterText: string - isExpanded: boolean isVisible: (path: string) => boolean + isLoading: boolean hasMatchingChildren: (path: string) => boolean - onToggle: (path: string) => Promise onGenerateQuery?: ( queryType: string, tableRef: TableReference | null, @@ -27,10 +28,9 @@ const CatalogViewerSchema: React.FC = ({ catalogName, schema, filterText, - isExpanded, isVisible, + isLoading, hasMatchingChildren, - onToggle, onGenerateQuery, }) => { const schemaPath = buildPath.schema(catalogName, schema.getName()) @@ -48,51 +48,60 @@ const CatalogViewerSchema: React.FC = ({ } return ( -
-
onToggle(schemaPath)}> - {schema.getName()} - schema - - {isExpanded ? 'â–¼' : 'â–¶'} - + {schema.getName()} - {onGenerateQuery && ( -
- -
- )} -
- - {isExpanded && ( -
- {Array.from(schema.getTables().values()) - .sort((a: Table, b: Table) => a.getName().localeCompare(b.getName())) - .map((table: Table) => { - const tablePath = buildPath.table(catalogName, schema.getName(), table.getName()) + + + + } + slotProps={{ + label: { + style: { + overflow: 'visible', + }, + }, + }} + > + {Array.from(schema.getTables().values()) + .sort((a: Table, b: Table) => a.getName().localeCompare(b.getName())) + .map((table: Table) => { + const tablePath = buildPath.table(catalogName, schema.getName(), table.getName()) - // Table visibility is handled within the component - return ( - - ) - })} -
- )} -
+ return ( + + ) + })} +
) } diff --git a/precise/src/controls/catalog_viewer/CatalogViewerTable.tsx b/precise/src/controls/catalog_viewer/CatalogViewerTable.tsx index 9283651..1b077a6 100644 --- a/precise/src/controls/catalog_viewer/CatalogViewerTable.tsx +++ b/precise/src/controls/catalog_viewer/CatalogViewerTable.tsx @@ -1,19 +1,22 @@ import React, { useState, useEffect } from 'react' +import { Alert, Box, IconButton, Typography } from '@mui/material' +import { TreeItem } from '@mui/x-tree-view' +import HourglassEmptyOutlinedIcon from '@mui/icons-material/HourglassEmptyOutlined' +import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined' +import TableRowsOutlined from '@mui/icons-material/TableRowsOutlined' import Table from '../../schema/Table' import SchemaProvider from '../../sql/SchemaProvider' import TableReference from '../../schema/TableReference' import CatalogViewerColumn from './CatalogViewerColumn' import { buildPath } from './ViewerState' -import './catalogviewer.css' -import { ChevronRight } from 'lucide-react' interface CatalogViewerTableProps { tableRef: TableReference filterText: string isExpanded: boolean isVisible: (path: string) => boolean + isLoading: boolean hasMatchingChildren: (path: string) => boolean - onToggle: (path: string) => Promise onGenerateQuery?: (queryType: string, tableRef: TableReference) => void } @@ -22,7 +25,7 @@ const CatalogViewerTable: React.FC = ({ filterText, isExpanded, isVisible, - onToggle, + isLoading, onGenerateQuery, }) => { const [table, setTable] = useState(() => new Table(tableRef.tableName)) @@ -63,60 +66,70 @@ const CatalogViewerTable: React.FC = ({ } return ( -
-
-
onToggle(tablePath)}> - {table.getName()} - table - {isExpanded ? 'â–¼' : 'â–¶'} + + {table.getName()} - {onGenerateQuery && ( -
- -
- )} -
-
+ + + + + } + slotProps={{ + label: { + style: { + overflow: 'visible', + }, + }, + }} + > + + {table.getError() ? ( + {table.getError()} + ) : table.getColumns().length === 0 && table.isLoading() ? null : ( + table.getColumns().length > 0 && + table.getColumns().map((column) => { + const columnPath = buildPath.column( + tableRef.catalogName, + tableRef.schemaName, + tableRef.tableName, + column.getName() + ) - {isExpanded && ( -
- {table.getError() ? ( -
{table.getError()}
- ) : table.getColumns().length === 0 && isExpanded && table.isLoading() ? ( -
Loading columns...
- ) : ( - table.getColumns().length > 0 && - table.getColumns().map((column) => { - const columnPath = buildPath.column( - tableRef.catalogName, - tableRef.schemaName, - tableRef.tableName, - column.getName() - ) + if (!isVisible(columnPath)) { + return null + } - if (!isVisible(columnPath)) { - return null - } - - return ( - - ) - }) - )} -
- )} -
+ return ( + + ) + }) + )} + + ) } diff --git a/precise/src/controls/catalog_viewer/ViewerState.ts b/precise/src/controls/catalog_viewer/ViewerState.ts index c02737b..9325a06 100644 --- a/precise/src/controls/catalog_viewer/ViewerState.ts +++ b/precise/src/controls/catalog_viewer/ViewerState.ts @@ -19,7 +19,7 @@ export const buildPath = { } export class ViewerStateManager { - private userExpanded = new Set() + public userExpanded = new Set() private matches = new Set() private onStateUpdate: StateUpdateCallback private isSearching = false diff --git a/precise/src/controls/catalog_viewer/catalogviewer.css b/precise/src/controls/catalog_viewer/catalogviewer.css deleted file mode 100644 index 8610726..0000000 --- a/precise/src/controls/catalog_viewer/catalogviewer.css +++ /dev/null @@ -1,204 +0,0 @@ -.filter-input { - width: 95%; - padding: 0.5em; - margin: 0.5em; - border: 1px solid #2b2b2b; - border-radius: 3px; - background-color: #181818; - color: #9d9d9d; - font-size: small; -} - -/* A plain text with no obvious element of it being a button */ -.reload-button { - border: 0; - background-color: transparent; - color: #bbb; - right: 20px; - position: absolute; - padding: 0; - margin: 0; - font-size: 2em; -} - -/* catalog explorer */ -.viewer_catalog { - font-weight: bold; - color: #fff; - cursor: pointer; -} - -.catalog-content { - padding-left: 0.5em; -} - -.viewer_catalog:hover { - color: #fff; - background-color: #689CC5; -} - -.viewer_catalog_body { - margin-left: 10px; - padding-left: 10px; - border-left: #444 1px solid; -} - -.viewer_schema { - font-weight: regular; - color: #bbb; - cursor: pointer; -} - -.viewer_schema:hover { - color: #fff; - background-color: #416480; -} - -.viewer-schema-body { - margin-left: 10px; - padding-left: 10px; - border-left: #444 1px solid; -} - -.viewer_table { - font-weight: regular; - color: #999; - cursor: pointer; - position: relative; - padding-right: 30px; -} - -.viewer_samplevalue { - color: #999; -} - -.viewer_table_body { - margin-left: 10px; - padding-left: 10px; - border-left: #888 1px solid; - padding-right: 10px; -} - -.viewer_samplevalues_body { - margin-left: 10px; - padding-left: 10px; - border-left: #444 1px solid; -} - -.viewer_table:hover { - color: #fff; - background-color: #334e64; -} - -/* monospaced text */ -.viewer_column { - font-family: monospace; - cursor: pointer; -} - -.viewer_column:hover { - color: #fff; - background-color: #334e64; - cursor: pointer; -} - -.loading-message { - color: #999; - font-size: x-small; -} - -.table-name { - padding-right: 0.25em; -} - -.schema-name { - padding-right: 0.25em; -} - -.catalog-viewer { - position: relative; -} - -.catalog-viewer-header { - padding-bottom: 0.5em; -} - -.catalog-name { - padding-right: 0.25em; -} - -.column-name { - padding-right: 0.25em; -} - -.expand-indicator { - color: #444; - font-size: x-small; - padding-left: 0.25em; -} - -/* Add to catalogviewer.css */ -.search-container { - position: relative; - display: flex; - flex: 1; - align-items: center; -} - -/* input inside the search container needs to have text cursor */ -.search-container > .filter-input { - cursor: text !important; -} - -.clear-search { - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - cursor: pointer; - opacity: 0.6; - transition: opacity 0.2s; - padding: 4px; - display: flex; - align-items: center; - justify-content: center; -} - -.clear-search:hover { - opacity: 1; -} - -.filter-input { - padding-right: 28px; /* Make room for the clear button */ -} - -.expand-indicator-has-matches { - color: #689CC5; /* Light blue to match the hover color */ -} - -/* Add to catalogviewer.css */ -.generate-query-button { - margin-left: auto; - color: #666; - cursor: pointer; - padding: 2px 8px; - transition: color 0.2s; - display: flex; - align-items: center; -} - -.generate-query-button:hover { - color: #fff; -} - -/* Make viewer rows display as flex to accommodate the button on the right */ -.viewer_catalog, .viewer_schema, .viewer_table { - display: flex; - align-items: center; - width: 100%; -} - -/* Add spacing between the expand indicator and text */ -.catalog-name, .schema-name, .table-name { - margin-right: 0.5em; -} \ No newline at end of file diff --git a/precise/src/controls/tabs/EnterpriseTabs.tsx b/precise/src/controls/tabs/EnterpriseTabs.tsx index 823cf49..c12275b 100644 --- a/precise/src/controls/tabs/EnterpriseTabs.tsx +++ b/precise/src/controls/tabs/EnterpriseTabs.tsx @@ -1,8 +1,7 @@ import React, { Component } from 'react' -import { DndProvider } from 'react-dnd' -import { HTML5Backend } from 'react-dnd-html5-backend' -import './tabs.css' -import TabItem from './TabItem' +import { Box, Divider, IconButton, Tab as MuiTab, Tabs as MuiTabs } from '@mui/material' +import AddIcon from '@mui/icons-material/Add' +import CloseIcon from '@mui/icons-material/Close' import TabsEllipsesMenu from './TabsEllipsesMenu' import Tabs from './Tabs' import TabInfo from './TabInfo' @@ -131,29 +130,69 @@ class EnterpriseTabs extends Component const { newTabLabel = '+' } = this.props return ( - -
-
- {tabs.map((tab, index) => ( - + + this.handleTabClick(id)} + sx={{ flexGrow: 1 }} + > + {tabs.map((tab) => ( + this.handleTabClick(tab.id)} - handleTabClose={() => this.handleTabClose(tab.id)} - handleTabRename={(id, newTitle) => this.handleTabRename(tab.id, newTitle)} - handleTabPin={() => this.handleTabPin(tab.id)} + value={tab.id} + label={tab.title} + sx={{ minHeight: 36, py: 0 }} + icon={ + { + e.stopPropagation() + this.handleTabClose(tab.id) + }} + > + + + } + iconPosition="end" /> ))} -
- {newTabLabel} -
-
- -
-
+ { + e.stopPropagation() + this.handleNewTab() + }} + > + + + } + aria-label="Add tab" + sx={{ minWidth: 0, px: 1 }} + disableRipple + /> + + + + + + + + ) } } diff --git a/precise/src/controls/tabs/TabItem.tsx b/precise/src/controls/tabs/TabItem.tsx deleted file mode 100644 index e96f62c..0000000 --- a/precise/src/controls/tabs/TabItem.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react' -import { useDrag, useDrop } from 'react-dnd' -import TabInfo from './TabInfo' -import PinUpIcon from '../../assets/pin_up.png' -import PinDownIcon from '../../assets/pin_down.png' -import CloseIcon from '../../assets/close.png' - -const ItemType = { - TAB: 'tab', -} - -interface TabItemProps { - tab: T - isActive: boolean - index: number - moveTab: (fromIndex: number, toIndex: number) => void - handleTabClick: (tabId: string) => void - handleTabClose: (tabId: string) => void - handleTabRename: (tabId: string, newTitle: string) => void - handleTabPin: (tabId: string) => void -} - -function TabItem({ - tab, - isActive, - index, - moveTab, - handleTabClick, - handleTabClose, - handleTabRename, - handleTabPin, -}: TabItemProps) { - const [isEditing, setIsEditing] = useState(false) - const [editedTitle, setEditedTitle] = useState(tab.title) - const inputRef = useRef(null) - const containerRef = useRef(null) - const dragHandleRef = useRef(null) - - const [{ isDragging }, drag, preview] = useDrag({ - type: ItemType.TAB, - item: () => ({ index }), - collect: (monitor) => ({ - isDragging: monitor.isDragging(), - }), - canDrag: () => !isEditing, // Disable dragging when editing - }) - - const [, drop] = useDrop({ - accept: ItemType.TAB, - hover: (draggedItem: { index: number }) => { - if (draggedItem.index !== index) { - moveTab(draggedItem.index, index) - draggedItem.index = index - } - }, - }) - - useEffect(() => { - preview(drop(containerRef)) - }, [preview, drop]) - - useEffect(() => { - drag(dragHandleRef) - }, [drag]) - - useEffect(() => { - if (isEditing && inputRef.current) { - inputRef.current.focus() - inputRef.current.select() // Select all text when editing starts - } - }, [isEditing]) - - const handleDoubleClick = (e: React.MouseEvent) => { - e.stopPropagation() - setIsEditing(true) - } - - const handleInputChange = (event: React.ChangeEvent) => { - setEditedTitle(event.target.value) - } - - const handleInputBlur = () => { - setIsEditing(false) - if (editedTitle.trim() !== tab.title && editedTitle.trim() !== '') { - handleTabRename(tab.id, editedTitle.trim()) - } else { - setEditedTitle(tab.title) // Reset to original title if empty or unchanged - } - } - - const handleInputKeyPress = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - handleInputBlur() - } - } - - return ( -
handleTabClick(tab.id)} - > -
- {isEditing ? ( - e.stopPropagation()} - style={{ cursor: 'text' }} - /> - ) : ( -
- {tab.title} -
- )} -
-
-
{ - e.stopPropagation() - handleTabPin(tab.id) - }} - > - Pin -
-
{ - e.stopPropagation() - handleTabClose(tab.id) - }} - > - Close -
-
-
- ) -} - -export default TabItem diff --git a/precise/src/controls/tabs/TabsEllipsesMenu.tsx b/precise/src/controls/tabs/TabsEllipsesMenu.tsx index 2e729bb..b92d794 100644 --- a/precise/src/controls/tabs/TabsEllipsesMenu.tsx +++ b/precise/src/controls/tabs/TabsEllipsesMenu.tsx @@ -1,4 +1,7 @@ import React, { useState, useRef, useEffect } from 'react' +import { Box, IconButton, Popover, TextField, List, ListItemButton, ListItemText } from '@mui/material' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import PushPinIcon from '@mui/icons-material/PushPin' import TabInfo from './TabInfo' interface TabsEllipsesMenuProps { @@ -12,62 +15,61 @@ function TabsEllipsesMenu({ onTabSelect, filterPlaceholder = 'Filter tabs...', }: TabsEllipsesMenuProps) { - const [isOpen, setIsOpen] = useState(false) + const [anchorEl, setAnchorEl] = useState(null) const [filter, setFilter] = useState('') - const menuRef = useRef(null) + const isOpen = Boolean(anchorEl) const filteredTabs = tabs.filter((tab) => tab.title.toLowerCase().includes(filter.toLowerCase())) - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if (menuRef.current && !menuRef.current.contains(event.target as Node)) { - setIsOpen(false) - } - } - - document.addEventListener('mousedown', handleClickOutside) - return () => { - document.removeEventListener('mousedown', handleClickOutside) - } - }, [menuRef]) - - const handleEllipsesClick = (event: React.MouseEvent) => { + const handleEllipsesClick = (event: React.MouseEvent) => { event.stopPropagation() - setIsOpen(true) + setAnchorEl(event.currentTarget) } + const handleClosePopover = () => setAnchorEl(null) + return ( -
- - {isOpen && ( -
- + + + + + e.stopPropagation()}> + setFilter(e.target.value)} - autoFocus + sx={{ mb: 1 }} /> -
+ {filteredTabs.map((tab) => ( -
{ onTabSelect(tab.id) - setIsOpen(false) + handleClosePopover() }} + sx={{ gap: 1 }} > - {tab.title} - {tab.isPinned && 📌} -
+ + {tab.isPinned && } + ))} -
-
- )} -
+ + + + ) } diff --git a/precise/src/controls/tabs/tabs.css b/precise/src/controls/tabs/tabs.css deleted file mode 100644 index 5fada60..0000000 --- a/precise/src/controls/tabs/tabs.css +++ /dev/null @@ -1,193 +0,0 @@ -.tabs-container { - position: relative; - display: flex; - align-items: center; - overflow-x: auto; - white-space: nowrap; - margin-top: 0px; /* Tabs overlap the control bar, better fix might be to prevent the animation from doing this */ -} - -.tabs { - display: flex; - flex-direction: row; - flex-grow: 1; - overflow-x: auto; - /* disable scrollbars */ - scrollbar-width: none; - height: 2.5em; -} - -.controltab { - display: inline-flex; - align-items: center; - padding: 10px; - padding-left: 20px; - padding-right: 20px; - background-color: #000000; - border-left: 1px solid #2b2b2b; - border-top: 1px solid #2b2b2b; - border-radius: 3px; - font-size: xx-large; - cursor: pointer; - flex-shrink: 0; - box-sizing: border-box; - color: #ffffff; - font-size: small; -} - -.controltab:hover, .tabs-ellipses-menu:hover { - background-color: #2b2b2b; -} - -.tab-item, -.tab-item-selected { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px; - border-left: 1px solid #2b2b2b; - border-top: 1px solid #2b2b2b; - border-top-left-radius: 5px; - border-top-right-radius: 5px; - cursor: pointer; - box-sizing: border-box; - font-size: small; - max-width: 200px; - animation: tabPopup 0.3s ease-out; -} - -@keyframes tabPopup { - from { - transform: translateY(10px); - opacity: 0; - } - to { - transform: translateY(0); - opacity: 1; - } -} - -.tab-item { - background-color: #101010; - color: #9d9d9d; -} - -.tab-item-selected { - background-color: #1F1F1F; - color: #dddddd; -} - -.tab-item:hover, -.tab-item-selected:hover { - background-color: #2b2b2b; -} - -.tab-content { - flex-grow: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - margin-right: 5px; -} - -.tab-buttons { - display: flex; - align-items: center; -} - -.tab-button { - background: none; - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - padding: 2px; - color: inherit; - font-size: 14px; - width: 14px; - height: 14px; - margin-left: 0px; -} - -.tab-button:hover { - background-color: rgba(255, 255, 255, 0.1); - border-radius: 3px; -} - -.close-button:hover { - color: #ff4444; -} - -.tab-list-button-and-menu { - position: absolute; - right: 0px; - top: 0px; - z-index: 1000; -} - -.ellipses-button { - display: flex; - align-items: center; - justify-content: center; - width: 30px; - height: 30px; - padding: 0; - margin-right: 20px; - background-color: #000000; - border: none; - border-left: 1px solid #2b2b2b; - border-top: 1px solid #2b2b2b; - border-radius: 3px; - cursor: pointer; - color: #ffffff; - font-size: 18px; - outline: none; -} - -.ellipses-button:hover { - background-color: #2b2b2b; -} - -.tabs-ellipses-menu-content { - position: fixed; - right: 10px; - top: 130px; - background-color: #101010; - min-width: 300px; - box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); - z-index: 1001; - max-height: calc(100vh - 50px); - overflow-y: auto; - overflow-x: hidden; - border: 1px solid #2b2b2b; - border-radius: 3px; -} - -.tabs-ellipses-menu-content input { - width: calc(100% - 20px); - margin: 10px; - padding: 5px; - background-color: #181818; - border: 1px solid #2b2b2b; - color: #9d9d9d; - font-size: small; -} - -.tabs-ellipses-menu-content .tab-list { - max-height: 250px; - overflow-y: auto; -} - -.tabs-ellipses-menu-content .tab-item { - display: block; - padding: 10px; - text-decoration: none; - cursor: pointer; - border: none; - border-radius: 0; -} - -.tabs-ellipses-menu-content .tab-item:hover { - background-color: #2b2b2b; -} \ No newline at end of file diff --git a/precise/src/main.tsx b/precise/src/main.tsx index b7b1aff..4480051 100644 --- a/precise/src/main.tsx +++ b/precise/src/main.tsx @@ -1,10 +1,50 @@ -import React from 'react' +import React, { useLayoutEffect, useRef, useState } from 'react' import ReactDOM from 'react-dom/client' import QueryEditor from './QueryEditor' +function useObservedHeight(ref: React.RefObject | React.RefObject) { + const [height, setHeight] = useState(0) + + useLayoutEffect(() => { + const el = ref.current + if (!el) return + + // Initial measure + setHeight(el.getBoundingClientRect().height) + + const ro = new ResizeObserver(([entry]) => { + setHeight(entry.contentRect.height) + }) + ro.observe(el) + + return () => ro.disconnect() + }, [ref]) + + return height +} + +export default function App() { + const slotRef = useRef(null) + const slotHeight = useObservedHeight(slotRef) + + return ( +
+

Trino query editor - Example app

+
+ +
+
+ ) +} + ReactDOM.createRoot(document.getElementById('root')!).render( -

Trino Query Editor - Example app

- +
) diff --git a/precise/src/sql/SchemaProvider.ts b/precise/src/sql/SchemaProvider.ts index 9e2c41b..627a4ed 100644 --- a/precise/src/sql/SchemaProvider.ts +++ b/precise/src/sql/SchemaProvider.ts @@ -63,7 +63,10 @@ class SchemaProvider { return null } - static populateCatalogsAndRefreshTableList(callback: any = null, errorCallback: any = null) { + static populateCatalogsAndRefreshTableList( + callback: ((nextCatalogs: Map) => void) | null = null, + errorCallback: ((error: string) => void) | null = null + ) { // refresh catalogs new TrinoQueryRunner() .SetAllResultsCallback((results: any[], isError: boolean) => { @@ -73,7 +76,6 @@ class SchemaProvider { this.catalogs.set(catalog.getName(), catalog) } this.lastSchemaFetchError = undefined - callback() // refresh tables and schemas for this catalog new TrinoQueryRunner() @@ -97,7 +99,7 @@ class SchemaProvider { if (!isError) { catalog.clearErrorMessage() } - callback() + callback?.(new Map(this.catalogs)) }) .SetErrorMessageCallback((error: string) => { catalog.setErrorMessage(error.toString()) @@ -111,7 +113,7 @@ class SchemaProvider { }) .SetErrorMessageCallback((error: string) => { this.lastSchemaFetchError = error.toString() - errorCallback(error.toString()) + errorCallback?.(error.toString()) }) .StartQuery('select catalog_name, connector_name from system.metadata.catalogs') } diff --git a/precise/src/style/components.css b/precise/src/style/components.css deleted file mode 100644 index 57a1d65..0000000 --- a/precise/src/style/components.css +++ /dev/null @@ -1,201 +0,0 @@ -/** - * components.css - * Styles for reusable UI components and utility classes - */ - -.collapse-button { - background-color: var(--dark-gray); - color: var(--highlight-blue); - border-color: var(--light-gray); - font-size: large; - border-radius: 0px 10px 10px 0px; - padding: 0.5em; - cursor: pointer; - text-align: center; - vertical-align: middle; - position: fixed; - left: 0; - bottom: 0; - z-index: 1000; -} - -.button-align-right { - padding-left: 1vw; - bottom: 0; -} - -.small-rounded-dark-grey-button { - background-color: var(--very-dark-gray); - color: var(--muted-text-color); - font-size: large; - border-radius: 10px; - padding: 0em; - cursor: pointer; - width: 1.5em; - height: 1.5em; - border: 1px solid var(--dark-gray); - text-align: center; - vertical-align: middle; - position: absolute; - left: 50%; - line-height: 1.0em; -} - -/* Progress Indicators */ -.spinner { - display: inline-block; - width: 30px; - height: 30px; - border: 3px solid rgba(255, 255, 255, 0.3); - border-radius: 50%; - border-top-color: var(--white); - animation: spin 1s linear infinite; -} - -.progress-percent { - font-weight: bold; - font-size: 1.5em; -} - -.progress-bar { - width: 100%; - border: 1px solid var(--medium-gray); - background-color: var(--dark-accent); - display: grid; - grid-template-columns: 1fr; -} - -.progress-bar-fill { - transition: width 1100ms; - transition-timing-function: linear; - grid-column-start: 0; - left: 0px; - top: 0px; - line-height: 30px; - text-align: center; - bottom: 0px; - width: 0%; - background: var(--brand-gradient); - z-index: 1; - white-space: nowrap; -} - -.progress-bar-running-state { - width: 100%; - transition: opacity 500ms; - transition-timing-function: linear; - grid-column-start: 0; - left: 0px; - top: 0px; - line-height: 30px; - text-align: center; - bottom: 0px; - background: var(--brand-gradient); - z-index: 1; - white-space: nowrap; -} - -.progress-bar-timer { - padding-left: 1em; - font-size: large; -} - -/* Add Query Button */ -.add-query-button { - display: grid; - grid-template-columns: auto; - align-items: center; - width: 100%; - height: 1em; -} - -.add-query-button hr { - border: 0; - height: 1px; - width: 100%; - background: #333; - background-image: linear-gradient(to right, #111, #222, #111); -} - -/* Error Handling */ -.result-cell-null { - color: var(--muted-text-color); - font-style: italic; -} - -/* Links and Text */ -.link-to-query { - font-size: small; - text-align: right; - margin-right: 1em; -} - -.status-text { - color: var(--muted-text-color); - font-size: small; -} - -.read-the-docs { - color: var(--muted-text-color); -} - -.helper-text { - color: #666; - font-size: x-small; -} - -.offset-page-for-copy { - font-size: .875em; - margin-right: .125em; - position: relative; - top: -.25em; - left: -.125em; -} - -.offset-page-for-copy-inner { - position: absolute; - top: .25em; - left: .25em; -} - -/* Utility Classes - Only include the ones actually used in the project */ -.flex { - display: flex; -} - -.items-center { - align-items: center; -} - -.justify-center { - justify-content: center; -} - - button:focus, - button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; - } - - /* Progress bar color consistency */ - .progress-bar { - width: 100%; - border: 1px solid #777; - background-color: #222; - display: grid; - grid-template-columns: 1fr; - } - - .progress-bar-fill { - transition: width 1100ms; - transition-timing-function: linear; - grid-column-start: 0; - left: 0px; - top: 0px; - line-height: 30px; - text-align: center; - bottom: 0px; - width: 0%; - background: var(--brand-gradient); - z-index: 1; - white-space: nowrap; - } \ No newline at end of file diff --git a/precise/src/style/control.css b/precise/src/style/control.css deleted file mode 100644 index 5fc42ca..0000000 --- a/precise/src/style/control.css +++ /dev/null @@ -1,231 +0,0 @@ -/** - * control.css - * Styles for control elements and interactions - */ - -/* Basic Interactive Elements */ -.clickable { - cursor: pointer; -} - -.selectable { - user-select: text; -} - -.non-selectable { - user-select: none; -} - -input:focus { - outline: none; - border-color: var(--highlight-blue); -} - -/* Tab Controls */ -.tab-container { - display: flex; - border-bottom: 1px solid var(--dark-gray); - background-color: var(--subtle-darker-accent-color); -} - -.tab { - padding: 8px 16px; - cursor: pointer; - border-right: 1px solid var(--dark-gray); - user-select: none; -} - -.tab.active { - background-color: var(--lighter-accent-color); - border-bottom: 2px solid var(--highlight-blue); -} - -.tab:hover:not(.active) { - background-color: var(--subtle-lighter-accent-color); -} - -/* Editor Controls */ -.editor-toolbar { - display: flex; - align-items: center; - padding: 4px; - background-color: var(--subtle-darker-accent-color); - border-bottom: 1px solid var(--dark-gray); -} - -.toolbar-button { - background: none; - border: none; - color: var(--muted-text-color); - padding: 4px 8px; - cursor: pointer; - border-radius: 4px; -} - -.toolbar-button:hover { - background-color: var(--subtle-lighter-accent-color); - color: var(--white); -} - -.toolbar-button.active { - background-color: var(--lighter-accent-color); - color: var(--white); -} - -/* Resizable Elements */ -.resizable { - position: relative; -} - -.resize-handle { - position: absolute; - width: 10px; - height: 100%; - right: 0; - top: 0; - cursor: col-resize; - z-index: 10; -} - -.resize-handle-horizontal { - width: 100%; - height: 10px; - left: 0; - bottom: 0; - cursor: row-resize; -} - -/* Context Menus and Dropdowns */ -.context-menu { - position: absolute; - background-color: var(--lighter-accent-color); - border: 1px solid var(--dark-gray); - border-radius: 4px; - z-index: 1000; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); -} - -.menu-item { - padding: 6px 12px; - cursor: pointer; -} - -.menu-item:hover { - background-color: var(--subtle-lighter-accent-color); -} - -.menu-divider { - height: 1px; - background-color: var(--dark-gray); - margin: 4px 0; -} - -/* parent type of small control buttons */ -.small-rounded-button { - background-color: #111; - color: #7d7d7d; - border-color: #689CC5; - font-size: large; - border-radius: 10px; - padding: 0.5em; - cursor: pointer; - height: auto; - text-align: center; - vertical-align: middle; - margin-right: 2px; -} - -.small-rounded-button:hover { - background-color: #333; - /* animate */ - transition: background-color 0.25s; -} - -/* input box with no border and transparent background, no outline when selected */ -.query-title { - padding-left: 1em; - color: #61dafb; - font-weight: 300; - font-size: 1.5em; - border: none; - background-color: transparent; - outline: none; -} - -.card-header { - padding-top: 0.2em; - padding-left: 0.2em; - background-color: #1E1E1E; - right: 0px; - height: 3em; -} - -/* grid containing execute query and title text */ -.card-header-grid { - display: grid; - grid-template-columns: 4em auto 10em 10em 3.75em 5px 3.75em 5px 3.75em 5px 3.75em; - padding-right: 20px; /* to account for scrollbar */ -} - -.query-run-button { - background-color: #111; - color: #689CC5; - border-color: #689CC5; - font-size: large; - border-radius: 10px; - padding: 0.5em; - cursor: pointer; - width: 4em; - height: auto; - text-align: center; - vertical-align: middle; -} - -.query-run-button:hover { - background-color: var(--dark-accent); - color: var(--white); -} - -.query-control-button { - background-color: #111; - color: #689CC5; - border-color: #3f5f79; - font-size: large; - border-radius: 10px; - padding: 0.5em; - cursor: pointer; - width: 100%; - height: 100%; - text-align: center; - vertical-align: middle; - box-sizing: border-box; -} - -.query-control-button:hover { - background-color: var(--dark-accent); - color: var(--white); -} - - -.catalog-setting { - /* align text right */ - text-align: right; -} - -/* Catalog and schema settings */ -.catalog-setting, -.schema-setting { - padding-left: 1em; - color: #777; - font-weight: 300; - font-size: 1em; - border: none; - background-color: transparent; - outline: none; - } - -/* hover over the catalog setting and schema setting to change the color to white */ -.catalog-setting:hover, .schema-setting:hover { - color: #fff; - transition: background-color 0.25s, color 0.25s; -} diff --git a/precise/src/style/layout.css b/precise/src/style/layout.css deleted file mode 100644 index ede5c71..0000000 --- a/precise/src/style/layout.css +++ /dev/null @@ -1,144 +0,0 @@ -/** - * layout.css - * Layout structures and positioning styles - */ - -/* Page Layout */ -.page { - width: 100%; - } - - /* Grid Layout for page with collapsible sidebar */ - .pagegrid { - display: grid; - grid-template-columns: 0vw 100vw; - transition: grid-template-columns 0.1s; - max-width: 100%; - width: 100%; - box-sizing: border-box; - } - - /* For expanded layout */ - .catalog-expanded { - /* Will be applied via JavaScript */ - } - - /* For collapsed layout */ - .catalog-collapsed { - /* Will be applied via JavaScript */ - } - - /* Catalog Sidebar */ - .catalog-container { - width: auto; - height: calc(100vh); - } - - .catalog-wrapper { - overflow-y: auto; - overflow-x: hidden; - width: auto; - background-color: var(--dark-gray); - height: calc(100vh - 3em); - scrollbar-color: var(--medium-gray) var(--dark-gray); - } - - /* Branding and Header */ - .branding-header { - background: var(--brand-gradient); - height: 3em; - width: 100%; - position: fixed; - } - - .branding-padder { - height: 3em; - } - - /* Card Layout */ - .card { - margin: 0px; - } - - /* Editor and Results Layout */ - .query-editor { - margin: 0px; - padding: 0px; - } - - .editorspace { - /* Space for the Monaco editor */ - } - - .resultSetPort { - padding-top: 0em; - } - - /* Spacers and Common Layout Elements */ - .spacer { - margin-bottom: 1em; - } - - /* Status Layouts */ - .status { - display: grid; - grid-template-columns: 60px 60px auto; - align-items: left; - } - - .progress-bar-grid { - display: grid; - grid-template-columns: 90fr 10fr; - align-items: center; - } - - /* Query Status Table Layout */ - .query-status-table { - border-spacing: 0px; - } - - .query-status-table th { - padding: 4px 8px 4px 8px; - color: var(--muted-text-color); - font-size: small; - padding-right: 1em; - text-align: left; - } - - /* Image size modifiers */ - .full-height-image { - height: 100%; - width: auto; - object-fit: contain; - } - - .half-height-image { - height: 50%; - width: auto; - object-fit: contain; - } - - /* Fixes for scrollable areas */ - .scrollable { - overflow-y: auto; - overflow-x: auto; - height: 40vh; - width: 100%; - margin: 0; - padding: 0; - } - - /* Catalog container height */ - .catalog-container { - width: auto; - height: calc(100vh); - } - - .catalog-wrapper { - overflow-y: auto; - overflow-x: hidden; - width: auto; - background-color: var(--dark-gray); - height: calc(100vh - 3em); - scrollbar-color: var(--medium-gray) var(--dark-gray); - } \ No newline at end of file diff --git a/precise/src/style/normalize.css b/precise/src/style/normalize.css deleted file mode 100644 index a0ac59d..0000000 --- a/precise/src/style/normalize.css +++ /dev/null @@ -1,94 +0,0 @@ -/** - * normalize.css - * Base styles and resets for consistent rendering - */ - -/* Reset margins and paddings */ -html, body { - margin: 0; - padding: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - color: var(--text-color); - background-color: var(--bg-color); - } - - body { - overflow-y: auto; - } - - #root { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - color: #fff; - background-color: #121212; - margin: 0; - padding: 0; - max-width: 100%; - overflow-x: hidden; - } - - /* Basic elements styling */ - a { - font-weight: 500; - color: var(--link-color); - text-decoration: inherit; - } - - a:hover { - color: var(--link-hover-color); - } - - button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; - } - - button:hover { - border-color: var(--border-color); - } - - button:focus, - button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; - } - - /* Tables */ - table { - border-collapse: collapse; - border-spacing: 0; - width: 100%; - box-sizing: border-box; - color: var(--text-color); - } - - /* Basic animations */ - @keyframes spin { - to { - transform: rotate(360deg); - } - } - - @keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } - } - - .animate-spin { - animation: spin 1s linear infinite; - } - - @media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } - } \ No newline at end of file diff --git a/precise/src/style/query-editor.css b/precise/src/style/query-editor.css deleted file mode 100644 index ef51423..0000000 --- a/precise/src/style/query-editor.css +++ /dev/null @@ -1,162 +0,0 @@ -/** - * query-editor.css - * Styles specific to the query editor component - */ - - /* Query title input */ - .query-title { - background-color: transparent; - color: var(--highlight-blue); - border: 0px; - padding: 0px 12px; - font-size: 2em; - width: 100%; - } - - /* Query Editor Area */ - .editorspace { - /* Container for Monaco editor */ - } - - /* SQL Syntax Highlighting */ - .qualifiedName { - color: var(--white) !important; - background-color: var(--success-color); - cursor: pointer !important; - font-weight: bold; - } - - .relationReference { - color: var(--white) !important; - background-color: rgba(0, 0, 255, 0.5); - cursor: pointer !important; - font-weight: bold; - } - - .columnType { - color: var(--muted-text-color); - } - - .columnExtraOrComment { - font-style: italic; - } - - /* Query Stage Indicators */ - .status-stage-default { - border-left: 4px solid var(--lighter-accent-color); - border-top: 1px solid var(--lighter-accent-color); - } - - .status-stage-rows { - border-left: 4px solid var(--rows-color); - border-top: 1px solid var(--rows-color-partial-transparent); - } - - .status-stage-bytes { - border-left: 4px solid var(--bytes-color); - border-top: 1px solid var(--bytes-color-partial-transparent); - } - - .status-stage-splits { - border-left: 4px solid var(--splits-color); - border-top: 1px solid var(--splits-color-partial-transparent); - } - - /* Stage Headers */ - .status-stage-category-header-splits { - border-left: 4px solid var(--splits-color); - text-align: center; - } - - .status-stage-category-header-bytes { - border-left: 4px solid var(--bytes-color); - text-align: center; - } - - .status-stage-category-header-rows { - border-left: 4px solid var(--rows-color); - text-align: center; - } - - /* Stage Row Styles */ - .stage-running td { - padding: 4px 8px 4px 8px; - text-align: left; - } - - .stage-not-running td { - padding: 4px 8px 4px 8px; - color: var(--muted-text-color); - text-align: left; - } - - .substitution-field { - /* Field container styling */ - margin-bottom: 10px; - } - - .substitution-field label { - /* Label styling */ - margin-right: 8px; - } - - .substitution-field input { - /* Input styling */ - padding: 4px 8px; - border-radius: 4px; - border: 1px solid var(--lighter-accent-color); - background-color: var(--subtle-lighter-accent-color); - color: var(--text-color); - } - - /* Add this to your query-editor.css file */ - -.editor-toolbar { - position: absolute; - top: 5px; - right: 25px; - z-index: 1000; - display: flex; - gap: 8px; -} - -.editor-button { - background: none; - border: none; - cursor: pointer; - color: rgba(255, 255, 255, 0.7); - display: flex; - align-items: center; - justify-content: center; - padding: 4px; - border-radius: 4px; - transition: all 0.2s ease; -} - -.editor-button:hover { - background-color: rgba(255, 255, 255, 0.1); - color: white; -} - -.editor-button:active { - background-color: rgba(255, 255, 255, 0.2); -} - -.editor-button[disabled] { - opacity: 0.5; - cursor: not-allowed; -} - -.editor-button[data-tooltip]:hover::after { - content: attr(data-tooltip); - position: absolute; - bottom: -30px; - left: 50%; - transform: translateX(-50%); - background-color: rgba(0, 0, 0, 0.8); - color: white; - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; - white-space: nowrap; -} \ No newline at end of file diff --git a/precise/src/style/results.css b/precise/src/style/results.css deleted file mode 100644 index 8c69fbd..0000000 --- a/precise/src/style/results.css +++ /dev/null @@ -1,231 +0,0 @@ -/** - * results.css - * Styles for the query results display - */ - -/* Results Container */ -.result-set { - width: 100%; - overflow-x: auto; - box-sizing: border-box; - padding: 0; - margin: 0; - position: relative; /* For positioning clear and copy buttons */ - } - - .result-table-container { - width: 100%; - overflow-x: auto; - margin: 0; - padding: 0; - } - - /* Results Table */ - .result-table { - table-layout: fixed; - width: 100%; - min-width: 100%; - border-collapse: collapse; - } - - .result-set table { - border-collapse: collapse; - border-spacing: 0; - width: 100%; - box-sizing: border-box; - } - - .result-set tr, - .result-set td, - .result-set th { - box-sizing: border-box; - } - - /* Table Rows & Cells */ - .result-set tr:nth-child(even) td { - background-color: var(--subtle-lighter-accent-color); - } - - .result-set tr:nth-child(odd) td { - background-color: var(--lighter-accent-color); - } - - .result-set td { - padding: 10px; - border: 1px solid var(--lighter-accent-color); - text-align: left; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .result-set th { - padding: 10px; - text-align: left; - background-color: var(--subtle-darker-accent-color); - color: darkgray; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - /* Null Values */ - .result-cell-null { - color: var(--muted-text-color); - font-style: italic; - } - - /* Clear Results Button */ - .clear-result-table { - position: absolute; - right: 0; - max-width: calc(100% - 20px); - color: var(--muted-text-color); - font-size: small; - cursor: pointer; - margin-right: 1em; - } - - /* Stats Display */ - .stats { - margin: 0em; - } - - /* Collapsed Display */ - .displaydefaultcollapsed { - display: none; - } - - /* Reload Link */ - .reload-link-div { - text-align: right; - float: right; - padding-right: 0.5em; - cursor: pointer; - font-size: small; - color: var(--error-color-subtle); - } - - .reload-link-div:hover { - color: var(--white); - } - - /* Results table structure improvements */ -.result-set tr, .result-set td, .result-set th { - box-sizing: border-box; - } - - .result-set table { - border-collapse: collapse; - border-spacing: 0; - width: 100%; - box-sizing: border-box; - } - - /* Adding missing hover style */ - .reload-link-div:hover { - color: var(--white); - } - - /* Fix table container sizing */ - .result-table-container { - width: 100%; - overflow-x: auto; - margin: 0; - padding: 0; - } - - /* Fix copy link formatting */ - .offset-page-for-copy { - font-size: .875em; - margin-right: .125em; - position: relative; - top: -.25em; - left: -.125em; - } - - .offset-page-for-copy-inner { - position: absolute; - top: .25em; - left: .25em; - } - - /* Helper text formatting */ - .helper-text { - color: #666; - font-size: x-small; - } - - /* Shared styles for action buttons (copy and clear) */ -.result-action-button { - position: absolute; - background-color: rgba(40, 40, 40, 0.6); - color: rgba(200, 200, 200, 0.8); - padding: 8px 12px; - border-radius: 4px; - font-size: small; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease-in-out; - backdrop-filter: blur(2px); - z-index: 10; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); - right: 10px; /* Positioned on the right */ - width: auto; /* Allow content to determine width */ - white-space: nowrap; /* Keep text on one line */ - } - - - .result-action-button:hover { - transform: scale(1.15); - background-color: rgba(60, 60, 60, 0.8); - color: white; - box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3); - } - - .copy-link { - position: absolute; - top: 2px; - right: 10px; - max-width: calc(100% - 20px); - } - - .copy-link.copied { - animation: action-pulse 0.3s ease-in-out; - background-color: rgba(40, 100, 40, 0.8); - } - - /* Clear button specific styles */ - .clear-result-table { - top: 2px; - margin-right: 80px; - } - - .clear-result-table.confirmed { - animation: action-pulse 0.3s ease-in-out; - background-color: rgba(120, 40, 40, 0.8); - } - - /* Shared animation for feedback */ - @keyframes action-pulse { - 0% { transform: scale(1.15); } - 50% { transform: scale(1.3); } - 100% { transform: scale(1.15); } - } - - /* Icon styling */ - .action-icon { - margin-right: 6px; - } - - /* Text label styling */ - .action-text { - user-select: none; - } - - /* Make sure the result container has proper positioning context */ - .result-set { - position: relative; - } \ No newline at end of file diff --git a/precise/src/style/theme.css b/precise/src/style/theme.css deleted file mode 100644 index a2eb627..0000000 --- a/precise/src/style/theme.css +++ /dev/null @@ -1,66 +0,0 @@ -/** - * theme.css - * Contains all theme variables and color definitions - */ - - :root { - /* Base typography */ - font-weight: 400; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - /* Color scheme */ - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - /* Brand Colors */ - --bytes-color: #1C364D; - --bytes-color-partial-transparent: rgba(28, 54, 77, 0.5); - --rows-color: #0C61A6; - --rows-color-partial-transparent: rgba(12, 97, 166, 0.5); - --splits-color: #564682; - --splits-color-partial-transparent: rgba(86, 70, 130, 0.5); - - /* UI Colors */ - --link-color: #5bc0de; - --link-hover-color: #747bff; - --border-color: #646cff; - --subtle-lighter-accent-color: #1e1e1e; - --lighter-accent-color: #2c2c2c; - --subtle-darker-accent-color: #131313; - - /* Text Colors */ - --muted-text-color: #888; - --white: #fff; - - /* Gradient */ - --brand-gradient: linear-gradient(45deg, #564682, #1C2F44, #1C364D, #0C61A6); - - /* Structural Colors */ - --dark-gray: #333; - --medium-gray: #555; - --light-gray: #666; - --very-dark-gray: #111; - --dark-accent: #222; - --highlight-blue: #689CC5; - - /* Success/Error States */ - --success-color: rgba(1, 123, 15, 0.5); - --error-color-subtle: #999; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} \ No newline at end of file diff --git a/precise/src/theme.tsx b/precise/src/theme.tsx new file mode 100644 index 0000000..ebdf6b0 --- /dev/null +++ b/precise/src/theme.tsx @@ -0,0 +1,45 @@ +import { createTheme } from '@mui/material/styles' +import darkScrollbar from '@mui/material/darkScrollbar' + +export const lightTheme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#0b1367', + }, + secondary: { + main: '#f50057', + }, + }, + components: { + MuiLink: { + styleOverrides: { + root: { + color: '#f50057', + textDecoration: 'none', + }, + }, + }, + }, +}) + +export const darkTheme = createTheme({ + palette: { + mode: 'dark', + }, + components: { + MuiLink: { + styleOverrides: { + root: { + color: '#dd33fa', + textDecoration: 'none', + }, + }, + }, + MuiCssBaseline: { + styleOverrides: { + body: darkScrollbar(), + }, + }, + }, +}) diff --git a/precise/src/utils/ClearButton.tsx b/precise/src/utils/ClearButton.tsx index 89031ca..6809022 100644 --- a/precise/src/utils/ClearButton.tsx +++ b/precise/src/utils/ClearButton.tsx @@ -1,5 +1,7 @@ import React, { useState } from 'react' -import { Trash2, AlertTriangle } from 'lucide-react' +import { Button, Tooltip } from '@mui/material' +import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline' +import WarningAmberIcon from '@mui/icons-material/WarningAmber' interface ClearButtonProps { onClear: () => void @@ -19,23 +21,18 @@ const ClearButton: React.FC = ({ onClear }) => { } return ( -
- {confirming ? ( - <> - - Confirm - - ) : ( - <> - - Clear - - )} -
+ + + ) } diff --git a/precise/src/utils/CopyLink.tsx b/precise/src/utils/CopyLink.tsx index 790032a..06d8fe0 100644 --- a/precise/src/utils/CopyLink.tsx +++ b/precise/src/utils/CopyLink.tsx @@ -1,5 +1,7 @@ import React, { useState } from 'react' -import { Copy, CheckCircle } from 'lucide-react' +import { Button, Tooltip } from '@mui/material' +import CopyAllOutlinedIcon from '@mui/icons-material/CopyAllOutlined' +import DoneOutlinedIcon from '@mui/icons-material/DoneOutlined' interface CopyLinkProps { copy: () => void @@ -15,23 +17,24 @@ const CopyLink: React.FC = ({ copy }) => { } return ( -
- {copied ? ( - <> - - Copied! - - ) : ( - <> - - Copy - - )} -
+ + + ) } diff --git a/precise/src/utils/ErrorBoxProvider.tsx b/precise/src/utils/ErrorBoxProvider.tsx deleted file mode 100644 index 5d7bdca..0000000 --- a/precise/src/utils/ErrorBoxProvider.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { useState } from 'react' -import CloseIcon from '../assets/close.png' -import './errorbox.css' - -interface ErrorBoxProviderProps { - errorMessage: string - errorContext: string -} - -const ErrorBox: React.FC = ({ errorMessage, errorContext }) => { - const [isVisible, setIsVisible] = useState(true) - const [errorTimestamp] = useState(new Date().toISOString()) // Capture timestamp when error occurs - - if (!errorMessage || !isVisible) { - return null - } - - return ( -
-
- Error: {errorContext} -
setIsVisible(false)} - role="button" - aria-label="Close error message" - > - Close -
-
-
-
- {errorMessage} -
{errorTimestamp}
-
-
-
- ) -} - -export default ErrorBox diff --git a/precise/src/utils/ProgressBar.tsx b/precise/src/utils/ProgressBar.tsx deleted file mode 100644 index 9a7cada..0000000 --- a/precise/src/utils/ProgressBar.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react' - -// ProgressBar properties -interface ProgressBarProps { - progress: number - state: string -} - -// ProgressBar state -interface ProgressBarState {} - -export default class ProgressBar extends React.Component { - constructor(props: ProgressBarProps) { - super(props) - this.state = { - progress: 0, - } - } - - render() { - return ( -
- {this.props.progress === 0 && Number.isFinite(this.props.progress) ? ( -
{this.props.state}
- ) : ( -
- {Math.round(this.props.progress) + '%'} -
- )} -
- ) - } -} diff --git a/precise/src/utils/ResizableContainer.tsx b/precise/src/utils/ResizableContainer.tsx deleted file mode 100644 index 4268c8a..0000000 --- a/precise/src/utils/ResizableContainer.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react' - -interface ResizableContainerProps { - children: React.ReactNode - initialHeight: string - minHeight?: string - maxHeight?: string - onHeightChange: (newHeight: string) => void -} - -const ResizableContainer: React.FC = ({ - children, - initialHeight, - minHeight = '100px', - maxHeight = '80vh', - onHeightChange, -}) => { - const [height, setHeight] = useState(initialHeight) - const containerRef = useRef(null) - const resizeHandleRef = useRef(null) - - useEffect(() => { - const container = containerRef.current - const resizeHandle = resizeHandleRef.current - let isResizing = false - let startY: number - let startHeight: number - - const onMouseDown = (e: MouseEvent) => { - isResizing = true - startY = e.clientY - startHeight = container!.getBoundingClientRect().height - document.addEventListener('mousemove', onMouseMove) - document.addEventListener('mouseup', onMouseUp) - } - - const onMouseMove = (e: MouseEvent) => { - if (!isResizing) return - const diff = e.clientY - startY - const newHeight = startHeight + diff - const minHeightPx = parseInt(minHeight) - const maxHeightPx = parseInt(maxHeight) - const clampedHeight = Math.max(minHeightPx, Math.min(maxHeightPx, newHeight)) - const newHeightString = `${clampedHeight}px` - setHeight(newHeightString) - onHeightChange(newHeightString) - } - - const onMouseUp = () => { - isResizing = false - document.removeEventListener('mousemove', onMouseMove) - document.removeEventListener('mouseup', onMouseUp) - } - - resizeHandle?.addEventListener('mousedown', onMouseDown) - - return () => { - resizeHandle?.removeEventListener('mousedown', onMouseDown) - document.removeEventListener('mousemove', onMouseMove) - document.removeEventListener('mouseup', onMouseUp) - } - }, [minHeight, maxHeight, onHeightChange]) - - return ( -
- {children} -
-
- ) -} - -export default ResizableContainer diff --git a/precise/src/utils/errorbox.css b/precise/src/utils/errorbox.css deleted file mode 100644 index 9f89172..0000000 --- a/precise/src/utils/errorbox.css +++ /dev/null @@ -1,40 +0,0 @@ -.error-box { - background-color: #2b2b2b; - border: 1px solid #ff6b6b; - border-radius: 4px; - margin: 10px; - padding: 10px; -} - -.error-box-header { - color: #ff6b6b; - display: flex; - justify-content: space-between; - align-items: center; - font-weight: bold; - margin-bottom: 8px; -} - -.error-box-close { - cursor: pointer; - opacity: 0.8; - transition: opacity 0.2s; -} - -.error-box-close:hover { - opacity: 1; -} - -.error-box-body { - color: #ff9999; -} - -.error-box-message { - margin-bottom: 8px; -} - -.error-box-context { - color: #999; - font-size: 0.9em; - margin-top: 8px; -} \ No newline at end of file diff --git a/screenshot.png b/screenshot.png new file mode 100755 index 0000000..f0a833e Binary files /dev/null and b/screenshot.png differ