diff --git a/config-overrides.js b/config-overrides.js index 98185f0970..d50d08de47 100644 --- a/config-overrides.js +++ b/config-overrides.js @@ -2,6 +2,7 @@ const path = require('path'); const MiniCSSExtractPlugin = require('mini-css-extract-plugin'); const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); +const webpack = require('webpack'); const srcRoot = path.resolve(__dirname, 'src'); const uiKitRoot = path.resolve(__dirname, 'node_modules/@gravity-ui/uikit'); const antlr4C3Root = path.resolve(__dirname, 'node_modules/antlr4-c3'); @@ -9,6 +10,8 @@ const websqlRoot = path.resolve(__dirname, 'node_modules/@gravity-ui/websql-auto const antlr4ngRoot = path.resolve(__dirname, 'node_modules/antlr4ng'); const uiKitIconsRoot = path.resolve(__dirname, 'node_modules/@gravity-ui/icons'); +const packageJson = require('./package.json'); + module.exports = { webpack: (config, env) => { const oneOfRule = config.module.rules.find((r) => r.oneOf); @@ -45,6 +48,13 @@ module.exports = { }), ); + // Add DefinePlugin to expose just the version + config.plugins.push( + new webpack.DefinePlugin({ + 'process.env.UI_VERSION': JSON.stringify(packageJson.version), + }), + ); + const cssExtractPlugin = config.plugins.find((p) => p instanceof MiniCSSExtractPlugin); if (cssExtractPlugin) { cssExtractPlugin.options.ignoreOrder = true; diff --git a/package-lock.json b/package-lock.json index 6e215962cc..9dd1d1f834 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@gravity-ui/date-utils": "^2.5.6", "@gravity-ui/i18n": "^1.7.0", "@gravity-ui/icons": "^2.12.0", + "@gravity-ui/illustrations": "^2.0.0", "@gravity-ui/navigation": "^2.30.0", "@gravity-ui/paranoid": "^2.0.1", "@gravity-ui/react-data-table": "^2.2.1", @@ -39,6 +40,7 @@ "monaco-editor": "^0.52.2", "numeral": "^2.0.6", "path-to-regexp": "^3.3.0", + "qrcode": "^1.5.4", "qs": "^6.13.1", "react-error-boundary": "^4.1.2", "react-helmet-async": "^2.0.5", @@ -72,6 +74,7 @@ "@types/jest": "^29.5.14", "@types/lodash": "^4.17.14", "@types/numeral": "^2.0.5", + "@types/qrcode": "^1.5.5", "@types/qs": "^6.9.18", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", @@ -98,7 +101,8 @@ "source-map-explorer": "^2.5.3", "stylelint": "^15.11.0", "ts-jest": "^29.2.5", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "webpack": "^5.98.0" }, "peerDependencies": { "monaco-yql-languages": ">=1.3.0", @@ -3149,6 +3153,15 @@ } } }, + "node_modules/@gravity-ui/illustrations": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/illustrations/-/illustrations-2.0.0.tgz", + "integrity": "sha512-ipVtYuFgheDp0VoJNhpYnJmVrcnHj19ECfbossIwzC1CXYZdrzKYSS/29GfGL8b3G0ZsvPWJeI0XhV2uw1/pLw==", + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, "node_modules/@gravity-ui/navigation": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/@gravity-ui/navigation/-/navigation-2.30.0.tgz", @@ -5779,6 +5792,15 @@ "integrity": "sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==", "dev": true }, + "node_modules/@types/qrcode": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", + "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.18", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", @@ -6683,7 +6705,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "devOptional": true, "engines": { "node": ">=8" } @@ -7839,7 +7860,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "engines": { "node": ">=6" } @@ -10107,6 +10127,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -12861,7 +12886,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -19354,7 +19378,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "engines": { "node": ">=6" } @@ -19760,6 +19783,14 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -21537,6 +21568,203 @@ "teleport": ">=0.2.0" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/qrcode/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/qrcode/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/qrcode/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.13.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", @@ -24171,7 +24399,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -24185,6 +24412,11 @@ "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -24918,8 +25150,7 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "optional": true + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "node_modules/set-function-length": { "version": "1.2.2", @@ -25958,7 +26189,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "devOptional": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -28127,9 +28357,9 @@ } }, "node_modules/webpack": { - "version": "5.97.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", - "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "version": "5.98.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", + "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.7", @@ -28150,9 +28380,9 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", + "schema-utils": "^4.3.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", + "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, @@ -28292,55 +28522,6 @@ "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -28489,6 +28670,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, "node_modules/which-typed-array": { "version": "1.1.18", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", diff --git a/package.json b/package.json index e5e9a9b8c7..badc5cb582 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@gravity-ui/date-utils": "^2.5.6", "@gravity-ui/i18n": "^1.7.0", "@gravity-ui/icons": "^2.12.0", + "@gravity-ui/illustrations": "^2.0.0", "@gravity-ui/navigation": "^2.30.0", "@gravity-ui/paranoid": "^2.0.1", "@gravity-ui/react-data-table": "^2.2.1", @@ -41,6 +42,7 @@ "monaco-editor": "^0.52.2", "numeral": "^2.0.6", "path-to-regexp": "^3.3.0", + "qrcode": "^1.5.4", "qs": "^6.13.1", "react-error-boundary": "^4.1.2", "react-helmet-async": "^2.0.5", @@ -135,6 +137,7 @@ "@types/jest": "^29.5.14", "@types/lodash": "^4.17.14", "@types/numeral": "^2.0.5", + "@types/qrcode": "^1.5.5", "@types/qs": "^6.9.18", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", @@ -161,7 +164,8 @@ "source-map-explorer": "^2.5.3", "stylelint": "^15.11.0", "ts-jest": "^29.2.5", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "webpack": "^5.98.0" }, "peerDependencies": { "monaco-yql-languages": ">=1.3.0", diff --git a/src/components/ErrorBoundary/ErrorBoundary.scss b/src/components/ErrorBoundary/ErrorBoundary.scss index 290dc77d94..f392c82e5e 100644 --- a/src/components/ErrorBoundary/ErrorBoundary.scss +++ b/src/components/ErrorBoundary/ErrorBoundary.scss @@ -1,42 +1,38 @@ @use '../../styles/mixins.scss'; .ydb-error-boundary { - display: flex; - flex-direction: row; - align-items: flex-start; + --g-definition-list-item-gap: var(--g-spacing-1); - padding: 20px; + padding: var(--g-spacing-8); - @include mixins.body-2-typography(); + &__error-stack { + &-wrapper { + overflow: auto; - &__illustration { - width: 230px; - height: 230px; - margin-right: 20px; - } - &__error-title { - margin-top: 44px; - @include mixins.lead-typography(); - } - &__error-description { - margin-top: 12px; - } - &__show-details { - margin-top: 8px; - } - &__error-details { - padding: 13px 18px; + width: 800px; + height: 430px; + + border-radius: var(--g-border-radius-xs); + background-color: var(--code-background-color); + scrollbar-color: var(--g-color-scroll-handle) transparent; + } + + &-title { + position: sticky; + left: 0; + + padding: var(--g-spacing-2) var(--g-spacing-3); - white-space: pre-wrap; + border-bottom: 1px solid var(--g-color-line-generic); + } + &-code { + padding: var(--g-spacing-3) var(--g-spacing-3) var(--g-spacing-2); - border: 1px solid var(--g-color-line-generic); - background-color: var(--g-color-base-generic-ultralight); + white-space: pre-wrap; + } } - &__actions { - display: flex; - flex-direction: row; - gap: 10px; - margin-top: 20px; + &__qr-help-text { + text-align: right; } } diff --git a/src/components/ErrorBoundary/ErrorBoundary.tsx b/src/components/ErrorBoundary/ErrorBoundary.tsx index 8b5e1085e6..77332a5288 100644 --- a/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,12 +1,17 @@ -import {Button, Disclosure} from '@gravity-ui/uikit'; +import React from 'react'; + +import {InternalError} from '@gravity-ui/illustrations'; +import {DefinitionList, Flex, Text} from '@gravity-ui/uikit'; +import QRCode from 'qrcode'; import {ErrorBoundary as ErrorBoundaryBase} from 'react-error-boundary'; import {cn} from '../../utils/cn'; import {registerError} from '../../utils/registerError'; import {useComponent} from '../ComponentsProvider/ComponentsProvider'; -import {Illustration} from '../Illustration'; import i18n from './i18n'; +import type {DiagnosticsData} from './utils'; +import {collectDiagnosticsData, prepareErrorStack} from './utils'; import './ErrorBoundary.scss'; @@ -19,29 +24,16 @@ export function ErrorBoundary({children}: {children?: React.ReactNode}) { interface ErrorBoundaryProps { children?: React.ReactNode; - useRetry?: boolean; - onReportProblem?: (error?: Error) => void; } -export function ErrorBoundaryInner({ - children, - useRetry = true, - onReportProblem, -}: ErrorBoundaryProps) { +export function ErrorBoundaryInner({children}: ErrorBoundaryProps) { return ( { registerError(error, info.componentStack ?? undefined, 'error-boundary'); }} - fallbackRender={({error, resetErrorBoundary}) => { - return ( - - ); + fallbackRender={({error}) => { + return ; }} > {children} @@ -51,38 +43,98 @@ export function ErrorBoundaryInner({ interface ErrorBoundaryFallbackProps { error: Error; - useRetry?: boolean; - resetErrorBoundary?: () => void; - onReportProblem?: (error?: Error) => void; } -export function ErrorBoundaryFallback({ - error, - resetErrorBoundary, - useRetry, - onReportProblem, -}: ErrorBoundaryFallbackProps) { +export function ErrorBoundaryFallback({error}: ErrorBoundaryFallbackProps) { + const [diagnosticsData, setDiagnosticsData] = React.useState(); + + React.useEffect(() => { + collectDiagnosticsData(error).then((data) => { + setDiagnosticsData(data); + }); + }, [error]); + + return ( + + + + + + {i18n('error-title')} + + {i18n('error-description')} + + + + + + + + + + {i18n('send-qr-message')} + + + + + + ); +} + +function DiagnosticsDataList({data}: {data?: DiagnosticsData}) { + return ( + + {data?.uiVersion && typeof data.uiVersion === 'string' && ( + + {data.uiVersion} + + )} + {data?.backendVersion && typeof data.backendVersion === 'string' && ( + + {data.backendVersion} + + )} + {data?.error.message} + + ); +} + +function ErrorStack({stack}: {stack?: string}) { + if (!stack) { + return null; + } + + const stackToDisplay = prepareErrorStack(stack, { + trim: false, + maxLines: undefined, + }); + return ( -
- -
-

{i18n('error-title')}

-
{i18n('error-description')}
- -
{error.stack}
-
-
- {useRetry && ( - - )} - {onReportProblem && ( - - )} -
-
-
+ + + {i18n('stack-title')} + + + {stackToDisplay} + + ); } + +function DiagnosticsDataQR({data}: {data?: DiagnosticsData}) { + const canvasRef = React.useRef(null); + + React.useEffect(() => { + if (data) { + QRCode.toCanvas(canvasRef.current, JSON.stringify(data), { + errorCorrectionLevel: 'L', + width: 400, + }); + } + }, [data]); + + if (!data) { + return null; + } + + return ; +} diff --git a/src/components/ErrorBoundary/__tests__/prepareErrorStack.test.ts b/src/components/ErrorBoundary/__tests__/prepareErrorStack.test.ts new file mode 100644 index 0000000000..bbcedcbab5 --- /dev/null +++ b/src/components/ErrorBoundary/__tests__/prepareErrorStack.test.ts @@ -0,0 +1,76 @@ +import {prepareErrorStack} from '../utils'; + +describe('prepareErrorStack', () => { + test('Replace origin in error trace', () => { + const stack = `TypeError: Cannot read properties of null (reading 'hello') + at Content (http://localhost:3000/static/js/bundle.js:4728:8) + at renderWithHooks (http://localhost:3000/static/js/bundle.js:98996:22) + at mountIndeterminateComponent (http://localhost:3000/static/js/bundle.js:102967:17) + at beginWork (http://localhost:3000/static/js/bundle.js:104270:20) + at beginWork$1 (http://localhost:3000/static/js/bundle.js:109229:18) + at performUnitOfWork (http://localhost:3000/static/js/bundle.js:108499:16) + at workLoopSync (http://localhost:3000/static/js/bundle.js:108422:9) + at renderRootSync (http://localhost:3000/static/js/bundle.js:108395:11) + at recoverFromConcurrentError (http://localhost:3000/static/js/bundle.js:107887:24) + at performConcurrentWorkOnRoot (http://localhost:3000/static/js/bundle.js:107800:26)`; + + const preparedStack = `TypeError: Cannot read properties of null (reading 'hello') + at Content (/static/js/bundle.js:4728:8) + at renderWithHooks (/static/js/bundle.js:98996:22) + at mountIndeterminateComponent (/static/js/bundle.js:102967:17) + at beginWork (/static/js/bundle.js:104270:20) + at beginWork$1 (/static/js/bundle.js:109229:18) + at performUnitOfWork (/static/js/bundle.js:108499:16) + at workLoopSync (/static/js/bundle.js:108422:9) + at renderRootSync (/static/js/bundle.js:108395:11) + at recoverFromConcurrentError (/static/js/bundle.js:107887:24) + at performConcurrentWorkOnRoot (/static/js/bundle.js:107800:26)`; + + // Mock window.location.origin that is used inside prepareErrorStack + const windowSpy = jest.spyOn(window, 'window', 'get'); + windowSpy.mockImplementation(() => { + return { + location: { + origin: 'http://localhost:3000', + }, + } as Window & typeof globalThis; + }); + + expect(prepareErrorStack(stack, {trim: false, maxLines: undefined})).toBe(preparedStack); + + windowSpy.mockRestore(); + }); + + test('Limit trace to maxLines', () => { + const stack = `TypeError: Cannot read properties of null (reading 'hello') + at Content (/static/js/bundle.js:4725:8) + at renderWithHooks (/static/js/bundle.js:98993:22) + at mountIndeterminateComponent (/static/js/bundle.js:102964:17) + at beginWork (/static/js/bundle.js:104267:20) + at beginWork$1 (/static/js/bundle.js:109226:18) + at performUnitOfWork (/static/js/bundle.js:108496:16) + at workLoopSync (/static/js/bundle.js:108419:9) + at renderRootSync (/static/js/bundle.js:108392:11) + at recoverFromConcurrentError (/static/js/bundle.js:107884:24) + at performConcurrentWorkOnRoot (/static/js/bundle.js:107797:26)`; + + const preparedStack = `TypeError: Cannot read properties of null (reading 'hello') + at Content (/static/js/bundle.js:4725:8) + at renderWithHooks (/static/js/bundle.js:98993:22) + at mountIndeterminateComponent (/static/js/bundle.js:102964:17)`; + + expect(prepareErrorStack(stack, {trim: false, maxLines: 3})).toBe(preparedStack); + }); + + test('Trims data if needed', () => { + const stack = `TypeError: Cannot read properties of null (reading 'hello') + at Content (/static/js/bundle.js:4725:8)`; + const preparedStack = `TypeError: Cannot read properties of null (reading 'hello') +at Content (/static/js/bundle.js:4725:8)`; + expect(prepareErrorStack(stack, {trim: true})).toBe(preparedStack); + }); + + test('Return undefined and undefined', () => { + expect(prepareErrorStack(undefined)).toBe(undefined); + }); +}); diff --git a/src/components/ErrorBoundary/i18n/en.json b/src/components/ErrorBoundary/i18n/en.json index 0ab42b4214..a40c2b7489 100644 --- a/src/components/ErrorBoundary/i18n/en.json +++ b/src/components/ErrorBoundary/i18n/en.json @@ -1,7 +1,10 @@ { - "error-title": "Something went wrong", - "error-description": "We have something broken, but don't worry, it won't last long", - "show-details": "Show details", - "report-problem": "Report a problem", - "button-reset": "Try again" + "error-title": "Oops! Something went wrong...", + "error-description": "Something seems to be broken. Please contact support for help.", + "send-qr-message": "Send QR code to the support", + "stack-title": "Trace", + + "ui-version": "UI version", + "backend-version": "Backend version", + "error": "Error" } diff --git a/src/components/ErrorBoundary/i18n/index.ts b/src/components/ErrorBoundary/i18n/index.ts index f4fb03e376..24b58092a0 100644 --- a/src/components/ErrorBoundary/i18n/index.ts +++ b/src/components/ErrorBoundary/i18n/index.ts @@ -1,8 +1,7 @@ import {registerKeysets} from '../../../utils/i18n'; import en from './en.json'; -import ru from './ru.json'; const COMPONENT = 'ydb-error-boundary'; -export default registerKeysets(COMPONENT, {ru, en}); +export default registerKeysets(COMPONENT, {en}); diff --git a/src/components/ErrorBoundary/i18n/ru.json b/src/components/ErrorBoundary/i18n/ru.json deleted file mode 100644 index be601c0aeb..0000000000 --- a/src/components/ErrorBoundary/i18n/ru.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "error-title": "Что-то пошло не так", - "error-description": "У нас что-то сломалось, но не переживайте, это ненадолго", - "show-details": "Показать детали", - "report-problem": "Сообщить о проблеме", - "button-reset": "Попробовать снова" -} diff --git a/src/components/ErrorBoundary/utils.tsx b/src/components/ErrorBoundary/utils.tsx new file mode 100644 index 0000000000..e7c154b5cb --- /dev/null +++ b/src/components/ErrorBoundary/utils.tsx @@ -0,0 +1,58 @@ +import {prepareErrorMessage} from '../../utils/prepareErrorMessage'; + +export async function collectDiagnosticsData(error: Error) { + return await getBackendVersion().then((backendVersion) => { + return { + location: window.location.href, + userAgent: navigator.userAgent, + error: { + message: prepareErrorMessage(error), + stack: prepareErrorStack(error.stack, {trim: true, maxLines: 10}), + }, + uiVersion: process.env.UI_VERSION, + backendVersion, + }; + }); +} + +export type DiagnosticsData = Awaited>; + +async function getBackendVersion() { + try { + // node_id=. returns data about node that fullfills request + // normally this request should be fast (200-300ms with good connection) + // timeout=1000 in order not to wait too much in case everything is broken + const data = await window.api.viewer.getNodeInfo('.', {timeout: 1000}); + return data?.SystemStateInfo?.[0]?.Version; + } catch (error) { + return {error: prepareErrorMessage(error)}; + } +} + +export function prepareErrorStack( + stack?: string, + {trim = true, maxLines}: {trim?: boolean; maxLines?: number} = {}, +) { + return ( + stack + ?.split('\n') + .map((line, index) => { + // Do not prepare line with error message + if (index === 0) { + return line; + } + + // Remove repeating origin from stack trace location + const preparedLine = line.replace(`(${window.location.origin}/`, '(/'); + + if (trim) { + return preparedLine.trim(); + } + + return preparedLine; + }) + // Do not slice first row + .slice(0, maxLines ? maxLines + 1 : undefined) + .join('\n') + ); +} diff --git a/src/components/Errors/ResponseError/ResponseError.tsx b/src/components/Errors/ResponseError/ResponseError.tsx index 026311f08a..7adf13fdaf 100644 --- a/src/components/Errors/ResponseError/ResponseError.tsx +++ b/src/components/Errors/ResponseError/ResponseError.tsx @@ -1,3 +1,4 @@ +import {prepareErrorMessage} from '../../../utils/prepareErrorMessage'; import i18n from '../i18n'; interface ResponseErrorProps { @@ -11,19 +12,7 @@ export const ResponseError = ({ className, defaultMessage = i18n('responseError.defaultMessage'), }: ResponseErrorProps) => { - let statusText = ''; + const message = prepareErrorMessage(error) || defaultMessage; - if (error && typeof error === 'string') { - statusText = error; - } - if (error && typeof error === 'object') { - if ('data' in error && typeof error.data === 'string') { - statusText = error.data; - } else if ('statusText' in error && typeof error.statusText === 'string') { - statusText = error.statusText; - } else if ('message' in error && typeof error.message === 'string') { - statusText = error.message; - } - } - return
{statusText || defaultMessage}
; + return
{message}
; }; diff --git a/src/containers/App/App.scss b/src/containers/App/App.scss index 597b208688..18db61c276 100644 --- a/src/containers/App/App.scss +++ b/src/containers/App/App.scss @@ -49,6 +49,16 @@ body, --ydb-color-status-black: var(--g-color-base-misc-heavy); --g-popover-max-width: 500px; + + &_theme_light, + &_theme_light-hc { + --code-background-color: var(--g-color-base-simple-hover); + } + &_theme_dark, + &_theme_dark-hc { + // monaco background - vscDarkPlus theme + --code-background-color: #1e1e1e; + } } :is(#tab, .g-tabs-item_active .g-tabs-item__title) { diff --git a/src/containers/Tenant/Query/QueryResult/components/QueryInfoDropdown/useQueryInfoMenuItems.tsx b/src/containers/Tenant/Query/QueryResult/components/QueryInfoDropdown/useQueryInfoMenuItems.tsx index 858c5ae8f0..9b02ed9daf 100644 --- a/src/containers/Tenant/Query/QueryResult/components/QueryInfoDropdown/useQueryInfoMenuItems.tsx +++ b/src/containers/Tenant/Query/QueryResult/components/QueryInfoDropdown/useQueryInfoMenuItems.tsx @@ -7,12 +7,12 @@ import {Text} from '@gravity-ui/uikit'; import {planToSvgApi} from '../../../../../../store/reducers/planToSvg'; import type {QueryPlan, ScriptPlan, TKqpStatsQuery} from '../../../../../../types/api/query'; import createToast from '../../../../../../utils/createToast'; +import {createAndDownloadJsonFile, downloadFile} from '../../../../../../utils/downloadFile'; import {prepareCommonErrorMessage} from '../../../../../../utils/errors'; import {parseQueryError} from '../../../../../../utils/query'; import i18n from '../../i18n'; import {b} from './shared'; -import {downloadFile} from './utils'; export interface MenuItemContentProps { title: string; @@ -146,12 +146,10 @@ export function useQueryInfoMenuItems({ ...(parsedError && {error: parsedError}), }; - const blob = new Blob([JSON.stringify(diagnosticsData, null, 2)], { - type: 'application/json', - }); - const url = URL.createObjectURL(blob); - downloadFile(url, `query-diagnostics-${new Date().getTime()}.json`); - URL.revokeObjectURL(url); + createAndDownloadJsonFile( + diagnosticsData, + `query-diagnostics-${new Date().getTime()}`, + ); }; menuItems.push([ diff --git a/src/containers/Tenant/Query/QueryResult/components/QueryInfoDropdown/utils.ts b/src/containers/Tenant/Query/QueryResult/components/QueryInfoDropdown/utils.ts deleted file mode 100644 index 0eb2f5c08a..0000000000 --- a/src/containers/Tenant/Query/QueryResult/components/QueryInfoDropdown/utils.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function downloadFile(url: string, filename: string) { - const link = document.createElement('a'); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); -} diff --git a/src/containers/UserSettings/settings.tsx b/src/containers/UserSettings/settings.tsx index 5a8692afe5..57907ef0a0 100644 --- a/src/containers/UserSettings/settings.tsx +++ b/src/containers/UserSettings/settings.tsx @@ -21,8 +21,6 @@ import {Lang, defaultLang} from '../../utils/i18n'; import type {SettingProps, SettingsInfoFieldProps} from './Setting'; import i18n from './i18n'; -import packageJson from '../../../package.json'; - export interface SettingsSection { id: string; title: string; @@ -143,7 +141,7 @@ export const autocompleteOnEnterSetting: SettingProps = { export const interfaceVersionInfoField: SettingsInfoFieldProps = { title: i18n('settings.about.interfaceVersionInfoField.title'), type: 'info', - content: packageJson.version, + content: process.env.UI_VERSION, }; export const appearanceSection: SettingsSection = { diff --git a/src/services/api/base.ts b/src/services/api/base.ts index f0792c15c8..0d1d85b884 100644 --- a/src/services/api/base.ts +++ b/src/services/api/base.ts @@ -11,6 +11,7 @@ export type AxiosOptions = { concurrentId?: string; signal?: AbortSignal; withRetries?: boolean; + timeout?: number; }; export class BaseYdbAPI extends AxiosWrapper { diff --git a/src/services/api/viewer.ts b/src/services/api/viewer.ts index 6c6342fbc4..9036396e34 100644 --- a/src/services/api/viewer.ts +++ b/src/services/api/viewer.ts @@ -53,13 +53,14 @@ export class ViewerAPI extends BaseYdbAPI { ); } - getNodeInfo(id?: string | number, {concurrentId, signal}: AxiosOptions = {}) { + /** id=. returns data about node that fullfills request */ + getNodeInfo(id?: string | number, {concurrentId, timeout, signal}: AxiosOptions = {}) { return this.get( this.getPath('/viewer/json/sysinfo?enums=true'), { node_id: id, }, - {concurrentId, requestConfig: {signal}}, + {concurrentId, requestConfig: {signal}, timeout}, ); } diff --git a/src/styles/illustrations.scss b/src/styles/illustrations.scss new file mode 100644 index 0000000000..10c9916df7 --- /dev/null +++ b/src/styles/illustrations.scss @@ -0,0 +1,20 @@ +@forward '@gravity-ui/illustrations/styles/styles.scss'; + +// Replace yellow and orange colors with blue + +.g-root { + &_theme_light, + &_theme_light-hc { + --gil-color-object-base: var(--g-color-private-blue-450-solid); + --gil-color-object-accent-heavy: var(--g-color-private-blue-850-solid); + --gil-color-object-hightlight: var(--g-color-private-blue-350-solid); + --gil-color-shadow-over-object: var(--g-color-private-blue-650-solid); + } + &_theme_dark, + &_theme_dark-hc { + --gil-color-object-base: var(--g-color-private-blue-450-solid); + --gil-color-object-accent-heavy: var(--g-color-private-blue-850-solid); + --gil-color-object-hightlight: var(--g-color-private-blue-350-solid); + --gil-color-shadow-over-object: var(--g-color-private-blue-650-solid); + } +} diff --git a/src/styles/index.scss b/src/styles/index.scss index b578f7d8b5..b941caa919 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -1,6 +1,7 @@ @forward '@gravity-ui/uikit/styles/styles.scss'; @forward './themes.scss'; @forward './unipika.scss'; +@forward './illustrations.scss'; body { margin: 0; diff --git a/src/utils/downloadFile.ts b/src/utils/downloadFile.ts new file mode 100644 index 0000000000..f07840c7fa --- /dev/null +++ b/src/utils/downloadFile.ts @@ -0,0 +1,17 @@ +export function downloadFile(url: string, filename: string) { + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); +} + +export const createAndDownloadJsonFile = (data: unknown, fileName: string) => { + const blob = new Blob([JSON.stringify(data, null, 2)], { + type: 'application/json', + }); + const url = URL.createObjectURL(blob); + downloadFile(url, `${fileName}.json`); + URL.revokeObjectURL(url); +}; diff --git a/src/utils/prepareErrorMessage.ts b/src/utils/prepareErrorMessage.ts new file mode 100644 index 0000000000..fdf995db2a --- /dev/null +++ b/src/utils/prepareErrorMessage.ts @@ -0,0 +1,17 @@ +export function prepareErrorMessage(error: unknown) { + if (error && typeof error === 'string') { + return error; + } + + if (error && typeof error === 'object') { + if ('data' in error && typeof error.data === 'string') { + return error.data; + } else if ('statusText' in error && typeof error.statusText === 'string') { + return error.statusText; + } else if ('message' in error && typeof error.message === 'string') { + return error.message; + } + } + + return ''; +}