diff --git a/.circleci/config.yml b/.circleci/config.yml index 5728666332..52bbedc828 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -94,6 +94,11 @@ aliases: - re-st # Redis Enterprise with Standalone inside - re-clu # Redis Enterprise with Cluster inside - re-crdt # Redis Enterprise with active-active database inside + iTestsNamesShort: &iTestsNamesShort + - oss-st-5-pass # OSS Standalone v5 with admin pass required + - oss-st-6-tls-auth # OSS Standalone v6 with TLS auth required + - oss-clu-tls # OSS Cluster with TLS enabled + - re-crdt # Redis Enterprise with active-active database inside guides-filter: &guidesFilter filters: branches: @@ -287,7 +292,7 @@ jobs: sudo cp -R /tmp/itest/coverages /usr/src/app && sudo chmod 777 -R /usr/src/app cd /usr/src/app && npx nyc report -t ./coverages -r text -r text-summary e2e-app-image: - executor: linux-executor + executor: linux-executor-dlc parameters: report: description: Send report for test run to slack @@ -306,7 +311,7 @@ jobs: - run: name: .AppImage tests command: | - cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/**/*.e2e.ts" | circleci tests split) && cd ../.. + cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/**/*.e2e.ts" | circleci tests split --split-by=timings) && cd ../.. .circleci/e2e/test.app-image.sh - when: condition: @@ -320,6 +325,9 @@ jobs: curl -H "Content-type: application/json" --data @e2e.report.json -H "Authorization: Bearer $SLACK_TEST_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage - store_test_results: path: ./tests/e2e/results + - store_artifacts: + path: tests/e2e/report + destination: tests/e2e/report e2e-exe: executor: name: win/default @@ -340,7 +348,7 @@ jobs: - run: choco install nodejs --version=16.15.1 - run: command: | - cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/**/*.e2e.ts" | circleci tests split) && cd ../.. + cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/**/*.e2e.ts" | circleci tests split --split-by=timings) && cd ../.. .circleci/e2e/test.exe.cmd shell: bash.exe - when: @@ -356,9 +364,11 @@ jobs: shell: bash.exe - store_test_results: path: ./tests/e2e/results - + - store_artifacts: + path: tests/e2e/report + destination: tests/e2e/report e2e-tests: - executor: linux-executor + executor: linux-executor-dlc parameters: build: description: Backend build to run tests over @@ -389,12 +399,12 @@ jobs: - run: name: Run tests command: | - cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/**/*.e2e.ts" | circleci tests split) && cd ../.. + cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/**/*.e2e.ts" | circleci tests split --split-by=timings) && cd ../.. TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \ docker-compose \ -f tests/e2e/rte.docker-compose.yml \ -f tests/e2e/docker.web.docker-compose.yml \ - up --abort-on-container-exit --force-recreate + up --abort-on-container-exit --force-recreate --build no_output_timeout: 5m - when: condition: @@ -403,7 +413,7 @@ jobs: - run: name: Run tests command: | - cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/**/*.e2e.ts" | circleci tests split) && cd ../.. + cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/**/*.e2e.ts" | circleci tests split --split-by=timings) && cd ../.. TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \ docker-compose \ -f tests/e2e/rte.docker-compose.yml \ @@ -422,6 +432,9 @@ jobs: curl -H "Content-type: application/json" --data @e2e.report.json -H "Authorization: Bearer $SLACK_TEST_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage - store_test_results: path: ./tests/e2e/results + - store_artifacts: + path: tests/e2e/report + destination: tests/e2e/report # Build jobs setup-sign-certificates: @@ -690,7 +703,6 @@ jobs: - store_artifacts: path: release destination: release - release-aws-test: executor: linux-executor steps: @@ -858,96 +870,193 @@ jobs: done workflows: - build: + # FE Unit tests for "fe/feature" or "fe/bugfix" branches only + frontend-tests: jobs: - # unit tests (on any commit) - unit-tests-ui: name: UTest - UI + filters: + branches: + only: + - /^fe/feature.*/ + - /^fe/bugfix.*/ + # BE Unit + Integration (limited RTEs) tests for "be/feature" or "be/bugfix" branches only + backend-tests: + jobs: - unit-tests-api: name: UTest - API - - # integration tests run in parallel (on any commit) - # target server runs locally to calculate code coverage + filters: + branches: + only: + - /^be/feature.*/ + - /^be/bugfix.*/ + - integration-tests-run: + matrix: + alias: itest-code + parameters: + rte: *iTestsNamesShort + name: ITest - << matrix.rte >> (code) + requires: + - UTest - API + # E2E tests for "e2e/feature" or "e2e/bugfix" branches only + e2e-tests: + jobs: + - approve: + name: Start E2E Tests + type: approval + filters: + branches: + only: + - /^e2e/feature.*/ + - /^e2e/bugfix.*/ + - docker: + name: Build docker image + requires: + - Start E2E Tests + - e2e-tests: + name: E2ETest + build: docker + parallelism: 4 + requires: + - Build docker image + # Workflow for feature, bugfix, main branches + feature-main-branch: + jobs: + # Approve to run all (unit, integration, e2e) tests + - approve: + name: Start All Tests + type: approval + filters: + branches: + only: + - /^feature.*/ + - /^bugfix.*/ + - main + # FE tests + - unit-tests-ui: + name: UTest - UI + requires: + - Start All Tests + # BE tests + - unit-tests-api: + name: UTest - API + requires: + - Start All Tests - integration-tests-run: matrix: alias: itest-code parameters: rte: *iTestsNames name: ITest - << matrix.rte >> (code) + requires: + - Start All Tests - integration-tests-coverage: name: ITest - Final coverage requires: - itest-code - # e2e tests (doesn't affect pipeline even if fail) + # E2E tests - docker: name: Build docker image - filters: &e2eFilter - branches: - only: - - /^release.*/ - - /^e2e.*/ - - /^feature/e2e.*/ - - main - - latest + requires: + - Start All Tests - e2e-tests: name: E2ETest build: docker parallelism: 4 - filters: *e2eFilter requires: - Build docker image - - # build and release electron app (dev) - - dev-build-approve: - name: Build dev app + # Approve to build + - approve: + name: Build App type: approval requires: - UTest - UI - UTest - API - ITest - Final coverage - <<: *devFilter + filters: + branches: + only: + - /^e2e/feature.*/ + - /^e2e/bugfix.*/ + # build electron app (dev) from "build" branches + build: + jobs: - setup-sign-certificates: name: Setup sign certificates (dev) - requires: - - Build dev app - <<: *devFilter + filters: + branches: + only: + - /^build.*/ - setup-build: name: Setup build (dev) env: dev requires: - Setup sign certificates (dev) - <<: *devFilter - linux: name: Build app - Linux (dev) env: dev - requires: &stageElectronBuildRequires + requires: &devBuildRequire - Setup build (dev) - <<: *devFilter - macosx: name: Build app - MacOS (dev) env: dev - requires: *stageElectronBuildRequires - <<: *devFilter + requires: *devBuildRequire - windows: name: Build app - Windows (dev) env: dev - requires: *stageElectronBuildRequires - <<: *devFilter + requires: *devBuildRequire - store-build-artifacts: name: Store build artifacts (dev) requires: - Build app - Linux (dev) - Build app - MacOS (dev) - Build app - Windows (dev) - - release-aws-test: - name: Release AWS test + name: Release AWS dev requires: - Build app - Linux (dev) - Build app - MacOS (dev) - Build app - Windows (dev) + # Main workflow for release/* and latest branches only + release: + jobs: + # unit tests (on any commit) + - unit-tests-ui: + name: UTest - UI + filters: &releaseAndLatestFilter + branches: + only: + - /^release.*/ + - latest + - unit-tests-api: + name: UTest - API + filters: *releaseAndLatestFilter + # integration tests + - integration-tests-run: + matrix: + alias: itest-code + parameters: + rte: *iTestsNames + name: ITest - << matrix.rte >> (code) + filters: *releaseAndLatestFilter + - integration-tests-coverage: + name: ITest - Final coverage + requires: + - itest-code + # e2e tests (doesn't affect pipeline even if fail) + - docker: + name: Build docker image + filters: *releaseAndLatestFilter + - e2e-tests: + name: E2ETest + build: docker + parallelism: 4 + requires: + - Build docker image - # build and release electron app (stage) + + # ================== STAGE ================== + # prebuild (stage) - setup-sign-certificates: name: Setup sign certificates (stage) requires: @@ -959,28 +1068,24 @@ workflows: name: Setup build (stage) requires: - Setup sign certificates (stage) - <<: *stageFilter + # build electron app (stage) - linux: name: Build app - Linux (stage) requires: &stageElectronBuildRequires - Setup build (stage) - <<: *stageFilter - macosx: name: Build app - MacOS (stage) requires: *stageElectronBuildRequires - <<: *stageFilter - windows: name: Build app - Windows (stage) requires: *stageElectronBuildRequires - <<: *stageFilter - + # release to AWS (stage) - release-aws-test: - name: Release AWS test + name: Release AWS stage requires: - Build app - Linux (stage) - Build app - MacOS (stage) - Build app - Windows (stage) - # Needs approval from QA team that build was tested before merging to latest - qa-approve: name: Approved by QA team @@ -990,6 +1095,7 @@ workflows: - Build app - MacOS (stage) - Build app - Windows (stage) + # ================== PROD ================== # build and release electron app (prod) - setup-sign-certificates: name: Setup sign certificates (prod) @@ -1003,24 +1109,20 @@ workflows: env: prod requires: - Setup sign certificates (prod) - <<: *prodFilter - linux: name: Build app - Linux (prod) env: prod requires: &prodElectronBuildRequires - Setup build (prod) - <<: *prodFilter - macosx: name: Build app - MacOS (prod) env: prod requires: *prodElectronBuildRequires - <<: *prodFilter - windows: name: Build app - Windows (prod) env: prod requires: *prodElectronBuildRequires - <<: *prodFilter - # virus check all electron apps (prod only) + # virus check all electron apps (prod) - virustotal: name: Virus check - AppImage (prod) ext: .AppImage @@ -1046,8 +1148,7 @@ workflows: ext: .exe requires: - Build app - Windows (prod) - - # upload release to AWS + # upload release to prerelease AWS folder - release-aws-private: name: Release AWS S3 Private (prod) requires: @@ -1056,7 +1157,6 @@ workflows: - Virus check x64 - dmg (prod) - Virus check arm64 - dmg (prod) - Virus check - exe (prod) - # Manual approve for publish release - approve-publish: name: Approve Publish Release (prod) @@ -1064,14 +1164,12 @@ workflows: requires: - Release AWS S3 Private (prod) <<: *prodFilter # double check for "latest" - # Publish release - publish-prod-aws: name: Publish AWS S3 requires: - Approve Publish Release (prod) <<: *prodFilter # double check for "latest" - # Nightly tests nightly: triggers: @@ -1120,12 +1218,12 @@ workflows: requires: - Build docker image # e2e desktop tests on AppImage build - - e2e-app-image: - name: E2ETest (AppImage) - Nightly - parallelism: 4 - report: true - requires: - - Build app - Linux (stage) + # - e2e-app-image: + # name: E2ETest (AppImage) - Nightly + # parallelism: 4 + # report: true + # requires: + # - Build app - Linux (stage) # # e2e desktop tests on exe build # - e2e-exe: # name: E2ETest (exe) - Nightly diff --git a/.eslintignore b/.eslintignore index 61e7959669..b75cc6839f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -57,3 +57,7 @@ package.json *.css.d.ts *.sass.d.ts *.scss.d.ts + +# temp folders - remove in future after fix all issues +redisinsight/ui/src/packages/redisgraph +redisinsight/ui/src/packages/redistimeseries-app diff --git a/.github/redisinsight_browser.png b/.github/redisinsight_browser.png index ef0d8ee738..fe717aa912 100644 Binary files a/.github/redisinsight_browser.png and b/.github/redisinsight_browser.png differ diff --git a/.gitignore b/.gitignore index f1a22a24ec..092d0b02ad 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ vendor # Parcel .parcel-cache + +# caches +.temp_cache diff --git a/README.md b/README.md index 2cc000bacc..9695f64cc1 100644 --- a/README.md +++ b/README.md @@ -20,28 +20,29 @@ RedisInsight is an intuitive and efficient GUI for Redis, allowing you to intera ### RedisInsight Highlights: -* Browse, filter and visualise your key-value Redis data structures -* CRUD support for Lists, Hashes, Strings, Sets, Sorted Sets +* Browse, filter, visualise your key-value Redis data structures and see key values in different formats (including JSON, Hex, ASCII, etc.) +* CRUD support for Lists, Hashes, Strings, Sets, Sorted Sets, and Streams * CRUD support for [RedisJSON](https://oss.redis.com/redisjson/) -* Profiler - analyze every command sent to Redis in real-time -* Introducing Workbench - advanced command line interface with intelligent command auto-complete and complex data visualizations +* Profiler - analyze every command sent to Redis in real-time +* SlowLog - analyze slow operations in Redis instances based on the [Slowlog](https://github.com/RedisInsight/RedisInsight/releases#:~:text=results%20of%20the-,Slowlog,-command%20to%20analyze) command +* Pub/Sub - support for [Redis pub/sub](https://redis.io/docs/manual/pubsub/), enabling subscription to channels and posting messages to channels +* Bulk actions - Delete the keys in bulk based on the filters set in Browser or Tree view +* Introducing Workbench - advanced command line interface with intelligent command auto-complete, complex data visualizations and support for the raw mode * Command auto-complete support for [RediSearch](https://oss.redis.com/redisearch/), [RedisJSON](https://oss.redis.com/redisjson/), [RedisGraph](https://oss.redis.com/redisgraph/), [RedisTimeSeries](https://oss.redis.com/redistimeseries/), [RedisAI](https://oss.redis.com/redisai/) * Visualizations of your [RediSearch](https://oss.redis.com/redisearch/) index, queries, and aggregations * Ability to build your own data visualization plugins * Built-in click-through guides for Redis capabilities * Oficially supported for Redis OSS, [Redis Cloud](https://redis.com/try-free/). Works with Microsoft Azure Cache for Redis (official support upcoming). -* Available for macOS, Windows and Linux +* Available for macOS (including M1), Windows and Linux Check out the [release notes](https://docs.redis.com/latest/ri/release-notes/). ## Get started with RedisInsight -This repository includes the code for RedisInsight 2.0, Currently available in public preview. Check out the [blogpost](https://redis.com/blog/introducing-redisinsight-2/) announcing it. - -The current GA version of RedisInsight is 1.11. You can install RedisInsight 2.0 along with the GA version. +This repository includes the code for the GA version of RedisInsight 2.0. Check out the [blogpost](https://redis.com/blog/introducing-redisinsight-2/) announcing it. ### Installable -Available to download for free from [here](https://redis.com/redis-enterprise/redis-insight/#insight-form). +Available to download for free from [here](https://redis.com/redis-enterprise/redis-insight/#insight-form). ### Build Alternatively you can also build from source. See our wiki for instructions. diff --git a/configs/webpack.config.base.js b/configs/webpack.config.base.js index 65e1d0f1c6..a58581b672 100644 --- a/configs/webpack.config.base.js +++ b/configs/webpack.config.base.js @@ -54,7 +54,7 @@ export default { // 'pnpapi', 'cache-manager', // 'class-validator', - 'fastify-static', + '@fastify/static', 'fastify-swagger', // 'hiredis', // 'reflect-metadata', diff --git a/configs/webpack.config.web.dev.babel.js b/configs/webpack.config.web.dev.babel.js index 6308793786..0cf064bebe 100644 --- a/configs/webpack.config.web.dev.babel.js +++ b/configs/webpack.config.web.dev.babel.js @@ -5,6 +5,7 @@ * https://webpack.js.org/concepts/hot-module-replacement/ */ +import path from 'path'; import webpack from 'webpack'; import { merge } from 'webpack-merge'; import ip from 'ip'; @@ -21,6 +22,18 @@ export default merge(commonConfig, { mode: 'development', + cache: { + type: 'filesystem', + allowCollectingMemory: true, + cacheDirectory: path.resolve(__dirname, '../.temp_cache'), + name: 'webpack', + maxAge: 86_400_000, // 1 day + buildDependencies: { + // This makes all dependencies of this file - build dependencies + config: [__filename], + } + }, + devtool: 'source-map', entry: [ diff --git a/electron-builder.json b/electron-builder.json index 5fba812a4e..ece3cdfcd6 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -75,8 +75,7 @@ "desktop": { "Name": "RedisInsight", "Type": "Application", - "Comment": "Redis GUI by Redis Ltd", - "Terminal": "true" + "Comment": "Redis GUI by Redis Ltd" } }, "directories": { diff --git a/jest.config.js b/jest.config.js index 5864bce91c..2756fd4cf7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,7 +16,11 @@ module.exports = { 'rehype-stringify': '/redisinsight/__mocks__/rehypeStringify.js', 'unist-util-visit': '/redisinsight/__mocks__/unistUtilsVisit.js', 'react-children-utilities': '/redisinsight/__mocks__/react-children-utilities.js', + d3: '/node_modules/d3/dist/d3.min.js', }, + setupFiles: [ + '/redisinsight/ui/src/setup-env.ts', + ], setupFilesAfterEnv: [ '/redisinsight/ui/src/setup-tests.ts', ], @@ -35,6 +39,11 @@ module.exports = { transformIgnorePatterns: [ 'node_modules/(?!(monaco-editor|react-monaco-editor)/)', ], + // TODO: add tests for plugins + modulePathIgnorePatterns: [ + '/redisinsight/ui/src/packages', + '/redisinsight/ui/src/mocks', + ], coverageThreshold: { global: { statements: 70, diff --git a/package.json b/package.json index ed97cd8047..9599366900 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "start:web:public": "cross-env PUBLIC_DEV=true webpack serve --config ./configs/webpack.config.web.dev.babel.js", "test": "jest ./redisinsight/ui -w 1", "test:watch": "jest ./redisinsight/ui --watch -w 1", - "test:cov": "jest ./redisinsight/ui --coverage -w 1" + "test:cov": "jest ./redisinsight/ui --coverage -w 1", + "type-check:ui": "tsc --project redisinsight/ui --noEmit" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ @@ -106,8 +107,10 @@ "@teamsupercell/typings-for-css-modules-loader": "^2.4.0", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", + "@testing-library/user-event": "^14.4.3", "@types/axios": "^0.14.0", "@types/classnames": "^2.2.11", + "@types/d3": "^7.4.0", "@types/date-fns": "^2.6.0", "@types/detect-port": "^1.3.0", "@types/electron-store": "^3.2.0", @@ -119,6 +122,7 @@ "@types/jsonpath": "^0.2.0", "@types/lodash": "^4.14.171", "@types/node": "14.14.10", + "@types/react": "^18.0.20", "@types/react-dom": "^18.0.5", "@types/react-monaco-editor": "^0.16.0", "@types/react-redux": "^7.1.12", @@ -174,6 +178,7 @@ "mini-css-extract-plugin": "^1.3.1", "moment": "^2.29.3", "monaco-editor-webpack-plugin": "^6.0.0", + "msw": "^0.45.0", "node-sass": "^6.0.1", "opencollective-postinstall": "^2.0.3", "react-hot-loader": "^4.13.0", @@ -201,6 +206,7 @@ "webpack-cli": "^4.3.0", "webpack-dev-server": "^3.11.0", "webpack-merge": "^5.4.0", + "whatwg-fetch": "^3.6.2", "yarn-deduplicate": "^3.1.0" }, "dependencies": { @@ -212,6 +218,7 @@ "buffer": "^6.0.3", "classnames": "^2.3.1", "connection-string": "^4.3.2", + "d3": "^7.6.1", "date-fns": "^2.16.1", "detect-port": "^1.3.0", "electron-context-menu": "^3.1.0", @@ -221,8 +228,12 @@ "formik": "^2.2.9", "html-entities": "^2.3.2", "html-react-parser": "^1.2.4", + "java-object-serialization": "^0.1.1", + "jpickle": "^0.4.1", "jsonpath": "^1.1.1", "lodash": "^4.17.21", + "php-serialize": "^4.0.2", + "rawproto": "^0.7.6", "react": "^18.2.0", "react-contenteditable": "^3.3.5", "react-dom": "^18.2.0", diff --git a/redisinsight/api/config/ormconfig.ts b/redisinsight/api/config/ormconfig.ts index a90b5be365..0649860754 100644 --- a/redisinsight/api/config/ormconfig.ts +++ b/redisinsight/api/config/ormconfig.ts @@ -29,9 +29,6 @@ const ormConfig: TypeOrmModuleOptions = { NotificationEntity, ], migrations, - cli: { - migrationsDir: 'migration', - }, }; export default ormConfig; diff --git a/redisinsight/api/config/production.ts b/redisinsight/api/config/production.ts index 893a6a877b..01cb28b7dd 100644 --- a/redisinsight/api/config/production.ts +++ b/redisinsight/api/config/production.ts @@ -22,6 +22,9 @@ export default { server: { env: 'production', }, + analytics: { + writeKey: process.env.SEGMENT_WRITE_KEY || 'lK5MNZgHbxj6vQwFgqZxygA0BiDQb32n', + }, db: { database: join(homedir, 'redisinsight.db'), }, diff --git a/redisinsight/api/config/staging.ts b/redisinsight/api/config/staging.ts index bb61d57a4d..d23664a670 100644 --- a/redisinsight/api/config/staging.ts +++ b/redisinsight/api/config/staging.ts @@ -22,6 +22,9 @@ export default { server: { env: 'staging', }, + analytics: { + writeKey: process.env.SEGMENT_WRITE_KEY || 'Ba1YuGnxzsQN9zjqTSvzPc6f3AvmH1mj', + }, db: { database: join(homedir, 'redisinsight.db'), }, diff --git a/redisinsight/api/migration/1663093411715-workbench-group-mode.ts b/redisinsight/api/migration/1663093411715-workbench-group-mode.ts new file mode 100644 index 0000000000..1e26d94952 --- /dev/null +++ b/redisinsight/api/migration/1663093411715-workbench-group-mode.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class workbenchGroupMode1663093411715 implements MigrationInterface { + name = 'workbenchGroupMode1663093411715' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_5cd90dd6def1fd7c521e53fb2c"`); + await queryRunner.query(`CREATE TABLE "temporary_command_execution" ("id" varchar PRIMARY KEY NOT NULL, "databaseId" varchar NOT NULL, "command" text NOT NULL, "result" text NOT NULL, "role" varchar, "nodeOptions" varchar, "encryption" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "mode" varchar, "resultsMode" varchar, "summary" varchar, CONSTRAINT "FK_ea8adfe9aceceb79212142206b8" FOREIGN KEY ("databaseId") REFERENCES "database_instance" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_command_execution"("id", "databaseId", "command", "result", "role", "nodeOptions", "encryption", "createdAt", "mode") SELECT "id", "databaseId", "command", "result", "role", "nodeOptions", "encryption", "createdAt", "mode" FROM "command_execution"`); + await queryRunner.query(`DROP TABLE "command_execution"`); + await queryRunner.query(`ALTER TABLE "temporary_command_execution" RENAME TO "command_execution"`); + await queryRunner.query(`CREATE INDEX "IDX_5cd90dd6def1fd7c521e53fb2c" ON "command_execution" ("createdAt") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_5cd90dd6def1fd7c521e53fb2c"`); + await queryRunner.query(`ALTER TABLE "command_execution" RENAME TO "temporary_command_execution"`); + await queryRunner.query(`CREATE TABLE "command_execution" ("id" varchar PRIMARY KEY NOT NULL, "databaseId" varchar NOT NULL, "command" text NOT NULL, "result" text NOT NULL, "role" varchar, "nodeOptions" varchar, "encryption" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "mode" varchar, CONSTRAINT "FK_ea8adfe9aceceb79212142206b8" FOREIGN KEY ("databaseId") REFERENCES "database_instance" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "command_execution"("id", "databaseId", "command", "result", "role", "nodeOptions", "encryption", "createdAt", "mode") SELECT "id", "databaseId", "command", "result", "role", "nodeOptions", "encryption", "createdAt", "mode" FROM "temporary_command_execution"`); + await queryRunner.query(`DROP TABLE "temporary_command_execution"`); + await queryRunner.query(`CREATE INDEX "IDX_5cd90dd6def1fd7c521e53fb2c" ON "command_execution" ("createdAt") `); + } + +} diff --git a/redisinsight/api/migration/index.ts b/redisinsight/api/migration/index.ts index ccb7c2c1de..d3111267f3 100644 --- a/redisinsight/api/migration/index.ts +++ b/redisinsight/api/migration/index.ts @@ -16,6 +16,7 @@ import { sni1650278664000 } from "./1650278664000-sni"; import { notification1655821010349 } from './1655821010349-notification'; import { notificationCategory1659687030433 } from './1659687030433-notification-category'; import { workbenchMode1660664717573 } from './1660664717573-workbench-mode'; +import { workbenchGroupMode1663093411715 } from './1663093411715-workbench-group-mode'; export default [ initialMigration1614164490968, @@ -36,4 +37,5 @@ export default [ notification1655821010349, notificationCategory1659687030433, workbenchMode1660664717573, + workbenchGroupMode1663093411715, ]; diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json index a54b778505..43601e8642 100644 --- a/redisinsight/api/package.json +++ b/redisinsight/api/package.json @@ -38,15 +38,15 @@ "typeorm:run": "yarn typeorm migration:run" }, "dependencies": { - "@nestjs/common": "^7.6.15", - "@nestjs/core": "^7.0.0", - "@nestjs/event-emitter": "^1.0.0", - "@nestjs/platform-express": "^7.0.0", - "@nestjs/platform-socket.io": "^8.2.3", - "@nestjs/serve-static": "^2.1.3", - "@nestjs/swagger": "^4.6.1", - "@nestjs/typeorm": "^7.1.5", - "@nestjs/websockets": "^8.2.3", + "@nestjs/common": "^9.0.11", + "@nestjs/core": "^9.0.11", + "@nestjs/event-emitter": "^1.3.1", + "@nestjs/platform-express": "^9.0.11", + "@nestjs/platform-socket.io": "^9.0.11", + "@nestjs/serve-static": "^3.0.0", + "@nestjs/swagger": "^6.1.2", + "@nestjs/typeorm": "^9.0.1", + "@nestjs/websockets": "^9.0.11", "adm-zip": "^0.5.9", "analytics-node": "^4.0.1", "axios": "^0.25.0", @@ -56,31 +56,30 @@ "dotenv": "^16.0.0", "express": "^4.17.1", "fs-extra": "^10.0.0", - "ioredis": "^4.27.1", + "ioredis": "^5.2.2", "is-glob": "^4.0.1", - "keytar": "^7.7.0", + "keytar": "^7.9.0", "lodash": "^4.17.20", "nest-router": "^1.0.9", "nest-winston": "^1.4.0", "reflect-metadata": "^0.1.13", - "rxjs": "^6.6.7", + "rxjs": "^7.5.6", "socket.io": "^4.4.0", "source-map-support": "^0.5.19", - "sqlite3": "^5.0.2", + "sqlite3": "^5.0.11", "swagger-ui-express": "^4.1.4", - "typeorm": "^0.2.29", + "typeorm": "^0.3.9", "uuid": "^8.3.2", "winston": "^3.3.3", "winston-daily-rotate-file": "^4.5.0" }, "devDependencies": { "@mochajs/json-file-reporter": "^1.3.0", - "@nestjs/cli": "^7.5.4", - "@nestjs/schematics": "^7.0.0", - "@nestjs/testing": "^7.0.0", + "@nestjs/cli": "^9.1.2", + "@nestjs/schematics": "^9.0.3", + "@nestjs/testing": "^9.0.11", "@types/axios": "^0.14.0", "@types/express": "^4.17.3", - "@types/ioredis": "^4.22.3", "@types/jest": "^26.0.15", "@types/lodash": "^4.14.167", "@types/node": "14.14.10", @@ -96,7 +95,7 @@ "eslint-config-prettier": "^6.10.0", "eslint-plugin-import": "^2.20.1", "eslint-plugin-sonarjs": "^0.9.1", - "ioredis-mock": "^5.5.4", + "ioredis-mock": "^8.2.2", "jest": "^26.6.3", "jest-when": "^3.2.1", "joi": "^17.4.0", diff --git a/redisinsight/api/src/__mocks__/analytics.ts b/redisinsight/api/src/__mocks__/analytics.ts index 2c86f04b6f..c59c0dca53 100644 --- a/redisinsight/api/src/__mocks__/analytics.ts +++ b/redisinsight/api/src/__mocks__/analytics.ts @@ -19,6 +19,7 @@ export const mockCliAnalyticsService = () => ({ }); export const mockWorkbenchAnalyticsService = () => ({ + sendCommandExecutedEvents: jest.fn(), sendCommandExecutedEvent: jest.fn(), sendCommandDeletedEvent: jest.fn(), }); diff --git a/redisinsight/api/src/__mocks__/common.ts b/redisinsight/api/src/__mocks__/common.ts index 7307982630..38577cd8b0 100644 --- a/redisinsight/api/src/__mocks__/common.ts +++ b/redisinsight/api/src/__mocks__/common.ts @@ -46,6 +46,7 @@ export const mockCreateQueryBuilder = jest.fn(() => ({ export const mockRepository = jest.fn(() => ({ findOne: jest.fn(), + findOneBy: jest.fn(), find: jest.fn(), findByIds: jest.fn(), create: jest.fn(), diff --git a/redisinsight/api/src/app.module.ts b/redisinsight/api/src/app.module.ts index 3f91675dff..7b87b1bcf7 100644 --- a/redisinsight/api/src/app.module.ts +++ b/redisinsight/api/src/app.module.ts @@ -15,6 +15,7 @@ import { SlowLogModule } from 'src/modules/slow-log/slow-log.module'; import { PubSubModule } from 'src/modules/pub-sub/pub-sub.module'; import { NotificationModule } from 'src/modules/notification/notification.module'; import { BulkActionsModule } from 'src/modules/bulk-actions/bulk-actions.module'; +import { ClusterMonitorModule } from 'src/modules/cluster-monitor/cluster-monitor.module'; import { SharedModule } from './modules/shared/shared.module'; import { InstancesModule } from './modules/instances/instances.module'; import { BrowserModule } from './modules/browser/browser.module'; @@ -50,6 +51,7 @@ const PATH_CONFIG = config.get('dir_path'); SlowLogModule, NotificationModule, BulkActionsModule, + ClusterMonitorModule, EventEmitterModule.forRoot(), ...(SERVER_CONFIG.staticContent ? [ diff --git a/redisinsight/api/src/app.routes.ts b/redisinsight/api/src/app.routes.ts index 2c529122b7..6d3d027ecf 100644 --- a/redisinsight/api/src/app.routes.ts +++ b/redisinsight/api/src/app.routes.ts @@ -7,6 +7,7 @@ import { CliModule } from 'src/modules/cli/cli.module'; import { WorkbenchModule } from 'src/modules/workbench/workbench.module'; import { SlowLogModule } from 'src/modules/slow-log/slow-log.module'; import { PubSubModule } from 'src/modules/pub-sub/pub-sub.module'; +import { ClusterMonitorModule } from 'src/modules/cluster-monitor/cluster-monitor.module'; export const routes: Routes = [ { @@ -33,6 +34,10 @@ export const routes: Routes = [ path: '/:dbInstance', module: PubSubModule, }, + { + path: '/:dbInstance', + module: ClusterMonitorModule, + }, ], }, { diff --git a/redisinsight/api/src/constants/telemetry-events.ts b/redisinsight/api/src/constants/telemetry-events.ts index 774f2aa5ef..307a74ea47 100644 --- a/redisinsight/api/src/constants/telemetry-events.ts +++ b/redisinsight/api/src/constants/telemetry-events.ts @@ -25,14 +25,14 @@ export enum TelemetryEvents { SentinelMasterGroupsDiscoveryFailed = 'CONFIG_DATABASES_REDIS_SENTINEL_AUTODISCOVERY_FAILED', // Events for cli tool - ClientCreated = 'CLIENT_CREATED', - ClientCreationFailed = 'CLIENT_CREATION_FAILED', - ClientConnectionError = 'CLIENT_CONNECTION_ERROR', - ClientDeleted = 'CLIENT_DELETED', - ClientRecreated = 'CLIENT_RECREATED', - CommandExecuted = 'COMMAND_EXECUTED', - ClusterNodeCommandExecuted = 'CLUSTER_COMMAND_EXECUTED', - CommandErrorReceived = 'COMMAND_ERROR_RECEIVED', + CliClientCreated = 'CLI_CLIENT_CREATED', + CliClientCreationFailed = 'CLI_CLIENT_CREATION_FAILED', + CliClientConnectionError = 'CLI_CLIENT_CONNECTION_ERROR', + CliClientDeleted = 'CLI_CLIENT_DELETED', + CliClientRecreated = 'CLI_CLIENT_RECREATED', + CliCommandExecuted = 'CLI_COMMAND_EXECUTED', + CliClusterNodeCommandExecuted = 'CLI_CLUSTER_COMMAND_EXECUTED', + CliCommandErrorReceived = 'CLI_COMMAND_ERROR_RECEIVED', // Events for workbench tool WorkbenchCommandExecuted = 'WORKBENCH_COMMAND_EXECUTED', @@ -56,7 +56,8 @@ export enum TelemetryEvents { BulkActionsStarted = 'BULK_ACTIONS_STARTED', BulkActionsStopped = 'BULK_ACTIONS_STOPPED', } + export enum CommandType { Core = 'core', Module = 'module', - } \ No newline at end of file +} diff --git a/redisinsight/api/src/models/redis-consumer.interface.ts b/redisinsight/api/src/models/redis-consumer.interface.ts index e3b997f8b1..8939b66d28 100644 --- a/redisinsight/api/src/models/redis-consumer.interface.ts +++ b/redisinsight/api/src/models/redis-consumer.interface.ts @@ -1,6 +1,6 @@ import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service'; import { ReplyError } from 'src/models/redis-client'; -import IORedis from 'ioredis'; +import { Redis } from 'ioredis'; export interface IRedisConsumer { execCommand( @@ -17,7 +17,7 @@ export interface IRedisConsumer { ): Promise<[ReplyError | null, any]>; execPipelineFromClient( - client: IORedis.Redis, + client: Redis, toolCommands: Array< [toolCommand: any, ...args: Array] >, diff --git a/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.spec.ts b/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.spec.ts index 1bdea789b6..d8bafbb73c 100644 --- a/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.spec.ts @@ -27,9 +27,9 @@ const mockClient = new Redis(); const mockCluster = new Redis.Cluster([]); const mockClusterNode1 = new Redis(); const mockClusterNode2 = new Redis(); -mockClusterNode1.send_command = jest.fn(); +mockClusterNode1.call = jest.fn(); mockClusterNode1.sendCommand = jest.fn(); -mockClusterNode2.send_command = jest.fn(); +mockClusterNode2.call = jest.fn(); mockClusterNode2.sendCommand = jest.fn(); mockClusterNode1.options = { host: '127.0.0.1', port: 7001 }; mockClusterNode2.options = { host: '127.0.0.1', port: 7002 }; @@ -66,7 +66,7 @@ describe('BrowserToolClusterService', () => { service, 'execPipelineFromClient', ); - mockClient.send_command = jest.fn(); + mockClient.call = jest.fn(); }); describe('execCommand', () => { @@ -80,7 +80,7 @@ describe('BrowserToolClusterService', () => { [keyName], ); - expect(mockClient.send_command).toHaveBeenCalledWith('memory', [ + expect(mockClient.call).toHaveBeenCalledWith('memory', [ 'usage', keyName, ]); @@ -98,7 +98,7 @@ describe('BrowserToolClusterService', () => { [keyName], ), ).rejects.toThrow(InternalServerErrorException); - expect(mockClient.send_command).not.toHaveBeenCalled(); + expect(mockClient.call).not.toHaveBeenCalled(); }); }); @@ -137,8 +137,8 @@ describe('BrowserToolClusterService', () => { it('should execute command for all nodes', async () => { getRedisClient.mockResolvedValue(mockCluster); - mockClusterNode1.send_command.mockResolvedValue(70); - mockClusterNode2.send_command.mockResolvedValue(10); + mockClusterNode1.call.mockResolvedValue(70); + mockClusterNode2.call.mockResolvedValue(10); mockCluster.nodes.mockReturnValue([mockClusterNode1, mockClusterNode2]); const result = await service.execCommandFromNodes( @@ -152,11 +152,11 @@ describe('BrowserToolClusterService', () => { { result: 70, ...mockClusterNode1.options }, { result: 10, ...mockClusterNode2.options }, ]); - expect(mockClusterNode1.send_command).toHaveBeenCalledWith('memory', [ + expect(mockClusterNode1.call).toHaveBeenCalledWith('memory', [ 'usage', keyName, ]); - expect(mockClusterNode2.send_command).toHaveBeenCalledWith('memory', [ + expect(mockClusterNode2.call).toHaveBeenCalledWith('memory', [ 'usage', keyName, ]); diff --git a/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.ts b/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.ts index 2dff29f6ee..2f97b76cc2 100644 --- a/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.ts +++ b/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.ts @@ -40,7 +40,7 @@ export class BrowserToolClusterService extends RedisConsumerAbstractService { this.logger.log(`Execute command '${toolCommand}', connectionName: ${getConnectionName(client)}`); const [command, ...commandArgs] = toolCommand.split(' '); // TODO: use sendCommand method - return client.send_command(command, [...commandArgs, ...args]); + return client.call(command, [...commandArgs, ...args]); } async execPipeline( @@ -68,10 +68,10 @@ export class BrowserToolClusterService extends RedisConsumerAbstractService { this.logger.log(`Execute command '${toolCommand}' from nodes, connectionName: ${getConnectionName(client)}`); return await Promise.all( nodes.map( - async (node: IORedis.Redis): Promise => { + async (node: Redis): Promise => { const { host, port } = node.options; const [command, ...commandArgs] = toolCommand.split(' '); - const result = await node.send_command(command, [ + const result = await node.call(command, [ ...commandArgs, ...args, ]); @@ -90,7 +90,7 @@ export class BrowserToolClusterService extends RedisConsumerAbstractService { toolCommand: BrowserToolCommands, args: Array, exactNode: EndpointDto, - replyEncoding: string = 'utf8', + replyEncoding: BufferEncoding = 'utf8', ): Promise { const client = await this.getRedisClient(clientOptions); this.logger.log(`Execute command '${toolCommand}' from node, connectionName: ${getConnectionName(client)}`); diff --git a/redisinsight/api/src/modules/browser/services/browser-tool/browser-tool.service.ts b/redisinsight/api/src/modules/browser/services/browser-tool/browser-tool.service.ts index ddb42d06db..1cfed898d8 100644 --- a/redisinsight/api/src/modules/browser/services/browser-tool/browser-tool.service.ts +++ b/redisinsight/api/src/modules/browser/services/browser-tool/browser-tool.service.ts @@ -26,7 +26,7 @@ export class BrowserToolService extends RedisConsumerAbstractService { clientOptions: IFindRedisClientInstanceByOptions, toolCommand: BrowserToolCommands, args: Array, - replyEncoding: string = null, + replyEncoding: BufferEncoding = null, ): Promise { const client = await this.getRedisClient(clientOptions); this.logger.log(`Execute command '${toolCommand}', connectionName: ${getConnectionName(client)}`); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.interface.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.interface.ts index 9cbd36dc2d..840c3e1930 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.interface.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.interface.ts @@ -1,6 +1,6 @@ import { RedisDataType } from 'src/modules/browser/dto'; import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service'; -import IORedis from 'ioredis'; +import { Redis } from 'ioredis'; interface IGetKeysArgs { cursor: string; @@ -14,7 +14,7 @@ export interface IGetNodeKeysResult { scanned: number; cursor: number; keys: any[]; - node?: IORedis.Redis, + node?: Redis, host?: string; port?: number; } diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.spec.ts index 6e6c68e986..9d5b4fa2cb 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.spec.ts @@ -14,13 +14,13 @@ import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-t import { StandaloneStrategy } from 'src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy'; import { AbstractStrategy } from 'src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy'; import { ISettingsProvider } from 'src/modules/core/models/settings-provider.interface'; -import * as Redis from 'ioredis'; +import IORedis from 'ioredis'; const mockClientOptions: IFindRedisClientInstanceByOptions = { instanceId: mockStandaloneDatabaseEntity.id, }; -const nodeClient = Object.create(Redis.prototype); +const nodeClient = Object.create(IORedis.prototype); const mockKeyInfo: GetKeyInfoResponse = { name: 'testString', diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.ts index 1f2f3f97cd..d6da7a0b1c 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.ts @@ -1,7 +1,7 @@ import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { IRedisConsumer, ReplyError } from 'src/models'; -import IORedis from 'ioredis'; +import IORedis, { Redis, Cluster } from 'ioredis'; import { RedisString } from 'src/common/constants'; import { IScannerStrategy } from '../scanner.interface'; @@ -15,12 +15,12 @@ export abstract class AbstractStrategy implements IScannerStrategy { abstract getKeys(clientOptions, args); public async getKeyInfo( - client: IORedis.Redis | IORedis.Cluster, + client: Redis | Cluster, key: RedisString, knownType?: RedisDataType, ) { const options = { - replyEncoding: 'utf8', + replyEncoding: 'utf8' as BufferEncoding, }; // @ts-ignore @@ -54,7 +54,7 @@ export abstract class AbstractStrategy implements IScannerStrategy { } public async getKeysInfo( - client: IORedis.Redis, + client: Redis, keys: RedisString[], type?: RedisDataType, ): Promise { @@ -74,7 +74,7 @@ export abstract class AbstractStrategy implements IScannerStrategy { } protected async getKeysTtl( - client: IORedis.Redis, + client: Redis, keys: RedisString[], ): Promise { const [ @@ -92,7 +92,7 @@ export abstract class AbstractStrategy implements IScannerStrategy { } protected async getKeysType( - client: IORedis.Redis, + client: Redis, keys: RedisString[], ): Promise { const [ @@ -110,7 +110,7 @@ export abstract class AbstractStrategy implements IScannerStrategy { } protected async getKeysSize( - client: IORedis.Redis, + client: Redis, keys: RedisString[], ): Promise { const [ diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.spec.ts index d25b0f12c4..f350fa17e8 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.spec.ts @@ -18,7 +18,7 @@ import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-t import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service'; import { IGetNodeKeysResult } from 'src/modules/browser/services/keys-business/scanner/scanner.interface'; import { ISettingsProvider } from 'src/modules/core/models/settings-provider.interface'; -import * as Redis from 'ioredis'; +import IORedis from 'ioredis'; import { ClusterStrategy } from './cluster.strategy'; const REDIS_SCAN_CONFIG = config.get('redis_scan'); @@ -26,12 +26,12 @@ const mockClientOptions: IFindRedisClientInstanceByOptions = { instanceId: mockStandaloneDatabaseEntity.id, }; -const nodeClient = Object.create(Redis.prototype); +const nodeClient = Object.create(IORedis.prototype); nodeClient.sendCommand = jest.fn(); const mockClusterNode1 = nodeClient; const mockClusterNode2 = nodeClient; -const clusterClient = Object.create(Redis.Cluster.prototype); +const clusterClient = Object.create(IORedis.Cluster.prototype); clusterClient.sendCommand = jest.fn(); mockClusterNode1.options = { ...nodeClient.options, host: 'localhost', port: 5000 }; mockClusterNode2.options = { ...nodeClient.options, host: 'localhost', port: 5001 }; @@ -788,11 +788,10 @@ describe('Cluster Scanner Strategy', () => { expect.anything(), null, ) - .mockResolvedValue({ result: [0, [Buffer.from(getKeyInfoResponse.name)]] }); + .mockResolvedValue({ result: [0, [Buffer.from(getKeyInfoResponse.name)]] }); strategy.getKeysInfo = jest .fn() .mockResolvedValue([getKeyInfoResponse]); - try { await strategy.getKeys(mockClientOptions, args); fail(); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts index 4b8134a1c1..57397a6526 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts @@ -15,7 +15,7 @@ import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-t import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service'; import { IGetNodeKeysResult } from 'src/modules/browser/services/keys-business/scanner/scanner.interface'; import { ISettingsProvider } from 'src/modules/core/models/settings-provider.interface'; -import * as Redis from 'ioredis'; +import IORedis from 'ioredis'; import { StandaloneStrategy } from './standalone.strategy'; const REDIS_SCAN_CONFIG = config.get('redis_scan'); @@ -23,7 +23,7 @@ const mockClientOptions: IFindRedisClientInstanceByOptions = { instanceId: mockStandaloneDatabaseEntity.id, }; -const nodeClient = Object.create(Redis.prototype); +const nodeClient = Object.create(IORedis.prototype); nodeClient.sendCommand = jest.fn(); const getKeyInfoResponse = { diff --git a/redisinsight/api/src/modules/bulk-actions/bulk-actions.service.spec.ts b/redisinsight/api/src/modules/bulk-actions/bulk-actions.service.spec.ts index 509c07584d..66263ffe20 100644 --- a/redisinsight/api/src/modules/bulk-actions/bulk-actions.service.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/bulk-actions.service.spec.ts @@ -1,4 +1,4 @@ -import * as Redis from 'ioredis'; +import IORedis from 'ioredis'; import * as MockedSocket from 'socket.io-mock'; import { Test, TestingModule } from '@nestjs/testing'; import { @@ -21,7 +21,7 @@ export const mockSocket2 = new MockedSocket(); mockSocket2.id = '2'; mockSocket2['emit'] = jest.fn(); -const nodeClient = Object.create(Redis.prototype); +const nodeClient = Object.create(IORedis.prototype); nodeClient.sendCommand = jest.fn(); const mockBulkActionFilter = Object.assign(new BulkActionFilter(), { diff --git a/redisinsight/api/src/modules/bulk-actions/models/bulk-action.spec.ts b/redisinsight/api/src/modules/bulk-actions/models/bulk-action.spec.ts index f2a562fb0d..2d71a8067c 100644 --- a/redisinsight/api/src/modules/bulk-actions/models/bulk-action.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/models/bulk-action.spec.ts @@ -1,4 +1,4 @@ -import * as Redis from 'ioredis'; +import IORedis from 'ioredis'; import { omit } from 'lodash'; import { mockSocket, @@ -14,14 +14,14 @@ import { BulkActionSummary } from 'src/modules/bulk-actions/models/bulk-action-s const mockExec = jest.fn(); -const nodeClient = Object.create(Redis.prototype); +const nodeClient = Object.create(IORedis.prototype); nodeClient.sendCommand = jest.fn(); nodeClient.pipeline = jest.fn(() => ({ exec: mockExec, })); nodeClient.options = { db: 0 }; -const clusterClient = Object.create(Redis.Cluster.prototype); +const clusterClient = Object.create(IORedis.Cluster.prototype); clusterClient.nodes = jest.fn(); clusterClient.sendCommand = jest.fn(); diff --git a/redisinsight/api/src/modules/bulk-actions/models/bulk-action.ts b/redisinsight/api/src/modules/bulk-actions/models/bulk-action.ts index 2cb4619a14..0a5e3aeade 100644 --- a/redisinsight/api/src/modules/bulk-actions/models/bulk-action.ts +++ b/redisinsight/api/src/modules/bulk-actions/models/bulk-action.ts @@ -1,4 +1,4 @@ -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; import { debounce } from 'lodash'; import { BulkActionStatus, BulkActionType } from 'src/modules/bulk-actions/contants'; import { BulkActionFilter } from 'src/modules/bulk-actions/models/bulk-action-filter'; diff --git a/redisinsight/api/src/modules/bulk-actions/models/runners/abstract.bulk-action.runner.spec.ts b/redisinsight/api/src/modules/bulk-actions/models/runners/abstract.bulk-action.runner.spec.ts index 2724bb6800..cf09b3e7f1 100644 --- a/redisinsight/api/src/modules/bulk-actions/models/runners/abstract.bulk-action.runner.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/models/runners/abstract.bulk-action.runner.spec.ts @@ -1,4 +1,4 @@ -import * as Redis from 'ioredis'; +import IORedis from 'ioredis'; import { mockSocket, } from 'src/__mocks__'; @@ -12,7 +12,7 @@ import { BulkActionProgress } from 'src/modules/bulk-actions/models/bulk-action- import { BulkActionSummary } from 'src/modules/bulk-actions/models/bulk-action-summary'; const mockExec = jest.fn(); -const nodeClient = Object.create(Redis.prototype); +const nodeClient = Object.create(IORedis.prototype); nodeClient.sendCommand = jest.fn(); nodeClient.pipeline = jest.fn(() => ({ exec: mockExec, diff --git a/redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.spec.ts b/redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.spec.ts index 350d7170a3..0b384c3fd9 100644 --- a/redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.spec.ts @@ -1,4 +1,4 @@ -import * as Redis from 'ioredis'; +import IORedis from 'ioredis'; import { mockSocket, } from 'src/__mocks__'; @@ -10,7 +10,7 @@ import { BulkActionStatus, BulkActionType } from 'src/modules/bulk-actions/conta import { BulkActionFilter } from 'src/modules/bulk-actions/models/bulk-action-filter'; const mockExec = jest.fn(); -const nodeClient = Object.create(Redis.prototype); +const nodeClient = Object.create(IORedis.prototype); nodeClient.sendCommand = jest.fn(); nodeClient.pipeline = jest.fn(() => ({ exec: mockExec, diff --git a/redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.ts b/redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.ts index 843bc9c2f9..12a8df2392 100644 --- a/redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.ts +++ b/redisinsight/api/src/modules/bulk-actions/models/runners/simple/abstract.bulk-action.simple.runner.ts @@ -1,4 +1,4 @@ -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; import { get } from 'lodash'; import { convertBulkStringsToObject, convertRedisInfoReplyToObject } from 'src/utils'; import { BulkActionStatus } from 'src/modules/bulk-actions/contants'; @@ -59,6 +59,8 @@ export abstract class AbstractBulkActionSimpleRunner extends AbstractBulkActionR if (keys.length) { const commands = this.prepareCommands(keys) as string[][]; const res = await this.node.pipeline(commands).exec(); + // @ts-expect-error + // https://github.com/luin/ioredis/issues/1572 this.processIterationResults(keys, res); } diff --git a/redisinsight/api/src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner.spec.ts b/redisinsight/api/src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner.spec.ts index aac0627b1a..4e74c98748 100644 --- a/redisinsight/api/src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner.spec.ts @@ -1,4 +1,4 @@ -import * as Redis from 'ioredis'; +import IORedis from 'ioredis'; import { mockSocket, } from 'src/__mocks__'; @@ -9,7 +9,7 @@ import { BulkAction } from 'src/modules/bulk-actions/models/bulk-action'; import { BulkActionType } from 'src/modules/bulk-actions/contants'; import { RedisDataType } from 'src/modules/browser/dto'; -const nodeClient = Object.create(Redis.prototype); +const nodeClient = Object.create(IORedis.prototype); const mockBulkActionFilter = { count: 10_000, diff --git a/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.spec.ts b/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.spec.ts index e9871e3852..e21ad9cedc 100644 --- a/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.spec.ts +++ b/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.spec.ts @@ -1,4 +1,4 @@ -import * as Redis from 'ioredis'; +import IORedis from 'ioredis'; import * as MockedSocket from 'socket.io-mock'; import { Test, TestingModule } from '@nestjs/testing'; import { @@ -22,7 +22,7 @@ export const mockSocket2 = new MockedSocket(); mockSocket2.id = '2'; mockSocket2['emit'] = jest.fn(); -const nodeClient = Object.create(Redis.prototype); +const nodeClient = Object.create(IORedis.prototype); nodeClient.sendCommand = jest.fn(); nodeClient.options = { db: 0 }; diff --git a/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts b/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts index d17fe42ac2..2b833316af 100644 --- a/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts +++ b/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts @@ -1,29 +1,42 @@ import { Test, TestingModule } from '@nestjs/testing'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { InternalServerErrorException } from '@nestjs/common'; -import { mockRedisWrongTypeError, mockStandaloneDatabaseEntity } from 'src/__mocks__'; -import { TelemetryEvents } from 'src/constants'; -import { AppTool, ReplyError } from 'src/models'; +import { mockRedisWrongTypeError, mockStandaloneDatabaseEntity, MockType } from 'src/__mocks__'; +import { CommandType, TelemetryEvents } from 'src/constants'; +import { ReplyError } from 'src/models'; import { CommandParsingError } from 'src/modules/cli/constants/errors'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { ICliExecResultFromNode } from 'src/modules/shared/services/base/redis-tool.service'; +import { CommandsService } from 'src/modules/commands/commands.service'; import { CliAnalyticsService } from './cli-analytics.service'; +const mockCommandsService = { + getCommandsGroups: jest.fn(), +}; + const redisReplyError: ReplyError = { ...mockRedisWrongTypeError, command: { name: 'sadd' }, }; -const instanceId = mockStandaloneDatabaseEntity.id; +const databaseId = mockStandaloneDatabaseEntity.id; const httpException = new InternalServerErrorException(); +const mockCustomData = { data: 'Some data' }; +const mockSetCommandName = 'set'; +const mockAdditionalData = { command: mockSetCommandName }; describe('CliAnalyticsService', () => { let service: CliAnalyticsService; let sendEventMethod: jest.SpyInstance; let sendFailedEventMethod: jest.SpyInstance; + let commandsService: MockType; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ + { + provide: CommandsService, + useFactory: () => mockCommandsService, + }, EventEmitter2, CliAnalyticsService, ], @@ -38,27 +51,59 @@ describe('CliAnalyticsService', () => { service, 'sendFailedEvent', ); + + commandsService = module.get(CommandsService); + commandsService.getCommandsGroups.mockResolvedValue({ + main: { + SET: { + summary: 'Set the string value of a key', + since: '1.0.0', + group: 'string', + complexity: 'O(1)', + acl_categories: [ + '@write', + '@string', + '@slow', + ], + }, + }, + redisbloom: { + 'BF.RESERVE': { + summary: 'Creates a new Bloom Filter', + complexity: 'O(1)', + since: '1.0.0', + group: 'bf', + }, + }, + custommodule: { + 'CUSTOM.COMMAND': { + summary: 'Creates a new Bloom Filter', + complexity: 'O(1)', + since: '1.0.0', + }, + }, + }); }); describe('sendCliClientCreatedEvent', () => { it('should emit CliClientCreated event', () => { - service.sendClientCreatedEvent(instanceId, AppTool.CLI, { data: 'Some data' }); + service.sendClientCreatedEvent(databaseId, mockCustomData); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.ClientCreated}`, + TelemetryEvents.CliClientCreated, { - databaseId: instanceId, - data: 'Some data', + databaseId, + ...mockCustomData, }, ); }); it('should emit CliClientCreated event without additional data', () => { - service.sendClientCreatedEvent(instanceId, AppTool.CLI); + service.sendClientCreatedEvent(databaseId); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.ClientCreated}`, + TelemetryEvents.CliClientCreated, { - databaseId: instanceId, + databaseId, }, ); }); @@ -66,25 +111,25 @@ describe('CliAnalyticsService', () => { describe('sendCliClientCreationFailedEvent', () => { it('should emit CliClientCreationFailed event', () => { - service.sendClientCreationFailedEvent(instanceId, AppTool.CLI, httpException, { data: 'Some data' }); + service.sendClientCreationFailedEvent(databaseId, httpException, mockCustomData); expect(sendFailedEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.ClientCreationFailed}`, + TelemetryEvents.CliClientCreationFailed, httpException, { - databaseId: instanceId, - data: 'Some data', + databaseId, + ...mockCustomData, }, ); }); it('should emit CliClientCreationFailed event without additional data', () => { - service.sendClientCreationFailedEvent(instanceId, AppTool.CLI, httpException); + service.sendClientCreationFailedEvent(databaseId, httpException); expect(sendFailedEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.ClientCreationFailed}`, + TelemetryEvents.CliClientCreationFailed, httpException, { - databaseId: instanceId, + databaseId, }, ); }); @@ -92,23 +137,23 @@ describe('CliAnalyticsService', () => { describe('sendCliClientRecreatedEvent', () => { it('should emit CliClientRecreated event', () => { - service.sendClientRecreatedEvent(instanceId, AppTool.CLI, { data: 'Some data' }); + service.sendClientRecreatedEvent(databaseId, mockCustomData); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.ClientRecreated}`, + TelemetryEvents.CliClientRecreated, { - databaseId: instanceId, - data: 'Some data', + databaseId, + ...mockCustomData, }, ); }); it('should emit CliClientRecreated event without additional data', () => { - service.sendClientRecreatedEvent(instanceId, AppTool.CLI); + service.sendClientRecreatedEvent(databaseId); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.ClientRecreated}`, + TelemetryEvents.CliClientRecreated, { - databaseId: instanceId, + databaseId, }, ); }); @@ -116,164 +161,144 @@ describe('CliAnalyticsService', () => { describe('sendCliClientDeletedEvent', () => { it('should emit CliClientDeleted event', () => { - service.sendClientDeletedEvent(1, instanceId, AppTool.CLI, { data: 'Some data' }); + service.sendClientDeletedEvent(1, databaseId, mockCustomData); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.ClientDeleted}`, + TelemetryEvents.CliClientDeleted, { - databaseId: instanceId, - data: 'Some data', + databaseId, + ...mockCustomData, }, ); }); it('should emit CliClientDeleted event without additional data', () => { - service.sendClientDeletedEvent(1, instanceId, AppTool.CLI); + service.sendClientDeletedEvent(1, databaseId); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.ClientDeleted}`, + TelemetryEvents.CliClientDeleted, { - databaseId: instanceId, + databaseId, }, ); }); it('should not emit event', () => { - service.sendClientDeletedEvent(0, instanceId, AppTool.CLI); + service.sendClientDeletedEvent(0, databaseId); expect(sendEventMethod).not.toHaveBeenCalled(); }); it('should not emit event on invalid input values', () => { const input: any = {}; - service.sendClientDeletedEvent(input, instanceId, AppTool.CLI); + service.sendClientDeletedEvent(input, databaseId); - expect(() => service.sendClientDeletedEvent(input, instanceId, AppTool.CLI)).not.toThrow(); + expect(() => service.sendClientDeletedEvent(input, databaseId)).not.toThrow(); expect(sendEventMethod).not.toHaveBeenCalled(); }); }); describe('sendCliCommandExecutedEvent', () => { - it('should emit CliCommandExecuted event', () => { - service.sendCommandExecutedEvent(instanceId, AppTool.CLI, { command: 'info' }); - - expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.CommandExecuted}`, - { - databaseId: instanceId, - command: 'info', - }, - ); - }); - it('should emit CliCommandExecuted event without additional data', () => { - service.sendCommandExecutedEvent(instanceId, AppTool.CLI); + it('should emit CliCommandExecuted event', async () => { + await service.sendCommandExecutedEvent(databaseId, mockAdditionalData); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.CommandExecuted}`, + TelemetryEvents.CliCommandExecuted, { - databaseId: instanceId, + databaseId, + command: mockAdditionalData.command, + commandType: CommandType.Core, + moduleName: 'n/a', + capability: 'string', }, ); }); - it('should emit CliCommandExecuted for undefined namespace', () => { - service.sendCommandExecutedEvent(instanceId, undefined, { command: 'info' }); + it('should emit CliCommandExecuted event without additional data', async () => { + await service.sendCommandExecutedEvent(databaseId); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.CommandExecuted}`, + TelemetryEvents.CliCommandExecuted, { - databaseId: instanceId, - command: 'info', - }, - ); - }); - it('should emit WorkbenchCommandExecuted event', () => { - service.sendCommandExecutedEvent(instanceId, 'workbench', { command: 'info' }); - - expect(sendEventMethod).toHaveBeenCalledWith( - `WORKBENCH_${TelemetryEvents.CommandExecuted}`, - { - databaseId: instanceId, - command: 'info', - }, - ); - }); - it('should emit WorkbenchCommandExecuted event without additional data', () => { - service.sendCommandExecutedEvent(instanceId, 'workbench'); - - expect(sendEventMethod).toHaveBeenCalledWith( - `WORKBENCH_${TelemetryEvents.CommandExecuted}`, - { - databaseId: instanceId, + databaseId, }, ); }); }); describe('sendCliCommandErrorEvent', () => { - it('should emit CliCommandError event', () => { - service.sendCommandErrorEvent(instanceId, AppTool.CLI, redisReplyError, { data: 'Some data' }); + it('should emit CliCommandError event', async () => { + await service.sendCommandErrorEvent(databaseId, redisReplyError, mockAdditionalData); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.CommandErrorReceived}`, + TelemetryEvents.CliCommandErrorReceived, { - databaseId: instanceId, + databaseId, error: ReplyError.name, - command: 'sadd', - data: 'Some data', + command: mockAdditionalData.command, + commandType: CommandType.Core, + moduleName: 'n/a', + capability: 'string', }, ); }); - it('should emit CliCommandError event without additional data', () => { - service.sendCommandErrorEvent(instanceId, AppTool.CLI, redisReplyError); + it('should emit CliCommandError event without additional data', async () => { + await service.sendCommandErrorEvent(databaseId, redisReplyError); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.CommandErrorReceived}`, + TelemetryEvents.CliCommandErrorReceived, { - databaseId: instanceId, + databaseId, error: ReplyError.name, command: 'sadd', }, ); }); - it('should emit event for custom error', () => { + it('should emit event for custom error', async () => { const error: any = CommandParsingError; - service.sendCommandErrorEvent(instanceId, AppTool.CLI, error); + await service.sendCommandErrorEvent(databaseId, error, mockAdditionalData); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.CommandErrorReceived}`, + TelemetryEvents.CliCommandErrorReceived, { - databaseId: instanceId, + databaseId, error: CommandParsingError.name, + command: mockAdditionalData.command, + commandType: CommandType.Core, + moduleName: 'n/a', + capability: 'string', }, ); }); }); describe('sendCliClientCreationFailedEvent', () => { - it('should emit CliConnectionError event', () => { - service.sendConnectionErrorEvent(instanceId, AppTool.CLI, httpException, { data: 'Some data' }); + it('should emit CliConnectionError event', async () => { + await service.sendConnectionErrorEvent(databaseId, httpException, mockAdditionalData); expect(sendFailedEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.ClientConnectionError}`, + TelemetryEvents.CliClientConnectionError, httpException, { - databaseId: instanceId, - data: 'Some data', + databaseId, + command: mockAdditionalData.command, + commandType: CommandType.Core, + moduleName: 'n/a', + capability: 'string', }, ); }); - it('should emit CliConnectionError event without additional data', () => { - service.sendConnectionErrorEvent(instanceId, AppTool.CLI, httpException); + it('should emit CliConnectionError event without additional data', async () => { + await service.sendConnectionErrorEvent(databaseId, httpException); expect(sendFailedEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.ClientConnectionError}`, + TelemetryEvents.CliClientConnectionError, httpException, { - databaseId: instanceId, + databaseId, }, ); }); }); describe('sendCliClusterCommandExecutedEvent', () => { - it('should emit success event', () => { + it('should emit success event', async () => { const nodExecResult: ICliExecResultFromNode = { response: '(integer) 5', host: '127.0.0.1', @@ -281,17 +306,20 @@ describe('CliAnalyticsService', () => { status: CommandExecutionStatus.Success, }; - service.sendClusterCommandExecutedEvent(instanceId, AppTool.CLI, nodExecResult, { command: 'sadd' }); + await service.sendClusterCommandExecutedEvent(databaseId, nodExecResult, mockAdditionalData); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.ClusterNodeCommandExecuted}`, + TelemetryEvents.CliClusterNodeCommandExecuted, { - databaseId: instanceId, - command: 'sadd', + databaseId, + command: mockAdditionalData.command, + commandType: CommandType.Core, + moduleName: 'n/a', + capability: 'string', }, ); }); - it('should emit event failed event for [RedisReply] error', () => { + it('should emit event failed event for [RedisReply] error', async () => { const nodExecResult: ICliExecResultFromNode = { response: redisReplyError.message, host: '127.0.0.1', @@ -300,18 +328,18 @@ describe('CliAnalyticsService', () => { status: CommandExecutionStatus.Fail, }; - service.sendClusterCommandExecutedEvent(instanceId, AppTool.CLI, nodExecResult); + await service.sendClusterCommandExecutedEvent(databaseId, nodExecResult); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.CommandErrorReceived}`, + TelemetryEvents.CliCommandErrorReceived, { - databaseId: instanceId, + databaseId, error: redisReplyError.name, command: 'sadd', }, ); }); - it('should emit event failed for custom error', () => { + it('should emit event failed for custom error', async () => { const nodExecResult: ICliExecResultFromNode = { response: redisReplyError.message, host: '127.0.0.1', @@ -320,24 +348,24 @@ describe('CliAnalyticsService', () => { status: CommandExecutionStatus.Fail, }; - service.sendClusterCommandExecutedEvent(instanceId, AppTool.CLI, nodExecResult); + await service.sendClusterCommandExecutedEvent(databaseId, nodExecResult); expect(sendEventMethod).toHaveBeenCalledWith( - `CLI_${TelemetryEvents.CommandErrorReceived}`, + TelemetryEvents.CliCommandErrorReceived, { - databaseId: instanceId, + databaseId, error: CommandParsingError.name, }, ); }); - it('should not emit event event', () => { + it('should not emit event event', async () => { const nodExecResult: any = { response: redisReplyError.message, host: '127.0.0.1', port: 7002, status: 'undefined status', }; - service.sendClusterCommandExecutedEvent(instanceId, AppTool.CLI, nodExecResult); + await service.sendClusterCommandExecutedEvent(databaseId, nodExecResult); expect(sendEventMethod).not.toHaveBeenCalled(); }); diff --git a/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts b/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts index 8382d02725..cc29ef1fd5 100644 --- a/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts +++ b/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts @@ -1,56 +1,57 @@ import { HttpException, Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { TelemetryEvents } from 'src/constants'; -import { TelemetryBaseService } from 'src/modules/shared/services/base/telemetry.base.service'; -import { AppTool, ReplyError } from 'src/models'; +import { ReplyError } from 'src/models'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { ICliExecResultFromNode } from 'src/modules/shared/services/base/redis-tool.service'; +import { CommandsService } from 'src/modules/commands/commands.service'; +import { CommandTelemetryBaseService } from 'src/modules/shared/services/base/command.telemetry.base.service'; @Injectable() -export class CliAnalyticsService extends TelemetryBaseService { - constructor(protected eventEmitter: EventEmitter2) { - super(eventEmitter); +export class CliAnalyticsService extends CommandTelemetryBaseService { + constructor( + protected eventEmitter: EventEmitter2, + protected readonly commandsService: CommandsService, + ) { + super(eventEmitter, commandsService); } sendClientCreatedEvent( - instanceId: string, - namespace: string, + databaseId: string, additionalData: object = {}, ): void { this.sendEvent( - this.getNamespaceEvent(TelemetryEvents.ClientCreated, namespace), + TelemetryEvents.CliClientCreated, { - databaseId: instanceId, + databaseId, ...additionalData, }, ); } sendClientCreationFailedEvent( - instanceId: string, - namespace: string, + databaseId: string, exception: HttpException, additionalData: object = {}, ): void { this.sendFailedEvent( - this.getNamespaceEvent(TelemetryEvents.ClientCreationFailed, namespace), + TelemetryEvents.CliClientCreationFailed, exception, { - databaseId: instanceId, + databaseId, ...additionalData, }, ); } sendClientRecreatedEvent( - instanceId: string, - namespace: string, + databaseId: string, additionalData: object = {}, ): void { this.sendEvent( - this.getNamespaceEvent(TelemetryEvents.ClientRecreated, namespace), + TelemetryEvents.CliClientRecreated, { - databaseId: instanceId, + databaseId, ...additionalData, }, ); @@ -58,16 +59,15 @@ export class CliAnalyticsService extends TelemetryBaseService { sendClientDeletedEvent( affected: number, - instanceId: string, - namespace: string, + databaseId: string, additionalData: object = {}, ): void { try { if (affected > 0) { this.sendEvent( - this.getNamespaceEvent(TelemetryEvents.ClientDeleted, namespace), + TelemetryEvents.CliClientDeleted, { - databaseId: instanceId, + databaseId, ...additionalData, }, ); @@ -77,33 +77,37 @@ export class CliAnalyticsService extends TelemetryBaseService { } } - sendCommandExecutedEvent( - instanceId: string, - namespace: string, + public async sendCommandExecutedEvent( + databaseId: string, additionalData: object = {}, - ): void { - this.sendEvent( - this.getNamespaceEvent(TelemetryEvents.CommandExecuted, namespace), - { - databaseId: instanceId, - ...additionalData, - }, - ); + ): Promise { + try { + this.sendEvent( + TelemetryEvents.CliCommandExecuted, + { + databaseId, + ...(await this.getCommandAdditionalInfo(additionalData['command'])), + ...additionalData, + }, + ); + } catch (e) { + // ignore error + } } - sendCommandErrorEvent( - instanceId: string, - namespace: string, + public async sendCommandErrorEvent( + databaseId: string, error: ReplyError, additionalData: object = {}, - ): void { + ): Promise { try { this.sendEvent( - this.getNamespaceEvent(TelemetryEvents.CommandErrorReceived, namespace), + TelemetryEvents.CliCommandErrorReceived, { - databaseId: instanceId, + databaseId, error: error?.name, command: error?.command?.name, + ...(await this.getCommandAdditionalInfo(additionalData['command'])), ...additionalData, }, ); @@ -112,30 +116,32 @@ export class CliAnalyticsService extends TelemetryBaseService { } } - sendClusterCommandExecutedEvent( - instanceId: string, - namespace: string, + public async sendClusterCommandExecutedEvent( + databaseId: string, result: ICliExecResultFromNode, additionalData: object = {}, - ): void { + ): Promise { const { status, error } = result; try { if (status === CommandExecutionStatus.Success) { this.sendEvent( - this.getNamespaceEvent(TelemetryEvents.ClusterNodeCommandExecuted, namespace), + TelemetryEvents.CliClusterNodeCommandExecuted, { - databaseId: instanceId, + databaseId, + ...(await this.getCommandAdditionalInfo(additionalData['command'])), ...additionalData, }, ); } if (status === CommandExecutionStatus.Fail) { this.sendEvent( - this.getNamespaceEvent(TelemetryEvents.CommandErrorReceived, namespace), + TelemetryEvents.CliCommandErrorReceived, { - databaseId: instanceId, + databaseId, error: error.name, command: error?.command?.name, + ...(await this.getCommandAdditionalInfo(additionalData['command'])), + ...additionalData, }, ); } @@ -144,23 +150,19 @@ export class CliAnalyticsService extends TelemetryBaseService { } } - sendConnectionErrorEvent( - instanceId: string, - namespace: string, + public async sendConnectionErrorEvent( + databaseId: string, exception: HttpException, additionalData: object = {}, - ): void { + ): Promise { this.sendFailedEvent( - this.getNamespaceEvent(TelemetryEvents.ClientConnectionError, namespace), + TelemetryEvents.CliClientConnectionError, exception, { - databaseId: instanceId, + databaseId, + ...(await this.getCommandAdditionalInfo(additionalData['command'])), ...additionalData, }, ); } - - private getNamespaceEvent(event: TelemetryEvents, namespace: string = AppTool.CLI): string { - return namespace.toLowerCase() === 'workbench' ? `WORKBENCH_${event}` : `CLI_${event}`; - } } diff --git a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts index 3177002409..4ede6f07a8 100644 --- a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts +++ b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts @@ -9,7 +9,7 @@ import { mockRedisWrongTypeError, mockStandaloneDatabaseEntity, mockCliAnalyticsService, - mockRedisMovedError, + mockRedisMovedError, MockType, } from 'src/__mocks__'; import { ClusterNodeRole, @@ -23,7 +23,12 @@ import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/red import { ReplyError } from 'src/models'; import { CliToolUnsupportedCommands } from 'src/modules/cli/utils/getUnsupportedCommands'; import { EndpointDto } from 'src/modules/instances/dto/database-instance.dto'; -import { ClusterNodeNotFoundError, WrongDatabaseTypeError } from 'src/modules/cli/constants/errors'; +import { + ClusterNodeNotFoundError, + CommandNotSupportedError, + CommandParsingError, + WrongDatabaseTypeError, +} from 'src/modules/cli/constants/errors'; import { CliAnalyticsService } from 'src/modules/cli/services/cli-analytics/cli-analytics.service'; import { KeytarUnavailableException } from 'src/modules/core/encryption/exceptions'; import { RedisToolService } from 'src/modules/shared/services/base/redis-tool.service'; @@ -67,7 +72,7 @@ describe('CliBusinessService', () => { let cliTool; let textFormatter: IOutputFormatterStrategy; let rawFormatter: IOutputFormatterStrategy; - let commandsService: CommandsService; + let analyticsService: MockType; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -90,7 +95,7 @@ describe('CliBusinessService', () => { service = module.get(CliBusinessService); cliTool = module.get(RedisToolService); - commandsService = module.get(CommandsService); + analyticsService = module.get(CliAnalyticsService); const outputFormatterManager: OutputFormatterManager = get( service, 'outputFormatterManager', @@ -110,6 +115,9 @@ describe('CliBusinessService', () => { const result = await service.getClient(mockStandaloneDatabaseEntity.id); expect(result).toEqual({ uuid: mockClientUuid }); + expect(analyticsService.sendClientCreatedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + ); }); it('should throw internal exception on getClient error', async () => { @@ -122,6 +130,10 @@ describe('CliBusinessService', () => { fail(); } catch (err) { expect(err).toBeInstanceOf(InternalServerErrorException); + expect(analyticsService.sendClientCreationFailedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new InternalServerErrorException(mockENotFoundMessage), + ); } }); @@ -133,6 +145,10 @@ describe('CliBusinessService', () => { fail(); } catch (err) { expect(err).toBeInstanceOf(KeytarUnavailableException); + expect(analyticsService.sendClientCreationFailedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new KeytarUnavailableException(), + ); } }); }); @@ -147,6 +163,9 @@ describe('CliBusinessService', () => { ); expect(result).toEqual({ uuid: mockClientUuid }); + expect(analyticsService.sendClientRecreatedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + ); }); it('should throw internal exception on reCreateClient', async () => { @@ -162,6 +181,10 @@ describe('CliBusinessService', () => { fail(); } catch (err) { expect(err).toBeInstanceOf(InternalServerErrorException); + expect(analyticsService.sendClientCreationFailedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new InternalServerErrorException(mockENotFoundMessage), + ); } }); @@ -176,6 +199,10 @@ describe('CliBusinessService', () => { fail(); } catch (err) { expect(err).toBeInstanceOf(KeytarUnavailableException); + expect(analyticsService.sendClientCreationFailedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new KeytarUnavailableException(), + ); } }); }); @@ -190,6 +217,10 @@ describe('CliBusinessService', () => { ); expect(result).toEqual({ affected: 1 }); + expect(analyticsService.sendClientDeletedEvent).toHaveBeenCalledWith( + 1, + mockClientOptions.instanceId, + ); }); it('should throw internal exception on deleteClient', async () => { @@ -203,6 +234,7 @@ describe('CliBusinessService', () => { fail(); } catch (err) { expect(err).toBeInstanceOf(InternalServerErrorException); + expect(analyticsService.sendClientDeletedEvent).not.toHaveBeenCalled(); } }); }); @@ -223,6 +255,13 @@ describe('CliBusinessService', () => { expect(result).toEqual(mockResult); expect(formatSpy).toHaveBeenCalled(); + expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + command: 'memory', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should successfully execute command and return raw response', async () => { const dto: SendCommandDto = { @@ -242,6 +281,13 @@ describe('CliBusinessService', () => { expect(result).toEqual(mockResult); expect(formatSpy).toHaveBeenCalled(); + expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + command: 'memory', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should return response with [CLI_COMMAND_NOT_SUPPORTED] error for sendCommand', async () => { const command = CliToolUnsupportedCommands.ScriptDebug; @@ -256,6 +302,16 @@ describe('CliBusinessService', () => { const result = await service.sendCommand(mockClientOptions, dto); expect(result).toEqual(mockResult); + expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new CommandNotSupportedError( + ERROR_MESSAGES.CLI_COMMAND_NOT_SUPPORTED(command.toUpperCase()), + ), + { + command: 'script', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should return response with [CLI_UNTERMINATED_QUOTES] error for sendCommand', async () => { @@ -269,6 +325,15 @@ describe('CliBusinessService', () => { const result = await service.sendCommand(mockClientOptions, dto); expect(result).toEqual(mockResult); + expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new CommandParsingError( + ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES(), + ), + { + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should return response with redis reply error', async () => { @@ -287,6 +352,14 @@ describe('CliBusinessService', () => { const result = await service.sendCommand(mockClientOptions, dto); expect(result).toEqual(mockResult); + expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + replyError, + { + command: 'get', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should throw internal exception for sendCommand', async () => { @@ -298,6 +371,14 @@ describe('CliBusinessService', () => { fail(); } catch (err) { expect(err).toBeInstanceOf(InternalServerErrorException); + expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new Error(mockENotFoundMessage), + { + command: 'get', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); } }); @@ -310,6 +391,14 @@ describe('CliBusinessService', () => { fail(); } catch (err) { expect(err).toBeInstanceOf(KeytarUnavailableException); + expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new KeytarUnavailableException(), + { + command: 'get', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); } }); it('should return response in correct format for human-readable commands for sendCommand', async () => { @@ -325,6 +414,13 @@ describe('CliBusinessService', () => { const result = await service.sendCommand(mockClientOptions, dto); expect(result).toEqual(mockResult); + expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + command: 'info', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); }); @@ -388,6 +484,18 @@ describe('CliBusinessService', () => { ); expect(result).toEqual(mockResult); + expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: mockIntegerResponse, + status: CommandExecutionStatus.Success, + ...mockNode, + }, + { + command: 'memory', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should return response in correct format for human-readable commands for sendCommandForNodes', async () => { @@ -420,6 +528,18 @@ describe('CliBusinessService', () => { ClusterNodeRole.Master, 'utf8', ); + expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: mockRedisServerInfoResponse, + status: CommandExecutionStatus.Success, + ...mockNode, + }, + { + command: 'info', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should return response with [CLI_COMMAND_NOT_SUPPORTED] error for sendCommandForNodes', async () => { @@ -440,6 +560,16 @@ describe('CliBusinessService', () => { ); expect(result).toEqual(mockResult); + expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new CommandNotSupportedError(ERROR_MESSAGES.CLI_COMMAND_NOT_SUPPORTED( + command.toUpperCase(), + )), + { + command: 'script', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should return response with [CLI_UNTERMINATED_QUOTES] error for sendCommandForNodes', async () => { @@ -458,6 +588,13 @@ describe('CliBusinessService', () => { ); expect(result).toEqual(mockResult); + expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new CommandParsingError(ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES()), + { + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should throw [WrongDatabaseTypeError]', async () => { const command = mockMemoryUsageCommand; @@ -475,6 +612,14 @@ describe('CliBusinessService', () => { } catch (err) { expect(err).toBeInstanceOf(BadRequestException); expect(err.message).toEqual(ERROR_MESSAGES.WRONG_DATABASE_TYPE); + expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new WrongDatabaseTypeError(ERROR_MESSAGES.WRONG_DATABASE_TYPE), + { + command: 'memory', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); } }); it('should throw internal exception', async () => { @@ -490,6 +635,14 @@ describe('CliBusinessService', () => { fail(); } catch (err) { expect(err).toBeInstanceOf(InternalServerErrorException); + expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new Error(mockENotFoundMessage), + { + command: 'memory', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); } }); it('Should proxy EncryptionService errors', async () => { @@ -505,6 +658,14 @@ describe('CliBusinessService', () => { fail(); } catch (err) { expect(err).toBeInstanceOf(KeytarUnavailableException); + expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new KeytarUnavailableException(), + { + command: 'memory', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); } }); }); @@ -531,6 +692,18 @@ describe('CliBusinessService', () => { nodeOptions, ); expect(result).toEqual(mockResult); + expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: mockIntegerResponse, + ...mockNode, + status: CommandExecutionStatus.Success, + }, + { + command: 'memory', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should return human-readable commands for sendCommandForSingleNode', async () => { @@ -560,6 +733,18 @@ describe('CliBusinessService', () => { `${mockNode.host}:${mockNode.port}`, 'utf8', ); + expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: mockRedisServerInfoResponse, + ...mockNode, + status: CommandExecutionStatus.Success, + }, + { + command: 'info', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should successfully execute command for single node with redirection (RAW format)', async () => { @@ -592,6 +777,20 @@ describe('CliBusinessService', () => { expect(cliTool.execCommandForNode).toHaveBeenCalledTimes(2); expect(result).toEqual(mockResult); + expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: 'OK', + ...mockNode, + port: 7002, + slot: 7008, + status: CommandExecutionStatus.Success, + }, + { + command: 'set', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should successfully execute command for single node with redirection (Text format)', async () => { const command = 'set foo bar'; @@ -623,6 +822,20 @@ describe('CliBusinessService', () => { expect(cliTool.execCommandForNode).toHaveBeenCalledTimes(2); expect(result).toEqual(mockResult); + expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: mockResult.response, + ...mockNode, + port: 7002, + slot: 7008, + status: CommandExecutionStatus.Success, + }, + { + command: 'set', + outputFormat: CliOutputFormatterTypes.Text, + }, + ); }); it('should return response for single node with redirection error', async () => { const command = 'set foo bar'; @@ -647,6 +860,19 @@ describe('CliBusinessService', () => { expect(cliTool.execCommandForNode).toHaveBeenCalledTimes(1); expect(result).toEqual(mockResult); + expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + error: mockRedisMovedError, + response: mockRedisMovedError.message, + ...mockNode, + status: CommandExecutionStatus.Fail, + }, + { + command: 'set', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should return response with [CLI_COMMAND_NOT_SUPPORTED] error for sendCommandForSingleNode', async () => { const command = CliToolUnsupportedCommands.ScriptDebug; @@ -665,6 +891,16 @@ describe('CliBusinessService', () => { ); expect(result).toEqual(mockResult); + expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new CommandNotSupportedError(ERROR_MESSAGES.CLI_COMMAND_NOT_SUPPORTED( + command.toUpperCase(), + )), + { + command: 'script', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should return response with [CLI_UNTERMINATED_QUOTES] error for sendCommandForSingleNode', async () => { const command = mockGetEscapedKeyCommand; @@ -681,6 +917,13 @@ describe('CliBusinessService', () => { ); expect(result).toEqual(mockResult); + expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new CommandParsingError(ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES()), + { + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); }); it('should throw [WrongDatabaseTypeError]', async () => { const command = 'get key'; @@ -699,6 +942,14 @@ describe('CliBusinessService', () => { } catch (err) { expect(err).toBeInstanceOf(BadRequestException); expect(err.message).toEqual(ERROR_MESSAGES.WRONG_DATABASE_TYPE); + expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new WrongDatabaseTypeError(ERROR_MESSAGES.WRONG_DATABASE_TYPE), + { + command: 'get', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); } }); it('should throw [ClusterNodeNotFoundError]', async () => { @@ -719,11 +970,21 @@ describe('CliBusinessService', () => { fail(); } catch (err) { expect(err).toBeInstanceOf(BadRequestException); + expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new ClusterNodeNotFoundError( + ERROR_MESSAGES.CLUSTER_NODE_NOT_FOUND('127.0.0.1:7002'), + ), + { + command: 'get', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); } }); it('should throw internal exception', async () => { const command = 'get key'; - cliTool.execCommandForNodes.mockRejectedValue(new Error(mockENotFoundMessage)); + cliTool.execCommandForNode.mockRejectedValue(new Error(mockENotFoundMessage)); try { await service.sendCommandForSingleNode( @@ -735,6 +996,14 @@ describe('CliBusinessService', () => { fail(); } catch (err) { expect(err).toBeInstanceOf(InternalServerErrorException); + expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new Error(mockENotFoundMessage), + { + command: 'get', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); } }); it('Should proxy EncryptionService errors', async () => { @@ -751,6 +1020,14 @@ describe('CliBusinessService', () => { fail(); } catch (err) { expect(err).toBeInstanceOf(KeytarUnavailableException); + expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + new KeytarUnavailableException(), + { + command: 'get', + outputFormat: CliOutputFormatterTypes.Raw, + }, + ); } }); }); diff --git a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts index 270d000ae9..78c4b1396c 100644 --- a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts +++ b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts @@ -6,7 +6,6 @@ import { } from '@nestjs/common'; import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service'; import { CommandsService } from 'src/modules/commands/commands.service'; -import { CommandType } from 'src/constants'; import { ClusterNodeRole, ClusterSingleNodeOptions, @@ -77,11 +76,11 @@ export class CliBusinessService { try { const uuid = await this.cliTool.createNewToolClient(instanceId, namespace); this.logger.log('Succeed to create Redis client for CLI.'); - this.cliAnalyticsService.sendClientCreatedEvent(instanceId, namespace); + this.cliAnalyticsService.sendClientCreatedEvent(instanceId); return { uuid }; } catch (error) { this.logger.error('Failed to create redis client for CLI.', error); - this.cliAnalyticsService.sendClientCreationFailedEvent(instanceId, namespace, error); + this.cliAnalyticsService.sendClientCreationFailedEvent(instanceId, error); throw error; } } @@ -101,11 +100,11 @@ export class CliBusinessService { try { const clientUuid = await this.cliTool.reCreateToolClient(instanceId, uuid, namespace); this.logger.log('Succeed to re-create Redis client for CLI.'); - this.cliAnalyticsService.sendClientRecreatedEvent(instanceId, namespace); + this.cliAnalyticsService.sendClientRecreatedEvent(instanceId); return { uuid: clientUuid }; } catch (error) { this.logger.error('Failed to re-create redis client for CLI.', error); - this.cliAnalyticsService.sendClientCreationFailedEvent(instanceId, namespace, error); + this.cliAnalyticsService.sendClientCreationFailedEvent(instanceId, error); throw error; } } @@ -121,11 +120,10 @@ export class CliBusinessService { ): Promise { this.logger.log('Deleting Redis client for CLI.'); try { - const namespace = this.cliTool.getRedisClientNamespace({ instanceId, uuid }); const affected = await this.cliTool.deleteToolClient(instanceId, uuid); this.logger.log('Succeed to delete Redis client for CLI.'); if (affected) { - this.cliAnalyticsService.sendClientDeletedEvent(affected, instanceId, namespace); + this.cliAnalyticsService.sendClientDeletedEvent(affected, instanceId); } return { affected }; } catch (error) { @@ -145,29 +143,25 @@ export class CliBusinessService { ): Promise { this.logger.log('Executing redis CLI command.'); const { command: commandLine } = dto; - let namespace = AppTool.CLI.toString(); + let command: string; + let args: string[] = []; const outputFormat = dto.outputFormat || CliOutputFormatterTypes.Raw; try { const formatter = this.outputFormatterManager.getStrategy(outputFormat); - const [command, ...args] = splitCliCommandLine(commandLine); + [command, ...args] = splitCliCommandLine(commandLine); const replyEncoding = checkHumanReadableCommands(`${command} ${args[0]}`) ? 'utf8' : undefined; this.checkUnsupportedCommands(`${command} ${args[0]}`); - namespace = this.cliTool.getRedisClientNamespace(clientOptions); const reply = await this.cliTool.execCommand(clientOptions, command, args, replyEncoding); this.logger.log('Succeed to execute redis CLI command.'); - const commandType = await this.checkIsCoreCommand(command) ? CommandType.Core : CommandType.Module; - this.cliAnalyticsService.sendCommandExecutedEvent( clientOptions.instanceId, - namespace, { command, outputFormat, - commandType, }, ); return { @@ -182,10 +176,17 @@ export class CliBusinessService { || error instanceof CommandNotSupportedError || error?.name === 'ReplyError' ) { - this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, namespace, error); + this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, error, { + command, + outputFormat, + }); + return { response: error.message, status: CommandExecutionStatus.Fail }; } - this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, namespace, error); + this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, error, { + command, + outputFormat, + }); if (error instanceof EncryptionServiceErrorException || error instanceof ClientNotFoundErrorException) { throw error; @@ -227,14 +228,15 @@ export class CliBusinessService { role: ClusterNodeRole, outputFormat: CliOutputFormatterTypes = CliOutputFormatterTypes.Raw, ): Promise { - let namespace = AppTool.CLI.toString(); this.logger.log(`Executing redis.cluster CLI command for [${role}] nodes.`); + let command: string; + let args: string[] = []; + try { const formatter = this.outputFormatterManager.getStrategy(outputFormat); - const [command, ...args] = splitCliCommandLine(commandLine); + [command, ...args] = splitCliCommandLine(commandLine); const replyEncoding = checkHumanReadableCommands(`${command} ${args[0]}`) ? 'utf8' : undefined; this.checkUnsupportedCommands(`${command} ${args[0]}`); - namespace = this.cliTool.getRedisClientNamespace(clientOptions); const result = await this.cliTool.execCommandForNodes( clientOptions, @@ -247,7 +249,6 @@ export class CliBusinessService { return result.map((nodeExecReply) => { this.cliAnalyticsService.sendClusterCommandExecutedEvent( clientOptions.instanceId, - namespace, nodeExecReply, { command, outputFormat }, ); @@ -264,13 +265,19 @@ export class CliBusinessService { this.logger.error('Failed to execute redis.cluster CLI command.', error); if (error instanceof CommandParsingError || error instanceof CommandNotSupportedError) { - this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, namespace, error); + this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, error, { + command, + outputFormat, + }); return [ { response: error.message, status: CommandExecutionStatus.Fail }, ]; } - this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, namespace, error); + this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, error, { + command, + outputFormat, + }); if (error instanceof EncryptionServiceErrorException || error instanceof ClientNotFoundErrorException) { throw error; @@ -291,9 +298,12 @@ export class CliBusinessService { outputFormat: CliOutputFormatterTypes = CliOutputFormatterTypes.Raw, ): Promise { this.logger.log(`Executing redis.cluster CLI command for single node ${JSON.stringify(nodeOptions)}`); + let command: string; + let args: string[] = []; + try { const formatter = this.outputFormatterManager.getStrategy(outputFormat); - const [command, ...args] = splitCliCommandLine(commandLine); + [command, ...args] = splitCliCommandLine(commandLine); const replyEncoding = checkHumanReadableCommands(`${command} ${args[0]}`) ? 'utf8' : undefined; this.checkUnsupportedCommands(`${command} ${args[0]}`); const nodeAddress = `${nodeOptions.host}:${nodeOptions.port}`; @@ -322,7 +332,6 @@ export class CliBusinessService { } this.cliAnalyticsService.sendClusterCommandExecutedEvent( clientOptions.instanceId, - 'cli', result, { command, outputFormat }, ); @@ -334,11 +343,17 @@ export class CliBusinessService { this.logger.error('Failed to execute redis.cluster CLI command.', error); if (error instanceof CommandParsingError || error instanceof CommandNotSupportedError) { - this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, 'cli', error); + this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, error, { + command, + outputFormat, + }); return { response: error.message, status: CommandExecutionStatus.Fail }; } - this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, 'cli', error); + this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, error, { + command, + outputFormat, + }); if (error instanceof EncryptionServiceErrorException || error instanceof ClientNotFoundErrorException) { throw error; @@ -363,10 +378,4 @@ export class CliBusinessService { ); } } - - private async checkIsCoreCommand(command: string) { - const commands = await this.commandsService.getCommandsGroups(); - - return !!commands?.main[command.toUpperCase()]; - } } diff --git a/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.controller.ts b/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.controller.ts new file mode 100644 index 0000000000..cf170a0037 --- /dev/null +++ b/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.controller.ts @@ -0,0 +1,32 @@ +import { Controller, Get, Param } from '@nestjs/common'; +import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; +import { ClusterMonitorService } from 'src/modules/cluster-monitor/cluster-monitor.service'; +import { AppTool } from 'src/models'; +import { ApiTags } from '@nestjs/swagger'; +import { ClusterDetails } from 'src/modules/cluster-monitor/models'; + +@ApiTags('Cluster Monitor') +@Controller('/cluster-details') +export class ClusterMonitorController { + constructor(private readonly clusterMonitorService: ClusterMonitorService) {} + + @ApiEndpoint({ + statusCode: 200, + description: 'Get list of available plugins', + responses: [ + { + status: 200, + type: ClusterDetails, + }, + ], + }) + @Get() + async getClusterDetails( + @Param('dbInstance') instanceId: string, + ): Promise { + return this.clusterMonitorService.getClusterDetails({ + instanceId, + tool: AppTool.Common, + }); + } +} diff --git a/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.module.ts b/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.module.ts new file mode 100644 index 0000000000..9d475acae7 --- /dev/null +++ b/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { ClusterMonitorController } from 'src/modules/cluster-monitor/cluster-monitor.controller'; +import { ClusterMonitorService } from 'src/modules/cluster-monitor/cluster-monitor.service'; +import { SharedModule } from 'src/modules/shared/shared.module'; + +@Module({ + imports: [ + SharedModule, + ], + providers: [ + ClusterMonitorService, + ], + controllers: [ + ClusterMonitorController, + ], +}) +export class ClusterMonitorModule {} diff --git a/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.service.ts b/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.service.ts new file mode 100644 index 0000000000..be2c003a63 --- /dev/null +++ b/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.service.ts @@ -0,0 +1,97 @@ +import { get } from 'lodash'; +import IORedis from 'ioredis'; +import { + BadRequestException, HttpException, Injectable, Logger, +} from '@nestjs/common'; +import { catchAclError, convertRedisInfoReplyToObject } from 'src/utils'; +import { IFindRedisClientInstanceByOptions, RedisService } from 'src/modules/core/services/redis/redis.service'; +import { InstancesBusinessService } from 'src/modules/shared/services/instances-business/instances-business.service'; +import { IClusterInfo } from 'src/modules/cluster-monitor/strategies/cluster.info.interface'; +import { ClusterNodesInfoStrategy } from 'src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy'; +import { ClusterShardsInfoStrategy } from 'src/modules/cluster-monitor/strategies/cluster-shards.info.strategy'; +import { ClusterDetails } from 'src/modules/cluster-monitor/models'; + +export enum ClusterInfoStrategies { + CLUSTER_NODES = 'CLUSTER_NODES', + CLUSTER_SHARDS = 'CLUSTER_SHARDS', +} + +@Injectable() +export class ClusterMonitorService { + private logger = new Logger('ClusterMonitorService'); + + private infoStrategies: Map = new Map(); + + constructor( + private redisService: RedisService, + private instancesBusinessService: InstancesBusinessService, + ) { + this.infoStrategies.set(ClusterInfoStrategies.CLUSTER_NODES, new ClusterNodesInfoStrategy()); + this.infoStrategies.set(ClusterInfoStrategies.CLUSTER_SHARDS, new ClusterShardsInfoStrategy()); + } + + /** + * Get cluster details and details for all nodes + * @param clientOptions + */ + public async getClusterDetails(clientOptions: IFindRedisClientInstanceByOptions): Promise { + try { + const client = await this.getClient(clientOptions); + + if (!(client instanceof IORedis.Cluster)) { + return Promise.reject(new BadRequestException('Current database is not in a cluster mode')); + } + + const info = convertRedisInfoReplyToObject(await client.info('server')); + + const strategy = this.getClusterInfoStrategy(get(info, 'server.redis_version')); + + return await strategy.getClusterDetails(client); + } catch (e) { + this.logger.error('Unable to get cluster details', e); + + if (e instanceof HttpException) { + throw e; + } + + throw catchAclError(e); + } + } + + /** + * Return strategy on how we are going to fetch topology and other cluster info + * based on Redis version + * @param version + * @private + */ + private getClusterInfoStrategy(version: string): IClusterInfo { + const intVersion = parseInt(version, 10) || 0; + if (intVersion >= 7) { + return this.infoStrategies.get(ClusterInfoStrategies.CLUSTER_SHARDS); + } + + return this.infoStrategies.get(ClusterInfoStrategies.CLUSTER_NODES); + } + + /** + * Get or create redis "common" client + * + * @param clientOptions + * @private + */ + private async getClient(clientOptions: IFindRedisClientInstanceByOptions) { + const { tool, instanceId } = clientOptions; + + const commonClient = this.redisService.getClientInstance({ instanceId, tool })?.client; + + if (commonClient && this.redisService.isClientConnected(commonClient)) { + return commonClient; + } + + return this.instancesBusinessService.connectToInstance( + clientOptions.instanceId, + clientOptions.tool, + true, + ); + } +} diff --git a/redisinsight/api/src/modules/cluster-monitor/models/cluster-details.ts b/redisinsight/api/src/modules/cluster-monitor/models/cluster-details.ts new file mode 100644 index 0000000000..540ff0dca7 --- /dev/null +++ b/redisinsight/api/src/modules/cluster-monitor/models/cluster-details.ts @@ -0,0 +1,123 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { ClusterNodeDetails } from 'src/modules/cluster-monitor/models/cluster-node-details'; + +export class ClusterDetails { + @ApiProperty({ + type: String, + description: 'Redis version', + example: '7.0.2', + }) + version: string; + + @ApiProperty({ + type: String, + description: 'Redis mode. Currently one of: standalone, cluster or sentinel', + example: 'cluster', + }) + mode: string; + + @ApiProperty({ + type: String, + description: 'Username from the connection or undefined in case when connected with default user', + example: 'user1', + }) + user?: string; + + @ApiProperty({ + type: Number, + description: 'Maximum value uptime_in_seconds from all nodes', + example: 3600, + }) + uptimeSec: number; + + @ApiProperty({ + type: String, + description: 'cluster_state from CLUSTER INFO command', + example: 'ok', + }) + state: string; + + @ApiProperty({ + type: String, + description: 'cluster_slots_assigned from CLUSTER INFO command', + example: 16384, + }) + slotsAssigned: number; + + @ApiProperty({ + type: String, + description: 'cluster_slots_ok from CLUSTER INFO command', + example: 16384, + }) + slotsOk: number; + + @ApiProperty({ + type: String, + description: 'cluster_slots_pfail from CLUSTER INFO command', + example: 0, + }) + slotsPFail: number; + + @ApiProperty({ + type: String, + description: 'cluster_slots_fail from CLUSTER INFO command', + example: 0, + }) + slotsFail: number; + + @ApiProperty({ + type: String, + description: 'Calculated from (16384 - cluster_slots_assigned from CLUSTER INFO command)', + example: 0, + }) + slotsUnassigned: number; + + @ApiProperty({ + type: String, + description: 'cluster_stats_messages_sent from CLUSTER INFO command', + example: 2451, + }) + statsMessagesSent: number; + + @ApiProperty({ + type: String, + description: 'cluster_stats_messages_received from CLUSTER INFO command', + example: 2451, + }) + statsMessagesReceived: number; + + @ApiProperty({ + type: String, + description: 'cluster_current_epoch from CLUSTER INFO command', + example: 6, + }) + currentEpoch: number; + + @ApiProperty({ + type: String, + description: 'cluster_my_epoch from CLUSTER INFO command', + example: 2, + }) + myEpoch: number; + + @ApiProperty({ + type: String, + description: 'Number of shards. cluster_size from CLUSTER INFO command', + example: 3, + }) + size: number; + + @ApiProperty({ + type: String, + description: 'All nodes number in the Cluster. cluster_known_nodes from CLUSTER INFO command', + example: 9, + }) + knownNodes: number; + + @ApiProperty({ + type: () => ClusterNodeDetails, + isArray: true, + description: 'Details per each node', + }) + nodes: ClusterNodeDetails[]; +} diff --git a/redisinsight/api/src/modules/cluster-monitor/models/cluster-node-details.ts b/redisinsight/api/src/modules/cluster-monitor/models/cluster-node-details.ts new file mode 100644 index 0000000000..4265a61014 --- /dev/null +++ b/redisinsight/api/src/modules/cluster-monitor/models/cluster-node-details.ts @@ -0,0 +1,171 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export enum NodeRole { + Primary = 'primary', + Replica = 'replica', +} + +export enum HealthStatus { + Online = 'online', + Offline = 'offline', + Loading = 'loading', +} + +export class ClusterNodeDetails { + @ApiProperty({ + type: String, + description: 'Node id', + example: 'c33218e9ff2faf8749bfb6585ba1e6d40a4e94fb', + }) + id: string; + + @ApiProperty({ + type: String, + description: 'Redis version', + example: '7.0.2', + }) + version: string; + + @ApiProperty({ + type: String, + description: 'Redis mode', + example: 'cluster', + }) + mode: string; + + @ApiProperty({ + type: String, + description: 'Node IP address', + example: '172.30.0.101', + }) + host: string; + + @ApiProperty({ + type: Number, + description: 'Node IP address', + example: 6379, + }) + port: number; + + @ApiProperty({ + type: String, + enum: NodeRole, + description: 'Node role in cluster', + }) + role: NodeRole; + + @ApiProperty({ + type: String, + description: 'ID of primary node (for replica only)', + example: 'c33218e9ff2faf8749bfb6585ba1e6d40a4e94fb', + }) + primary?: string; + + @ApiProperty({ + type: String, + enum: HealthStatus, + description: 'Node\'s current health status', + }) + health: HealthStatus; + + @ApiProperty({ + type: String, + isArray: true, + description: 'Array of assigned slots or slots ranges. Shown for primary nodes only', + example: ['0-5638', '11256'], + }) + slots?: string[]; + + @ApiProperty({ + type: Number, + description: 'Total keys stored inside this node', + example: 256478, + }) + totalKeys: number; + + @ApiProperty({ + type: Number, + description: 'Memory used by node. "memory.used_memory" from INFO command', + example: 256478, + }) + usedMemory: number; + + @ApiProperty({ + type: Number, + description: 'Current operations per second. "stats.instantaneous_ops_per_sec" from INFO command', + example: 12569, + }) + opsPerSecond: number; + + @ApiProperty({ + type: Number, + description: 'Total connections received by node. "stats.total_connections_received" from INFO command', + example: 3256, + }) + connectionsReceived: number; + + @ApiProperty({ + type: Number, + description: 'Currently connected clients. "clients.connected_clients" from INFO command', + example: 3256, + }) + connectedClients: number; + + @ApiProperty({ + type: Number, + description: 'Total commands processed by node. "stats.total_commands_processed" from INFO command', + example: 32560000000, + }) + commandsProcessed: number; + + @ApiProperty({ + type: Number, + description: 'Current input network usage in KB/s. "stats.instantaneous_input_kbps" from INFO command', + example: 12000, + }) + networkInKbps: number; + + @ApiProperty({ + type: Number, + description: 'Current output network usage in KB/s. "stats.instantaneous_output_kbps" from INFO command', + example: 12000, + }) + networkOutKbps: number; + + @ApiProperty({ + type: Number, + description: 'Ratio for cache hits and misses [0 - 1]. Ideally should be close to 1', + example: 0.8, + }) + cacheHitRatio?: number; + + @ApiProperty({ + type: Number, + description: 'The replication offset of this node. This information can be used to ' + + 'send commands to the most up to date replicas.', + example: 12000, + }) + replicationOffset: number; + + @ApiProperty({ + type: Number, + description: 'For replicas only. Determines on how much replica is behind of primary.', + example: 0, + }) + replicationLag?: number; + + @ApiProperty({ + type: Number, + description: 'Current node uptime_in_seconds', + example: 12000, + }) + uptimeSec: number; + + @ApiProperty({ + type: () => ClusterNodeDetails, + isArray: true, + description: 'For primary nodes only. Replica node(s) details', + example: [], + }) + replicas?: ClusterNodeDetails[]; +} diff --git a/redisinsight/api/src/modules/cluster-monitor/models/index.ts b/redisinsight/api/src/modules/cluster-monitor/models/index.ts new file mode 100644 index 0000000000..72ee82bce8 --- /dev/null +++ b/redisinsight/api/src/modules/cluster-monitor/models/index.ts @@ -0,0 +1,2 @@ +export * from './cluster-details'; +export * from './cluster-node-details'; diff --git a/redisinsight/api/src/modules/cluster-monitor/strategies/abstract.info.strategy.spec.ts b/redisinsight/api/src/modules/cluster-monitor/strategies/abstract.info.strategy.spec.ts new file mode 100644 index 0000000000..ea2cc1ec37 --- /dev/null +++ b/redisinsight/api/src/modules/cluster-monitor/strategies/abstract.info.strategy.spec.ts @@ -0,0 +1,151 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { when } from 'jest-when'; +import { set } from 'lodash'; +import IORedis from 'ioredis'; +import { ClusterNodesInfoStrategy } from 'src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy'; +import { ClusterDetails, ClusterNodeDetails } from 'src/modules/cluster-monitor/models'; +import { mockStandaloneRedisInfoReply } from 'src/__mocks__'; + +const m1 = { + id: 'm1', + health: 'online', + host: '172.30.100.1', + primary: undefined, + port: 6379, + role: 'primary', + slots: ['0-5000'], +}; +const m2 = { + id: 'm2', + health: 'online', + host: '172.30.100.4', + primary: undefined, + port: 6379, + role: 'primary', + slots: ['5001-10921', '10922'], +}; +const m3 = { + id: 'm3', + health: 'online', + host: '172.30.100.7', + primary: undefined, + port: 6379, + role: 'primary', + slots: ['10923-16383'], +}; + +const node1 = Object.create(IORedis.prototype); +node1.sendCommand = jest.fn(); +set(node1, 'options', { + host: m1.host, + port: m1.port, +}); + +const node2 = Object.create(IORedis.prototype); +node2.sendCommand = jest.fn(); +set(node2, 'options', { + host: m2.host, + port: m2.port, +}); + +const clusterClient = Object.create(IORedis.Cluster.prototype); +clusterClient.sendCommand = jest.fn(); +clusterClient.nodes = jest.fn().mockReturnValue([node1, node2]); + +const mockClusterInfo: Partial = { + state: 'ok', + slotsAssigned: 16374, + slotsOk: 16360, + slotsPFail: 10, + slotsFail: 4, + slotsUnassigned: 10, + statsMessagesSent: 1000, + statsMessagesReceived: 999, + knownNodes: 9, + size: 3, + myEpoch: 2, + currentEpoch: 6, +}; + +const baseNodeDetails: Partial = { + cacheHitRatio: 1, + connectedClients: 1, + replicas: [], + replicationOffset: 0, + totalKeys: 1, + uptimeSec: 1000, + usedMemory: 1000000, + version: '6.0.5', + mode: 'standalone', +}; + +const mockNode1Details = { + ...baseNodeDetails, + ...m1, +} as ClusterNodeDetails; + +const mockNode2Details = { + ...baseNodeDetails, + ...m2, +} as ClusterNodeDetails; + +const mockClusterDetails: Partial = { + ...mockClusterInfo, + version: '6.0.5', + mode: 'standalone', + uptimeSec: 1000, + nodes: [mockNode1Details, mockNode2Details], +}; + +const mockClusterInfoReply = '' + + `cluster_state:${mockClusterInfo.state}\r\n` + + `cluster_slots_assigned:${mockClusterInfo.slotsAssigned}\r\n` + + `cluster_slots_ok:${mockClusterInfo.slotsOk}\r\n` + + `cluster_slots_pfail:${mockClusterInfo.slotsPFail}\r\n` + + `cluster_slots_fail:${mockClusterInfo.slotsFail}\r\n` + + `cluster_stats_messages_sent:${mockClusterInfo.statsMessagesSent}\r\n` + + `cluster_stats_messages_received:${mockClusterInfo.statsMessagesReceived}\r\n` + + `cluster_known_nodes:${mockClusterInfo.knownNodes}\r\n` + + `cluster_size:${mockClusterInfo.size}\r\n` + + `cluster_current_epoch:${mockClusterInfo.currentEpoch}\r\n` + + `cluster_my_epoch:${mockClusterInfo.myEpoch}\r\n`; + +describe('AbstractInfoStrategy', () => { + let service: ClusterNodesInfoStrategy; + + beforeEach(async () => { + jest.clearAllMocks(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ClusterNodesInfoStrategy, + ], + }).compile(); + + service = module.get(ClusterNodesInfoStrategy); + service['getClusterNodesFromRedis'] = jest.fn().mockResolvedValue([m1, m2, m3]); + }); + + describe('getClusterInfo', () => { + beforeEach(() => { + when(clusterClient.sendCommand) + .mockResolvedValue(mockClusterInfoReply); + }); + it('should return cluster info', async () => { + const info = await ClusterNodesInfoStrategy.getClusterInfo(clusterClient); + expect(info).toEqual(mockClusterInfo); + }); + }); + + describe('getClusterDetails', () => { + beforeEach(() => { + clusterClient.sendCommand.mockResolvedValue(mockClusterInfoReply); + node1.sendCommand.mockResolvedValue(mockStandaloneRedisInfoReply); + node2.sendCommand.mockResolvedValue(mockStandaloneRedisInfoReply); + }); + it('should return cluster info', async () => { + const info = await service.getClusterDetails(clusterClient); + expect(info).toEqual(mockClusterDetails); + }); + }); +}); diff --git a/redisinsight/api/src/modules/cluster-monitor/strategies/abstract.info.strategy.ts b/redisinsight/api/src/modules/cluster-monitor/strategies/abstract.info.strategy.ts new file mode 100644 index 0000000000..7c4160e1be --- /dev/null +++ b/redisinsight/api/src/modules/cluster-monitor/strategies/abstract.info.strategy.ts @@ -0,0 +1,188 @@ +import { IClusterInfo } from 'src/modules/cluster-monitor/strategies/cluster.info.interface'; +import { Cluster, Redis, Command } from 'ioredis'; +import { convertBulkStringsToObject, convertRedisInfoReplyToObject, convertStringToNumber } from 'src/utils'; +import { get, map, sum } from 'lodash'; +import { ClusterDetails, ClusterNodeDetails } from 'src/modules/cluster-monitor/models'; +import { plainToClass } from 'class-transformer'; + +export abstract class AbstractInfoStrategy implements IClusterInfo { + /** + * Get cluster detailed information + * with each node details and cluster topology + * @param client + */ + async getClusterDetails(client: Cluster): Promise { + let clusterDetails = await AbstractInfoStrategy.getClusterInfo(client); + + const redisClusterNodes = await this.getClusterNodesFromRedis(client); + + const nodes = await this.getClusterNodesInfo(client, redisClusterNodes); + + clusterDetails = { + ...clusterDetails, + ...(AbstractInfoStrategy.calculateAdditionalClusterMetrics(client, nodes)), + nodes: AbstractInfoStrategy.createClusterHierarchy(nodes), + version: get(nodes, '0.version'), + mode: get(nodes, '0.mode'), + }; + + return plainToClass(ClusterDetails, clusterDetails); + } + + /** + * Get array of ClusterNodeDetails + * @param client + * @param nodes + * @private + */ + private async getClusterNodesInfo(client: Cluster, nodes): Promise { + const clientNodes = client.nodes(); + return await Promise.all(nodes.map((node) => { + const clientNode = clientNodes.find((n) => n.options?.host === node.host && n.options?.port === node.port); + + if (clientNode) { + return this.getClusterNodeInfo(clientNode, node); + } + + return undefined; + }).filter((n) => n)); + } + + /** + * Get info (ClusterNodeDetails) for particular node + some extra fields + * which will be ignored on the later stage + * @param nodeClient + * @param node + * @private + */ + private async getClusterNodeInfo(nodeClient: Redis, node): Promise { + const info = convertRedisInfoReplyToObject(await nodeClient.info()); + + return { + ...node, + totalKeys: sum(map(get(info, 'keyspace', {}), (dbKeys): number => { + const { keys } = convertBulkStringsToObject(dbKeys, ',', '='); + return parseInt(keys, 10); + })), + usedMemory: convertStringToNumber(get(info, 'memory.used_memory')), + opsPerSecond: convertStringToNumber(get(info, 'stats.instantaneous_ops_per_sec')), + connectionsReceived: convertStringToNumber(get(info, 'stats.total_connections_received')), + connectedClients: convertStringToNumber(get(info, 'clients.connected_clients')), + commandsProcessed: convertStringToNumber(get(info, 'stats.total_commands_processed')), + networkInKbps: convertStringToNumber(get(info, 'stats.instantaneous_input_kbps')), + networkOutKbps: convertStringToNumber(get(info, 'stats.instantaneous_output_kbps')), + cacheHitRatio: AbstractInfoStrategy.calculateCacheHitRatio( + convertStringToNumber(get(info, 'stats.keyspace_hits'), 0), + convertStringToNumber(get(info, 'stats.keyspace_misses'), 0), + ), + replicationOffset: convertStringToNumber(get(info, 'replication.master_repl_offset')), + uptimeSec: convertStringToNumber(get(info, 'server.uptime_in_seconds'), 0), + version: get(info, 'server.redis_version'), + mode: get(info, 'server.redis_mode'), + }; + } + + /** + * Get bunch of fields from CLUSTER INFO command + * @param client + */ + static async getClusterInfo(client: Cluster): Promise> { + // @ts-ignore + const info = convertBulkStringsToObject(await client.sendCommand(new Command('cluster', ['info'], { + replyEncoding: 'utf8', + }))); + + const slotsState = { + slotsAssigned: convertStringToNumber(info.cluster_slots_assigned, 0), + slotsOk: convertStringToNumber(info.cluster_slots_ok, 0), + slotsPFail: convertStringToNumber(info.cluster_slots_pfail, 0), + slotsFail: convertStringToNumber(info.cluster_slots_fail, 0), + }; + + return { + state: info.cluster_state, + ...slotsState, + slotsUnassigned: 16384 - slotsState.slotsAssigned, + statsMessagesSent: convertStringToNumber(info.cluster_stats_messages_sent, 0), + statsMessagesReceived: convertStringToNumber(info.cluster_stats_messages_received, 0), + currentEpoch: convertStringToNumber(info.cluster_current_epoch, 0), + myEpoch: convertStringToNumber(info.cluster_my_epoch, 0), + size: convertStringToNumber(info.cluster_size, 0), + knownNodes: convertStringToNumber(info.cluster_known_nodes, 0), + }; + } + + /** + * Create cluster's topology and calculate primary/slave related metric such as replicationLag + * @param nodes + */ + static createClusterHierarchy(nodes): ClusterNodeDetails[] { + const primaryNodes = {}; + + // get primary nodes + nodes.forEach((node) => { + if (node.role === 'primary') { + primaryNodes[node.id] = { + ...node, + replicas: [], + }; + } + }); + + // assign replicas to primary nodes + // also calculate replicationLag + nodes.forEach((node) => { + if (node.primary && primaryNodes[node.primary]) { + const replicationLag = primaryNodes[node.primary].replicationOffset - node.replicationOffset; + primaryNodes[node.primary].replicas.push({ + ...node, + replicationLag: replicationLag > -1 ? replicationLag : 0, + }); + } + }); + + return Object.values(primaryNodes); + } + + /** + * Calculate hit ratio based on hits and misses values + * Will not fail in case of an error + * @param hits + * @param misses + */ + static calculateCacheHitRatio(hits: number, misses: number): number { + try { + const cacheHitRate = hits / (hits + misses); + return cacheHitRate >= 0 ? cacheHitRate : null; + } catch (e) { + // ignore error + } + + return undefined; + } + + /** + * Calculate additional cluster metrics based on current connection and nodes details + * @param client + * @param nodes + */ + static calculateAdditionalClusterMetrics( + client: Cluster, + nodes: ClusterNodeDetails[], + ): Partial { + const additionalDetails: Partial = { + user: get(client, 'options.redisOptions.username'), + uptimeSec: 0, + }; + + nodes.forEach((node) => { + if (additionalDetails.uptimeSec < node.uptimeSec) { + additionalDetails.uptimeSec = node.uptimeSec; + } + }); + + return additionalDetails; + } + + abstract getClusterNodesFromRedis(client: Cluster); +} diff --git a/redisinsight/api/src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy.spec.ts b/redisinsight/api/src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy.spec.ts new file mode 100644 index 0000000000..37977777bc --- /dev/null +++ b/redisinsight/api/src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy.spec.ts @@ -0,0 +1,73 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { when } from 'jest-when'; +import IORedis from 'ioredis'; +import { ClusterNodesInfoStrategy } from 'src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy'; + +const clusterClient = Object.create(IORedis.Cluster.prototype); +clusterClient.sendCommand = jest.fn(); + +const m1 = { + id: 'm1', + health: 'online', + host: '172.30.100.1', + primary: undefined, + port: 6379, + role: 'primary', + slots: ['0-5000'], +}; +const m2 = { + id: 'm2', + health: 'loading', + host: '172.30.100.4', + primary: undefined, + port: 6379, + role: 'primary', + slots: ['5001-10921', '10922'], +}; +const m3 = { + id: 'm3', + health: 'offline', + host: '172.30.100.7', + primary: undefined, + port: 6379, + role: 'primary', + slots: ['10923-16383'], +}; + +const mockClusterNodesReply = '' + + 'm1 172.30.100.1:6379@16379 master - 0 1661415706000 2 connected 0-5000\n' + + 's11 172.30.100.2:6379@16379 slave m1 0 1661415705000 3 connected\n' + + 's12 172.30.100.3:6379@16379 slave m1 0 1661415705000 3 connected\n' + + 'm2 172.30.100.4:6379@16379 myself,pfail - 0 1661415702000 1 connected 5001-10921 10922\n' + + 's21 172.30.100.5:6379@16379 slave m2 0 1661415704000 2 connected\n' + + 's22 172.30.100.6:6379@16379 slave m2 0 1661415705230 2 connected\n' + + 'm3 172.30.100.7:6379@16379 master,fail - 0 1661415702000 1 connected 10923-16383\n' + + 's31 172.30.100.8:6379@16379 slave m3 0 1661415704000 2 connected\n' + + 's32 172.30.100.9:6379@16379 slave m3 0 1661415705230 2 connected\n'; + +describe('ClusterNodesInfoStrategy', () => { + let service: ClusterNodesInfoStrategy; + + beforeEach(async () => { + jest.clearAllMocks(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ClusterNodesInfoStrategy, + ], + }).compile(); + + service = module.get(ClusterNodesInfoStrategy); + }); + + describe('getClusterNodesFromRedis', () => { + beforeEach(() => { + when(clusterClient.sendCommand) + .mockResolvedValue(mockClusterNodesReply); + }); + it('should return cluster info', async () => { + const info = await service.getClusterNodesFromRedis(clusterClient); + expect(info).toEqual([m1, m2, m3]); + }); + }); +}); diff --git a/redisinsight/api/src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy.ts b/redisinsight/api/src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy.ts new file mode 100644 index 0000000000..b5ea01f7f2 --- /dev/null +++ b/redisinsight/api/src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy.ts @@ -0,0 +1,39 @@ +import { AbstractInfoStrategy } from 'src/modules/cluster-monitor/strategies/abstract.info.strategy'; +import { Cluster, Command } from 'ioredis'; +import { ClusterNodeDetails, HealthStatus, NodeRole } from 'src/modules/cluster-monitor/models'; + +export class ClusterNodesInfoStrategy extends AbstractInfoStrategy { + async getClusterNodesFromRedis(client: Cluster): Promise[]> { + const resp = await client.sendCommand(new Command('cluster', ['nodes'], { + replyEncoding: 'utf8', + })) as string; + + return resp.split('\n').filter((e) => e).map((nodeString) => { + const [id, endpoint, flags, primary,,,,, ...slots] = nodeString.split(' '); + const [host, ports] = endpoint.split(':'); + const [port] = ports.split('@'); + return { + id, + host, + port: parseInt(port, 10), + role: primary && primary !== '-' ? NodeRole.Replica : NodeRole.Primary, + primary: primary && primary !== '-' ? primary : undefined, + slots: slots?.length ? slots : undefined, + health: ClusterNodesInfoStrategy.determineNodeHealth(flags), + }; + }) + .filter((node) => node.role === NodeRole.Primary); // tmp work with primary nodes only; + } + + static determineNodeHealth(flags: string): HealthStatus { + if (flags.indexOf('fail') > -1 && flags.indexOf('pfail') < 0) { + return HealthStatus.Offline; + } + + if (flags.indexOf('master') > -1 || flags.indexOf('slave') > -1) { + return HealthStatus.Online; + } + + return HealthStatus.Loading; + } +} diff --git a/redisinsight/api/src/modules/cluster-monitor/strategies/cluster-shards.info.strategy.spec.ts b/redisinsight/api/src/modules/cluster-monitor/strategies/cluster-shards.info.strategy.spec.ts new file mode 100644 index 0000000000..f2770a1cd9 --- /dev/null +++ b/redisinsight/api/src/modules/cluster-monitor/strategies/cluster-shards.info.strategy.spec.ts @@ -0,0 +1,139 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { when } from 'jest-when'; +import IORedis from 'ioredis'; +import { ClusterShardsInfoStrategy } from 'src/modules/cluster-monitor/strategies/cluster-shards.info.strategy'; + +const clusterClient = Object.create(IORedis.Cluster.prototype); +clusterClient.sendCommand = jest.fn(); + +const m1 = { + id: 'm1', + health: 'online', + host: '172.30.100.1', + primary: undefined, + port: 6379, + role: 'primary', + slots: ['0-5000'], +}; +const m2 = { + id: 'm2', + health: 'loading', + host: '172.30.100.4', + primary: undefined, + port: 6379, + role: 'primary', + slots: ['5001-10921', '10922'], +}; +const m3 = { + id: 'm3', + health: 'offline', + host: '172.30.100.7', + primary: undefined, + port: 6379, + role: 'primary', + slots: ['10923-16383'], +}; + +const mockClusterShardsReply = [ + [ + 'slots', [0, 5000], + 'nodes', [ + [ + 'id', 'm1', + 'port', 6379, + 'ip', '172.30.100.1', + 'endpoint', '172.30.100.212', + 'hostname', '', + 'role', 'master', + 'replication-offset', 107870, + 'health', 'online', + ], + [ + 'id', 's11', + 'port', 6379, + 'ip', '172.30.100.2', + 'endpoint', '172.30.100.212', + 'hostname', '', + 'role', 'slave', + 'replication-offset', 107870, + 'health', 'online', + ], + ], + ], + [ + 'slots', [5001, 10921, 10922, 10922], + 'nodes', [ + [ + 'id', 'm2', + 'port', 6379, + 'ip', '172.30.100.4', + 'endpoint', '172.30.100.212', + 'hostname', '', + 'role', 'master', + 'replication-offset', 107870, + 'health', 'loading', + ], + [ + 'id', 's21', + 'port', 6379, + 'ip', '172.30.100.5', + 'endpoint', '172.30.100.212', + 'hostname', '', + 'role', 'slave', + 'replication-offset', 107870, + 'health', 'online', + ], + ], + ], + [ + 'slots', [10923, 16383], + 'nodes', [ + [ + 'id', 'm3', + 'port', 6379, + 'ip', '172.30.100.7', + 'endpoint', '172.30.100.212', + 'hostname', '', + 'role', 'master', + 'replication-offset', 107870, + 'health', 'offline', + ], + [ + 'id', 's31', + 'port', 6379, + 'ip', '172.30.100.8', + 'endpoint', '172.30.100.212', + 'hostname', '', + 'role', 'slave', + 'replication-offset', 107870, + 'health', 'online', + ], + ], + ], +]; +describe('ClusterShardsInfoStrategy', () => { + let service: ClusterShardsInfoStrategy; + + beforeEach(async () => { + jest.clearAllMocks(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ClusterShardsInfoStrategy, + ], + }).compile(); + + service = module.get(ClusterShardsInfoStrategy); + }); + + describe('getClusterNodesFromRedis', () => { + beforeEach(() => { + when(clusterClient.sendCommand) + .mockResolvedValue(mockClusterShardsReply); + }); + it('should return cluster info', async () => { + const info = await service.getClusterNodesFromRedis(clusterClient); + expect(info).toEqual([m1, m2, m3]); + }); + }); +}); diff --git a/redisinsight/api/src/modules/cluster-monitor/strategies/cluster-shards.info.strategy.ts b/redisinsight/api/src/modules/cluster-monitor/strategies/cluster-shards.info.strategy.ts new file mode 100644 index 0000000000..44fc64653c --- /dev/null +++ b/redisinsight/api/src/modules/cluster-monitor/strategies/cluster-shards.info.strategy.ts @@ -0,0 +1,62 @@ +import { Cluster, Command } from 'ioredis'; +import { chunk } from 'lodash'; +import { AbstractInfoStrategy } from 'src/modules/cluster-monitor/strategies/abstract.info.strategy'; +import { convertStringsArrayToObject } from 'src/utils'; +import { ClusterNodeDetails, NodeRole } from 'src/modules/cluster-monitor/models'; + +export class ClusterShardsInfoStrategy extends AbstractInfoStrategy { + async getClusterNodesFromRedis(client: Cluster) { + const resp = await client.sendCommand(new Command('cluster', ['shards'], { + replyEncoding: 'utf8', + })) as any[]; + + return [].concat(...resp.map((shardArray) => { + const shard = convertStringsArrayToObject(shardArray); + const slots = ClusterShardsInfoStrategy.calculateSlots(shard.slots); + return ClusterShardsInfoStrategy.processShardNodes(shard.nodes, slots); + })); + } + + static calculateSlots(slots: number[]): string[] { + return chunk(slots, 2).map(([slot1, slot2]) => { + if (slot1 === slot2) { + return `${slot1}`; + } + + return `${slot1}-${slot2}`; + }); + } + + static processShardNodes(shardNodes: any[], slots: string[]): Partial[] { + let primary; + const nodes = shardNodes.map((nodeArray) => { + const nodeObj = convertStringsArrayToObject(nodeArray); + const node = { + id: nodeObj.id, + host: nodeObj.ip, + port: nodeObj.port, + role: nodeObj.role === 'master' ? NodeRole.Primary : NodeRole.Replica, + health: nodeObj.health, + }; + + if (node.role === 'primary') { + primary = node.id; + node['slots'] = slots; + } + + return node; + }); + + return nodes.map((node) => { + if (node.role !== NodeRole.Primary) { + return { + ...node, + primary, + }; + } + + return node; + }) + .filter((node) => node.role === NodeRole.Primary); // tmp work with primary nodes only + } +} diff --git a/redisinsight/api/src/modules/cluster-monitor/strategies/cluster.info.interface.ts b/redisinsight/api/src/modules/cluster-monitor/strategies/cluster.info.interface.ts new file mode 100644 index 0000000000..c8cc22d721 --- /dev/null +++ b/redisinsight/api/src/modules/cluster-monitor/strategies/cluster.info.interface.ts @@ -0,0 +1,6 @@ +import { Cluster } from 'ioredis'; +import { ClusterDetails } from 'src/modules/cluster-monitor/models'; + +export interface IClusterInfo { + getClusterDetails(client: Cluster): Promise; +} diff --git a/redisinsight/api/src/modules/commands/commands.module.ts b/redisinsight/api/src/modules/commands/commands.module.ts index 9423d13cae..c14ff6c84d 100644 --- a/redisinsight/api/src/modules/commands/commands.module.ts +++ b/redisinsight/api/src/modules/commands/commands.module.ts @@ -17,7 +17,7 @@ const COMMANDS_CONFIGS = config.get('commands'); }, ], exports: [ - CommandsService - ] + CommandsService, + ], }) export class CommandsModule {} diff --git a/redisinsight/api/src/modules/core/core.module.ts b/redisinsight/api/src/modules/core/core.module.ts index 43dfefce74..e4af74ffc8 100644 --- a/redisinsight/api/src/modules/core/core.module.ts +++ b/redisinsight/api/src/modules/core/core.module.ts @@ -3,13 +3,13 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CaCertificateEntity } from 'src/modules/core/models/ca-certificate.entity'; import { ClientCertificateEntity } from 'src/modules/core/models/client-certificate.entity'; import { PlainEncryptionStrategy } from 'src/modules/core/encryption/strategies/plain-encryption.strategy'; -import { AgreementsRepository } from './repositories/agreements.repository'; -import { ServerRepository } from './repositories/server.repository'; -import { SettingsRepository } from './repositories/settings.repository'; -import settingsOnPremiseFactory from './providers/settings-on-premise'; -import serverOnPremiseFactory from './providers/server-on-premise'; import { EncryptionService } from 'src/modules/core/encryption/encryption.service'; import { KeytarEncryptionStrategy } from 'src/modules/core/encryption/strategies/keytar-encryption.strategy'; +import { AgreementsEntity } from 'src/modules/core/models/agreements.entity'; +import { ServerEntity } from 'src/modules/core/models/server.entity'; +import { SettingsEntity } from 'src/modules/core/models/settings.entity'; +import settingsOnPremiseFactory from './providers/settings-on-premise'; +import serverOnPremiseFactory from './providers/server-on-premise'; import { CaCertBusinessService } from './services/certificates/ca-cert-business/ca-cert-business.service'; import { ClientCertBusinessService } from './services/certificates/client-cert-business/client-cert-business.service'; import { RedisService } from './services/redis/redis.service'; @@ -32,11 +32,11 @@ export class CoreModule { module: CoreModule, imports: [ TypeOrmModule.forFeature([ + ServerEntity, + SettingsEntity, + AgreementsEntity, CaCertificateEntity, ClientCertificateEntity, - AgreementsRepository, - ServerRepository, - SettingsRepository, ]), ], providers: [ diff --git a/redisinsight/api/src/modules/core/providers/server-on-premise/index.ts b/redisinsight/api/src/modules/core/providers/server-on-premise/index.ts index 70fac4d008..2cbc19263a 100644 --- a/redisinsight/api/src/modules/core/providers/server-on-premise/index.ts +++ b/redisinsight/api/src/modules/core/providers/server-on-premise/index.ts @@ -1,14 +1,6 @@ -import { ServerRepository } from 'src/modules/core/repositories/server.repository'; -import { EventEmitter2 } from '@nestjs/event-emitter'; -import { EncryptionService } from 'src/modules/core/encryption/encryption.service'; import { ServerOnPremiseService } from './server-on-premise.service'; export default { provide: 'SERVER_PROVIDER', - useFactory: ( - repository: ServerRepository, - eventEmitter: EventEmitter2, - encryptionService: EncryptionService, - ) => new ServerOnPremiseService(repository, eventEmitter, encryptionService), - inject: [ServerRepository, EventEmitter2, EncryptionService], + useClass: ServerOnPremiseService, }; diff --git a/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.spec.ts b/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.spec.ts index d947581546..7a178c5015 100644 --- a/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.spec.ts +++ b/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.spec.ts @@ -45,6 +45,7 @@ describe('ServerOnPremiseService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ EventEmitter2, + ServerOnPremiseService, { provide: getRepositoryToken(ServerEntity), useFactory: mockRepository, @@ -59,7 +60,7 @@ describe('ServerOnPremiseService', () => { serverRepository = await module.get(getRepositoryToken(ServerEntity)); eventEmitter = await module.get(EventEmitter2); encryptionService = module.get(EncryptionService); - service = new ServerOnPremiseService(serverRepository, eventEmitter, encryptionService); + service = module.get(ServerOnPremiseService); }); describe('onApplicationBootstrap', () => { @@ -67,26 +68,26 @@ describe('ServerOnPremiseService', () => { eventEmitter.emit = jest.fn(); }); it('should create server instance on first application launch', async () => { - serverRepository.findOne.mockResolvedValue(null); + serverRepository.findOneBy.mockResolvedValue(null); serverRepository.create.mockReturnValue(mockServerEntity); await service.onApplicationBootstrap(); - expect(serverRepository.findOne).toHaveBeenCalled(); + expect(serverRepository.findOneBy).toHaveBeenCalled(); expect(serverRepository.create).toHaveBeenCalled(); expect(serverRepository.save).toHaveBeenCalledWith(mockServerEntity); }); it('should not create server instance on the second application launch', async () => { - serverRepository.findOne.mockResolvedValue(mockServerEntity); + serverRepository.findOneBy.mockResolvedValue(mockServerEntity); await service.onApplicationBootstrap(); - expect(serverRepository.findOne).toHaveBeenCalled(); + expect(serverRepository.findOneBy).toHaveBeenCalled(); expect(serverRepository.create).not.toHaveBeenCalled(); expect(serverRepository.save).not.toHaveBeenCalled(); }); it('should emit APPLICATION_FIRST_START on first application launch', async () => { - serverRepository.findOne.mockResolvedValue(null); + serverRepository.findOneBy.mockResolvedValue(null); serverRepository.create.mockReturnValue(mockServerEntity); await service.onApplicationBootstrap(sessionId); @@ -106,7 +107,7 @@ describe('ServerOnPremiseService', () => { ); }); it('should emit APPLICATION_STARTED on second application launch', async () => { - serverRepository.findOne.mockResolvedValue(mockServerEntity); + serverRepository.findOneBy.mockResolvedValue(mockServerEntity); await service.onApplicationBootstrap(sessionId); @@ -128,7 +129,7 @@ describe('ServerOnPremiseService', () => { describe('getInfo', () => { it('should return server info', async () => { - serverRepository.findOne.mockResolvedValue(mockServerEntity); + serverRepository.findOneBy.mockResolvedValue(mockServerEntity); encryptionService.getAvailableEncryptionStrategies.mockResolvedValue([ EncryptionStrategy.PLAIN, EncryptionStrategy.KEYTAR, @@ -148,7 +149,7 @@ describe('ServerOnPremiseService', () => { }); }); it('should throw ServerInfoNotFoundException', async () => { - serverRepository.findOne.mockResolvedValue(null); + serverRepository.findOneBy.mockResolvedValue(null); try { await service.getInfo(); @@ -158,7 +159,7 @@ describe('ServerOnPremiseService', () => { } }); it('should throw InternalServerError', async () => { - serverRepository.findOne.mockRejectedValue(new Error('some error')); + serverRepository.findOneBy.mockRejectedValue(new Error('some error')); try { await service.getInfo(); diff --git a/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.ts b/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.ts index c48fb796a9..a0a4b606c1 100644 --- a/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.ts +++ b/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.ts @@ -4,10 +4,12 @@ import config from 'src/utils/config'; import { AppAnalyticsEvents } from 'src/constants/app-events'; import { TelemetryEvents } from 'src/constants/telemetry-events'; import { GetServerInfoResponse } from 'src/dto/server.dto'; -import { ServerRepository } from 'src/modules/core/repositories/server.repository'; import { AppType, BuildType, IServerProvider } from 'src/modules/core/models/server-provider.interface'; import { ServerInfoNotFoundException } from 'src/constants/exceptions'; import { EncryptionService } from 'src/modules/core/encryption/encryption.service'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ServerEntity } from 'src/modules/core/models/server.entity'; const SERVER_CONFIG = config.get('server'); const REDIS_STACK_CONFIG = config.get('redisStack'); @@ -17,19 +19,14 @@ export class ServerOnPremiseService implements OnApplicationBootstrap, IServerProvider { private logger = new Logger('ServerOnPremiseService'); - private repository: ServerRepository; - - private eventEmitter: EventEmitter2; - - private encryptionService: EncryptionService; - private sessionId: number; - constructor(repository, eventEmitter, encryptionService) { - this.repository = repository; - this.eventEmitter = eventEmitter; - this.encryptionService = encryptionService; - } + constructor( + @InjectRepository(ServerEntity) + private readonly repository: Repository, + private readonly eventEmitter: EventEmitter2, + private readonly encryptionService: EncryptionService, + ) {} async onApplicationBootstrap(sessionId: number = new Date().getTime()) { this.sessionId = sessionId; @@ -38,7 +35,7 @@ implements OnApplicationBootstrap, IServerProvider { private async upsertServerInfo() { this.logger.log('Checking server info.'); - let serverInfo = await this.repository.findOne(); + let serverInfo = await this.repository.findOneBy({}); if (!serverInfo) { this.logger.log('First application launch.'); // Create default server info on first application launch @@ -83,7 +80,7 @@ implements OnApplicationBootstrap, IServerProvider { public async getInfo(): Promise { this.logger.log('Getting server info.'); try { - const info = await this.repository.findOne(); + const info = await this.repository.findOneBy({}); if (!info) { return Promise.reject(new ServerInfoNotFoundException()); } diff --git a/redisinsight/api/src/modules/core/providers/settings-on-premise/index.ts b/redisinsight/api/src/modules/core/providers/settings-on-premise/index.ts index 999d4c3661..6272b8f112 100644 --- a/redisinsight/api/src/modules/core/providers/settings-on-premise/index.ts +++ b/redisinsight/api/src/modules/core/providers/settings-on-premise/index.ts @@ -1,26 +1,6 @@ -import { SettingsRepository } from 'src/modules/core/repositories/settings.repository'; -import { SettingsAnalyticsService } from 'src/modules/core/services/settings-analytics/settings-analytics.service'; -import { KeytarEncryptionStrategy } from 'src/modules/core/encryption/strategies/keytar-encryption.strategy'; import { SettingsOnPremiseService } from './settings-on-premise.service'; -import { AgreementsRepository } from '../../repositories/agreements.repository'; export default { provide: 'SETTINGS_PROVIDER', - useFactory: ( - agreementsRepository: AgreementsRepository, - settingsRepository: SettingsRepository, - analyticsService: SettingsAnalyticsService, - keytarEncryptionStrategy: KeytarEncryptionStrategy, - ) => new SettingsOnPremiseService( - agreementsRepository, - settingsRepository, - analyticsService, - keytarEncryptionStrategy, - ), - inject: [ - AgreementsRepository, - SettingsRepository, - SettingsAnalyticsService, - KeytarEncryptionStrategy, - ], + useClass: SettingsOnPremiseService, }; diff --git a/redisinsight/api/src/modules/core/providers/settings-on-premise/settings-on-premise.service.spec.ts b/redisinsight/api/src/modules/core/providers/settings-on-premise/settings-on-premise.service.spec.ts index 96c2928fec..80107e2253 100644 --- a/redisinsight/api/src/modules/core/providers/settings-on-premise/settings-on-premise.service.spec.ts +++ b/redisinsight/api/src/modules/core/providers/settings-on-premise/settings-on-premise.service.spec.ts @@ -39,11 +39,11 @@ describe('SettingsOnPremiseService', () => { let agreementsEntity: AgreementsEntity; let settingsEntity: SettingsEntity; let analyticsService: SettingsAnalyticsService; - let keytarEncryptionStrategy: KeytarEncryptionStrategy; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ + SettingsOnPremiseService, { provide: SettingsAnalyticsService, useFactory: mockSettingsAnalyticsService, @@ -76,41 +76,35 @@ describe('SettingsOnPremiseService', () => { ); settingsRepository = await module.get(getRepositoryToken(SettingsEntity)); analyticsService = await module.get(SettingsAnalyticsService); - keytarEncryptionStrategy = await module.get(KeytarEncryptionStrategy); - service = new SettingsOnPremiseService( - agreementsRepository, - settingsRepository, - analyticsService, - keytarEncryptionStrategy, - ); + service = await module.get(SettingsOnPremiseService); }); describe('onModuleInit', () => { it('should create settings and agreements instance on first application launch', async () => { - agreementsRepository.findOne.mockResolvedValue(null); + agreementsRepository.findOneBy.mockResolvedValue(null); agreementsRepository.create.mockReturnValue(agreementsEntity); - settingsRepository.findOne.mockResolvedValue(null); + settingsRepository.findOneBy.mockResolvedValue(null); settingsRepository.create.mockReturnValue(settingsEntity); await service.onModuleInit(); - expect(agreementsRepository.findOne).toHaveBeenCalled(); - expect(settingsRepository.findOne).toHaveBeenCalled(); + expect(agreementsRepository.findOneBy).toHaveBeenCalled(); + expect(settingsRepository.findOneBy).toHaveBeenCalled(); expect(agreementsRepository.create).toHaveBeenCalled(); expect(settingsRepository.create).toHaveBeenCalled(); expect(agreementsRepository.save).toHaveBeenCalledWith(agreementsEntity); expect(settingsRepository.save).toHaveBeenCalledWith(settingsEntity); }); it('should not create settings and agreements on the second application launch', async () => { - agreementsRepository.findOne.mockResolvedValue(agreementsEntity); - settingsRepository.findOne.mockResolvedValue(settingsEntity); + agreementsRepository.findOneBy.mockResolvedValue(agreementsEntity); + settingsRepository.findOneBy.mockResolvedValue(settingsEntity); await service.onModuleInit(); - expect(agreementsRepository.findOne).toHaveBeenCalled(); + expect(agreementsRepository.findOneBy).toHaveBeenCalled(); expect(agreementsRepository.create).not.toHaveBeenCalled(); expect(agreementsRepository.save).not.toHaveBeenCalled(); - expect(settingsRepository.findOne).toHaveBeenCalled(); + expect(settingsRepository.findOneBy).toHaveBeenCalled(); expect(settingsRepository.create).not.toHaveBeenCalled(); expect(settingsRepository.save).not.toHaveBeenCalled(); }); @@ -118,8 +112,8 @@ describe('SettingsOnPremiseService', () => { describe('getSettings', () => { it('should return default application settings', async () => { - agreementsRepository.findOne.mockResolvedValue(agreementsEntity); - settingsRepository.findOne.mockResolvedValue(settingsEntity); + agreementsRepository.findOneBy.mockResolvedValue(agreementsEntity); + settingsRepository.findOneBy.mockResolvedValue(settingsEntity); const result = await service.getSettings(); @@ -143,8 +137,8 @@ describe('SettingsOnPremiseService', () => { version: '1.0.0', eula: true, }); - agreementsRepository.findOne.mockResolvedValue(agreementsEntity); - settingsRepository.findOne.mockResolvedValue(settingsEntity); + agreementsRepository.findOneBy.mockResolvedValue(agreementsEntity); + settingsRepository.findOneBy.mockResolvedValue(settingsEntity); const result = await service.getSettings(); @@ -161,7 +155,7 @@ describe('SettingsOnPremiseService', () => { }); }); it('should throw InternalServerError', async () => { - agreementsRepository.findOne.mockRejectedValue(new Error('some error')); + agreementsRepository.findOneBy.mockRejectedValue(new Error('some error')); try { await service.getSettings(); @@ -180,8 +174,8 @@ describe('SettingsOnPremiseService', () => { agreementsEntity.toJSON = jest.fn().mockReturnValue({ ...mockAgreementsJSON, }); - settingsRepository.findOne.mockResolvedValue(settingsEntity); - agreementsRepository.findOne.mockResolvedValue(agreementsEntity); + settingsRepository.findOneBy.mockResolvedValue(settingsEntity); + agreementsRepository.findOneBy.mockResolvedValue(agreementsEntity); service.getSettings = jest.fn(); }); it('should update agreements and settings', async () => { @@ -248,7 +242,7 @@ describe('SettingsOnPremiseService', () => { expect(analyticsService.sendAnalyticsAgreementChange).toHaveBeenCalled(); }); it('should throw AgreementIsNotDefinedException', async () => { - agreementsRepository.findOne.mockResolvedValueOnce({ + agreementsRepository.findOneBy.mockResolvedValueOnce({ id: 1, version: null, data: null, @@ -264,7 +258,7 @@ describe('SettingsOnPremiseService', () => { const dto: UpdateSettingsDto = { agreements: mockAgreementsMap, }; - agreementsRepository.findOne.mockRejectedValue(new Error('some error')); + agreementsRepository.findOneBy.mockRejectedValue(new Error('some error')); try { await service.updateSettings(dto); diff --git a/redisinsight/api/src/modules/core/providers/settings-on-premise/settings-on-premise.service.ts b/redisinsight/api/src/modules/core/providers/settings-on-premise/settings-on-premise.service.ts index 5f3473ca5b..57a0af1048 100644 --- a/redisinsight/api/src/modules/core/providers/settings-on-premise/settings-on-premise.service.ts +++ b/redisinsight/api/src/modules/core/providers/settings-on-premise/settings-on-premise.service.ts @@ -20,8 +20,8 @@ import { AgreementsEntity, IAgreementsJSON } from 'src/modules/core/models/agree import { ISettingsJSON, SettingsEntity } from 'src/modules/core/models/settings.entity'; import { ISettingsProvider } from 'src/modules/core/models/settings-provider.interface'; import { KeytarEncryptionStrategy } from 'src/modules/core/encryption/strategies/keytar-encryption.strategy'; -import { AgreementsRepository } from '../../repositories/agreements.repository'; -import { SettingsRepository } from '../../repositories/settings.repository'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; import { SettingsAnalyticsService } from '../../services/settings-analytics/settings-analytics.service'; const REDIS_SCAN_CONFIG = config.get('redis_scan'); @@ -33,28 +33,22 @@ export class SettingsOnPremiseService implements OnModuleInit, ISettingsProvider { private logger = new Logger('SettingsOnPremiseService'); - private agreementRepository: AgreementsRepository; - - private settingsRepository: SettingsRepository; - - private analyticsService: SettingsAnalyticsService; - - private keytarEncryptionStrategy: KeytarEncryptionStrategy; - - constructor(agreementRepository, settingsRepository, analyticsService, keytarEncryptionStrategy) { - this.agreementRepository = agreementRepository; - this.settingsRepository = settingsRepository; - this.analyticsService = analyticsService; - this.keytarEncryptionStrategy = keytarEncryptionStrategy; - } + constructor( + @InjectRepository(AgreementsEntity) + private readonly agreementRepository: Repository, + @InjectRepository(SettingsEntity) + private readonly settingsRepository: Repository, + private readonly analyticsService: SettingsAnalyticsService, + private readonly keytarEncryptionStrategy: KeytarEncryptionStrategy, + ) {} async onModuleInit() { await this.upsertSettings(); } private async upsertSettings() { - const agreementsEntity = await this.agreementRepository.findOne(); - const settingsEntity = await this.settingsRepository.findOne(); + const agreementsEntity = await this.agreementRepository.findOneBy({}); + const settingsEntity = await this.settingsRepository.findOneBy({}); if (!agreementsEntity) { const agreements: AgreementsEntity = this.agreementRepository.create({}); await this.agreementRepository.save(agreements); @@ -72,10 +66,10 @@ implements OnModuleInit, ISettingsProvider { this.logger.log('Getting application settings.'); try { const agreements: IAgreementsJSON = ( - await this.agreementRepository.findOne() + await this.agreementRepository.findOneBy({}) ).toJSON(); const settings: ISettingsJSON = ( - await this.settingsRepository.findOne() + await this.settingsRepository.findOneBy({}) ).toJSON(); this.logger.log('Succeed to get application settings.'); return { @@ -102,7 +96,7 @@ implements OnModuleInit, ISettingsProvider { try { const oldSettings = await this.getSettings(); if (!isEmpty(settings)) { - const entity: SettingsEntity = await this.settingsRepository.findOne(); + const entity: SettingsEntity = await this.settingsRepository.findOneBy({}); entity.data = JSON.stringify({ ...entity.toJSON(), @@ -173,7 +167,7 @@ implements OnModuleInit, ISettingsProvider { dtoAgreements: Map = new Map(), ): Promise { this.logger.log('Updating application agreements.'); - const entity: AgreementsEntity = await this.agreementRepository.findOne(); + const entity: AgreementsEntity = await this.agreementRepository.findOneBy({}); const oldAgreements = JSON.parse(entity.data); const newValue = { ...oldAgreements, diff --git a/redisinsight/api/src/modules/core/repositories/agreements.repository.ts b/redisinsight/api/src/modules/core/repositories/agreements.repository.ts deleted file mode 100644 index fd4770a72c..0000000000 --- a/redisinsight/api/src/modules/core/repositories/agreements.repository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { EntityRepository, Repository } from 'typeorm'; -import { AgreementsEntity } from 'src/modules/core/models/agreements.entity'; - -@EntityRepository(AgreementsEntity) -export class AgreementsRepository extends Repository {} diff --git a/redisinsight/api/src/modules/core/repositories/base/base.interface.repository.ts b/redisinsight/api/src/modules/core/repositories/base/base.interface.repository.ts deleted file mode 100644 index 75fe8fb97b..0000000000 --- a/redisinsight/api/src/modules/core/repositories/base/base.interface.repository.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface BaseInterfaceRepository { - findAll(): Promise; - - create(data: T | any): Promise; - - findOneById(id: number | string): Promise; - - delete(id: string): Promise; -} diff --git a/redisinsight/api/src/modules/core/repositories/server.repository.ts b/redisinsight/api/src/modules/core/repositories/server.repository.ts deleted file mode 100644 index 8f1be24507..0000000000 --- a/redisinsight/api/src/modules/core/repositories/server.repository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { EntityRepository, Repository } from 'typeorm'; -import { ServerEntity } from 'src/modules/core/models/server.entity'; - -@EntityRepository(ServerEntity) -export class ServerRepository extends Repository {} diff --git a/redisinsight/api/src/modules/core/repositories/settings.repository.ts b/redisinsight/api/src/modules/core/repositories/settings.repository.ts deleted file mode 100644 index fad833f347..0000000000 --- a/redisinsight/api/src/modules/core/repositories/settings.repository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { EntityRepository, Repository } from 'typeorm'; -import { SettingsEntity } from 'src/modules/core/models/settings.entity'; - -@EntityRepository(SettingsEntity) -export class SettingsRepository extends Repository {} diff --git a/redisinsight/api/src/modules/core/services/certificates/ca-cert-business/ca-cert-business.service.spec.ts b/redisinsight/api/src/modules/core/services/certificates/ca-cert-business/ca-cert-business.service.spec.ts index b2ae7a4edb..bbbb341723 100644 --- a/redisinsight/api/src/modules/core/services/certificates/ca-cert-business/ca-cert-business.service.spec.ts +++ b/redisinsight/api/src/modules/core/services/certificates/ca-cert-business/ca-cert-business.service.spec.ts @@ -54,18 +54,18 @@ describe('CaCertBusinessService', () => { describe('getOneById', () => { it('should successfully find entity and decrypt field', async () => { - repository.findOne.mockResolvedValue(mockCaCertEntity); + repository.findOneBy.mockResolvedValue(mockCaCertEntity); encryptionService.decrypt.mockResolvedValueOnce(mockCaCertEntity.certificate); const result = await service.getOneById(mockCaCertEntity.id); - expect(repository.findOne).toHaveBeenCalledWith({ - where: { id: mockCaCertEntity.id }, + expect(repository.findOneBy).toHaveBeenCalledWith({ + id: mockCaCertEntity.id, }); expect(result).toEqual(mockCaCertEntity); }); it('should throw an error when certificate not found', async () => { - repository.findOne.mockResolvedValue(null); + repository.findOneBy.mockResolvedValue(null); // todo: refactor. why BadRequest? await expect(service.getOneById(mockCaCertEntity.id)).rejects.toThrow( @@ -73,13 +73,13 @@ describe('CaCertBusinessService', () => { ); }); it('should find entity and return encrypted fields to equal empty string on decrypted error', async () => { - repository.findOne.mockResolvedValue(mockCaCertEntity); + repository.findOneBy.mockResolvedValue(mockCaCertEntity); encryptionService.decrypt.mockRejectedValueOnce(new Error('Decryption error')); const result = await service.getOneById(mockCaCertEntity.id); - expect(repository.findOne).toHaveBeenCalledWith({ - where: { id: mockCaCertEntity.id }, + expect(repository.findOneBy).toHaveBeenCalledWith({ + id: mockCaCertEntity.id, }); expect(result).toEqual({ ...mockCaCertEntity, @@ -90,21 +90,21 @@ describe('CaCertBusinessService', () => { describe('create', () => { it('successfully create the certificate', async () => { - repository.findOne.mockResolvedValue(null); + repository.findOneBy.mockResolvedValue(null); repository.create.mockResolvedValueOnce(mockCaCertEntity); encryptionService.encrypt.mockResolvedValueOnce(mockEncryptResult); repository.save.mockResolvedValue(mockCaCertEntity); const result = await service.create(mockCaCertDto); - expect(repository.findOne).toHaveBeenCalledWith({ - where: { name: mockCaCertEntity.name }, + expect(repository.findOneBy).toHaveBeenCalledWith({ + name: mockCaCertEntity.name, }); expect(repository.save).toHaveBeenCalled(); expect(result).toEqual(mockCaCertEntity); }); it('certificate with this name exist', async () => { - repository.findOne.mockResolvedValue(mockCaCertEntity); + repository.findOneBy.mockResolvedValue(mockCaCertEntity); await expect(service.create(mockCaCertDto)).rejects.toThrow( BadRequestException, @@ -113,7 +113,7 @@ describe('CaCertBusinessService', () => { expect(repository.save).not.toHaveBeenCalled(); }); it('should throw and error when unable to encrypt the data', async () => { - repository.findOne.mockResolvedValueOnce(null); + repository.findOneBy.mockResolvedValueOnce(null); repository.create.mockResolvedValueOnce(mockCaCertEntity); encryptionService.encrypt.mockRejectedValueOnce(new KeytarEncryptionErrorException()); @@ -127,17 +127,17 @@ describe('CaCertBusinessService', () => { describe('delete', () => { it('successfully delete the certificate', async () => { - repository.findOne.mockResolvedValue(mockCaCertEntity); + repository.findOneBy.mockResolvedValue(mockCaCertEntity); await service.delete(mockCaCertEntity.id); - expect(repository.findOne).toHaveBeenCalledWith({ - where: { id: mockCaCertEntity.id }, + expect(repository.findOneBy).toHaveBeenCalledWith({ + id: mockCaCertEntity.id, }); expect(repository.delete).toHaveBeenCalledWith(mockCaCertEntity.id); }); it('certificate not found', async () => { - repository.findOne.mockResolvedValue(null); + repository.findOneBy.mockResolvedValue(null); await expect(service.delete(mockCaCertEntity.id)).rejects.toThrow( NotFoundException, diff --git a/redisinsight/api/src/modules/core/services/certificates/ca-cert-business/ca-cert-business.service.ts b/redisinsight/api/src/modules/core/services/certificates/ca-cert-business/ca-cert-business.service.ts index 17c7aeda72..e7b481d9f1 100644 --- a/redisinsight/api/src/modules/core/services/certificates/ca-cert-business/ca-cert-business.service.ts +++ b/redisinsight/api/src/modules/core/services/certificates/ca-cert-business/ca-cert-business.service.ts @@ -43,7 +43,7 @@ export class CaCertBusinessService { */ async getOneById(id: string): Promise { this.logger.log(`Getting CA certificate with id: ${id}.`); - const entity = await this.repository.findOne({ where: { id } }); + const entity = await this.repository.findOneBy({ id }); if (!entity) { this.logger.error(`Unable to find CA certificate with id: ${id}`); @@ -55,9 +55,7 @@ export class CaCertBusinessService { async create(certDto: CaCertDto): Promise { this.logger.log('Creating certificate.'); - const found = await this.repository.findOne({ - where: { name: certDto.name }, - }); + const found = await this.repository.findOneBy({ name: certDto.name }); if (found) { this.logger.error( `Failed to create certificate. ${ERROR_MESSAGES.CA_CERT_EXIST}. name: ${certDto.name}`, @@ -84,7 +82,7 @@ export class CaCertBusinessService { async delete(id: string): Promise { this.logger.log(`Deleting certificate. id: ${id}`); - const found = await this.repository.findOne({ where: { id } }); + const found = await this.repository.findOneBy({ id }); if (!found) { this.logger.error(`Failed to delete certificate. Not Found. id: ${id}`); throw new NotFoundException(); diff --git a/redisinsight/api/src/modules/core/services/certificates/client-cert-business/client-cert-business.service.spec.ts b/redisinsight/api/src/modules/core/services/certificates/client-cert-business/client-cert-business.service.spec.ts index df5fa62868..700244ec7d 100644 --- a/redisinsight/api/src/modules/core/services/certificates/client-cert-business/client-cert-business.service.spec.ts +++ b/redisinsight/api/src/modules/core/services/certificates/client-cert-business/client-cert-business.service.spec.ts @@ -56,33 +56,33 @@ describe('ClientCertBusinessService', () => { describe('getOneById', () => { it('successfully found the certificate', async () => { - repository.findOne.mockResolvedValue(mockClientCertEntity); + repository.findOneBy.mockResolvedValue(mockClientCertEntity); encryptionService.decrypt .mockResolvedValueOnce(mockClientCertEntity.certificate) .mockResolvedValueOnce(mockClientCertEntity.key); const result = await service.getOneById(mockClientCertEntity.id); - expect(repository.findOne).toHaveBeenCalledWith({ - where: { id: mockClientCertEntity.id }, + expect(repository.findOneBy).toHaveBeenCalledWith({ + id: mockClientCertEntity.id, }); expect(result).toEqual(mockClientCertEntity); }); it('certificate not found', async () => { - repository.findOne.mockResolvedValue(null); + repository.findOneBy.mockResolvedValue(null); await expect(service.getOneById(mockClientCertEntity.id)).rejects.toThrow(BadRequestException); }); it('should find entity and return encrypted fields to equal empty string on decrypted error', async () => { - repository.findOne.mockResolvedValue(mockClientCertEntity); + repository.findOneBy.mockResolvedValue(mockClientCertEntity); encryptionService.decrypt .mockRejectedValueOnce(new Error('Decryption error')) .mockRejectedValueOnce(new Error('Decryption error')); const result = await service.getOneById(mockClientCertEntity.id); - expect(repository.findOne).toHaveBeenCalledWith({ - where: { id: mockClientCertEntity.id }, + expect(repository.findOneBy).toHaveBeenCalledWith({ + id: mockClientCertEntity.id, }); expect(result).toEqual({ ...mockClientCertEntity, @@ -94,7 +94,7 @@ describe('ClientCertBusinessService', () => { describe('create', () => { it('successfully create the certificate', async () => { - repository.findOne.mockResolvedValue(null); + repository.findOneBy.mockResolvedValue(null); repository.create.mockResolvedValueOnce(mockClientCertEntity); encryptionService.encrypt .mockResolvedValueOnce(mockEncryptResult) @@ -103,14 +103,14 @@ describe('ClientCertBusinessService', () => { const result = await service.create(mockClientCertDto); - expect(repository.findOne).toHaveBeenCalledWith({ - where: { name: mockClientCertEntity.name }, + expect(repository.findOneBy).toHaveBeenCalledWith({ + name: mockClientCertEntity.name, }); expect(repository.save).toHaveBeenCalled(); expect(result).toEqual(mockClientCertEntity); }); it('certificate with this name exist', async () => { - repository.findOne.mockResolvedValue(mockClientCertEntity); + repository.findOneBy.mockResolvedValue(mockClientCertEntity); await expect(service.create(mockClientCertDto)).rejects.toThrow( BadRequestException, @@ -118,7 +118,7 @@ describe('ClientCertBusinessService', () => { expect(repository.save).not.toHaveBeenCalled(); }); it('should throw an error when unable to encrypt the data', async () => { - repository.findOne.mockResolvedValueOnce(null); + repository.findOneBy.mockResolvedValueOnce(null); repository.create.mockResolvedValueOnce(mockClientCertEntity); encryptionService.encrypt.mockRejectedValueOnce(new KeytarEncryptionErrorException()); @@ -132,17 +132,17 @@ describe('ClientCertBusinessService', () => { describe('delete', () => { it('successfully delete the certificate', async () => { - repository.findOne.mockResolvedValue(mockClientCertEntity); + repository.findOneBy.mockResolvedValue(mockClientCertEntity); await service.delete(mockClientCertEntity.id); - expect(repository.findOne).toHaveBeenCalledWith({ - where: { id: mockClientCertEntity.id }, + expect(repository.findOneBy).toHaveBeenCalledWith({ + id: mockClientCertEntity.id, }); expect(repository.delete).toHaveBeenCalledWith(mockClientCertEntity.id); }); it('certificate not found', async () => { - repository.findOne.mockResolvedValue(null); + repository.findOneBy.mockResolvedValue(null); await expect(service.delete(mockClientCertEntity.id)).rejects.toThrow( NotFoundException, diff --git a/redisinsight/api/src/modules/core/services/certificates/client-cert-business/client-cert-business.service.ts b/redisinsight/api/src/modules/core/services/certificates/client-cert-business/client-cert-business.service.ts index 294caa9a8a..12f6392dc8 100644 --- a/redisinsight/api/src/modules/core/services/certificates/client-cert-business/client-cert-business.service.ts +++ b/redisinsight/api/src/modules/core/services/certificates/client-cert-business/client-cert-business.service.ts @@ -43,7 +43,7 @@ export class ClientCertBusinessService { */ async getOneById(id: string): Promise { this.logger.log(`Getting client certificate with id: ${id}.`); - const entity = await this.repository.findOne({ where: { id } }); + const entity = await this.repository.findOneBy({ id }); if (!entity) { this.logger.error(`Unable to find client certificate with id: ${id}`); @@ -55,9 +55,7 @@ export class ClientCertBusinessService { async create(certDto: ClientCertPairDto): Promise { this.logger.log('Creating certificate.'); - const found = await this.repository.findOne({ - where: { name: certDto.name }, - }); + const found = await this.repository.findOneBy({ name: certDto.name }); if (found) { this.logger.error( @@ -90,7 +88,7 @@ export class ClientCertBusinessService { async delete(id: string): Promise { this.logger.log(`Deleting client-certificate. id: ${id}`); - const found = await this.repository.findOne({ where: { id } }); + const found = await this.repository.findOneBy({ id }); if (!found) { this.logger.error( diff --git a/redisinsight/api/src/modules/core/services/redis/redis.service.spec.ts b/redisinsight/api/src/modules/core/services/redis/redis.service.spec.ts index 3260a4850a..e2c1602c41 100644 --- a/redisinsight/api/src/modules/core/services/redis/redis.service.spec.ts +++ b/redisinsight/api/src/modules/core/services/redis/redis.service.spec.ts @@ -130,7 +130,7 @@ describe('RedisService', () => { }); it('should select redis database by number', async () => { const mockClient = new Redis(); - mockClient.send_command = jest.fn(); + mockClient.call = jest.fn(); const dto = convertEntityToDto(mockStandaloneDatabaseEntity); service.createStandaloneClient = jest.fn().mockResolvedValue(mockClient); diff --git a/redisinsight/api/src/modules/core/services/redis/redis.service.ts b/redisinsight/api/src/modules/core/services/redis/redis.service.ts index ad40dd2ff9..44e7be2ac5 100644 --- a/redisinsight/api/src/modules/core/services/redis/redis.service.ts +++ b/redisinsight/api/src/modules/core/services/redis/redis.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConnectionOptions, SecureContextOptions } from 'tls'; -import * as Redis from 'ioredis'; -import IORedis, { RedisOptions } from 'ioredis'; +import Redis from 'ioredis'; +import { RedisOptions, Cluster } from 'ioredis'; import { find, findIndex, isEmpty, isNil, omitBy, remove, } from 'lodash'; @@ -61,7 +61,7 @@ export class RedisService { appTool: AppTool, useRetry: boolean, connectionName: string = CONNECTION_NAME_GLOBAL_PREFIX, - ): Promise { + ): Promise { const config = await this.getRedisConnectionConfig(options); return new Promise((resolve, reject) => { @@ -95,7 +95,7 @@ export class RedisService { nodes: IRedisClusterNodeAddress[], useRetry: boolean = false, connectionName: string = CONNECTION_NAME_GLOBAL_PREFIX, - ): Promise { + ): Promise { const config = await this.getRedisConnectionConfig(options); return new Promise((resolve, reject) => { try { @@ -106,7 +106,6 @@ export class RedisService { showFriendlyErrorStack: true, maxRetriesPerRequest: REDIS_CLIENTS_CONFIG.maxRetriesPerRequest, connectionName, - retryStrategy: useRetry ? this.retryStrategy : () => undefined, }, }); cluster.on('error', (e): void => { @@ -129,7 +128,7 @@ export class RedisService { appTool: AppTool, useRetry: boolean = false, connectionName: string = CONNECTION_NAME_GLOBAL_PREFIX, - ): Promise { + ): Promise { const { username, password, sentinelMaster, tls, db, } = options; @@ -179,7 +178,7 @@ export class RedisService { databaseDto: DatabaseInstanceResponse, tool = AppTool.Common, connectionName?, - ): Promise { + ): Promise { const database = databaseDto; Object.keys(database).forEach((key: string) => { if (database[key] === null) { @@ -205,7 +204,7 @@ export class RedisService { return client; } - public isClientConnected(client: IORedis.Redis | IORedis.Cluster): boolean { + public isClientConnected(client: Redis | Cluster): boolean { try { return client.status === 'ready'; } catch (e) { @@ -277,11 +276,11 @@ export class RedisService { private async getRedisConnectionConfig( options: ConnectionOptionsDto, - ): Promise { + ): Promise { const { host, port, password, username, tls, db, } = options; - const config: IORedis.RedisOptions = { + const config: RedisOptions = { host, port, username, password, db, }; if (tls) { diff --git a/redisinsight/api/src/modules/profiler/interfaces/monitor-data.interface.ts b/redisinsight/api/src/modules/profiler/interfaces/monitor-data.interface.ts index b378b465e5..a056d96aa9 100644 --- a/redisinsight/api/src/modules/profiler/interfaces/monitor-data.interface.ts +++ b/redisinsight/api/src/modules/profiler/interfaces/monitor-data.interface.ts @@ -1,4 +1,4 @@ -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; export interface IMonitorData { time: string; diff --git a/redisinsight/api/src/modules/profiler/interfaces/shard-observer.interface.ts b/redisinsight/api/src/modules/profiler/interfaces/shard-observer.interface.ts index 8a2d626dd9..84a7b83c08 100644 --- a/redisinsight/api/src/modules/profiler/interfaces/shard-observer.interface.ts +++ b/redisinsight/api/src/modules/profiler/interfaces/shard-observer.interface.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'events'; -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; export interface IShardObserver extends EventEmitter { disconnect(): void; diff --git a/redisinsight/api/src/modules/profiler/models/redis.observer.spec.ts b/redisinsight/api/src/modules/profiler/models/redis.observer.spec.ts index 1c282e816d..b4c91c3d6c 100644 --- a/redisinsight/api/src/modules/profiler/models/redis.observer.spec.ts +++ b/redisinsight/api/src/modules/profiler/models/redis.observer.spec.ts @@ -1,4 +1,4 @@ -import * as Redis from 'ioredis'; +import Redis from 'ioredis'; import { RedisObserver } from 'src/modules/profiler/models/redis.observer'; import { RedisObserverStatus } from 'src/modules/profiler/constants'; import { @@ -15,7 +15,7 @@ nodeClient.monitor = jest.fn(); nodeClient.status = 'ready'; nodeClient.disconnect = jest.fn(); nodeClient.duplicate = jest.fn(); -nodeClient.send_command = jest.fn(); +nodeClient.call = jest.fn(); const mockClusterNode1 = nodeClient; const mockClusterNode2 = nodeClient; @@ -71,7 +71,7 @@ describe('RedisObserver', () => { }); it('should subscribe to a standalone', async () => { - nodeClient.send_command.mockResolvedValue('OK'); + nodeClient.call.mockResolvedValue('OK'); await redisObserver.init(getRedisClientFn); await redisObserver.subscribe(mockProfilerClient); @@ -183,7 +183,7 @@ describe('RedisObserver', () => { describe('connect', () => { beforeEach(async () => { - nodeClient.send_command.mockResolvedValue('OK'); + nodeClient.call.mockResolvedValue('OK'); nodeClient.duplicate.mockReturnValue(nodeClient); nodeClient.monitor.mockReturnValue(mockRedisShardObserver); }); @@ -197,7 +197,7 @@ describe('RedisObserver', () => { }); it('connect fail due to NOPERM', (done) => { - nodeClient.send_command.mockRejectedValueOnce(NO_PERM_ERROR); + nodeClient.call.mockRejectedValueOnce(NO_PERM_ERROR); redisObserver.init(getRedisClientFn); redisObserver.on('connect_error', (e) => { expect(redisObserver['shardsObservers']).toEqual([]); @@ -208,7 +208,7 @@ describe('RedisObserver', () => { }); it('connect fail due an error', (done) => { - nodeClient.send_command.mockRejectedValueOnce(new Error('some error')); + nodeClient.call.mockRejectedValueOnce(new Error('some error')); redisObserver.init(getRedisClientFn); redisObserver.on('connect_error', (e) => { expect(e).toBeInstanceOf(ServiceUnavailableException); diff --git a/redisinsight/api/src/modules/profiler/models/redis.observer.ts b/redisinsight/api/src/modules/profiler/models/redis.observer.ts index a02bb25469..f4a423c9f9 100644 --- a/redisinsight/api/src/modules/profiler/models/redis.observer.ts +++ b/redisinsight/api/src/modules/profiler/models/redis.observer.ts @@ -1,4 +1,4 @@ -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; import { ForbiddenException, Logger, ServiceUnavailableException } from '@nestjs/common'; import { RedisErrorCodes } from 'src/constants'; import { ProfilerClient } from 'src/modules/profiler/models/profiler.client'; @@ -211,11 +211,11 @@ export class RedisObserver extends EventEmitter2 { const duplicate = redis.duplicate({ ...redis.options, monitor: false, - lazyLoading: false, + lazyConnect: false, connectionName: `redisinsight-monitor-perm-check-${Math.random()}`, }); - await duplicate.send_command('monitor'); + await duplicate.call('monitor'); duplicate.disconnect(); return true; diff --git a/redisinsight/api/src/modules/profiler/providers/redis-observer.provider.spec.ts b/redisinsight/api/src/modules/profiler/providers/redis-observer.provider.spec.ts index 9468def64c..7b74aaae87 100644 --- a/redisinsight/api/src/modules/profiler/providers/redis-observer.provider.spec.ts +++ b/redisinsight/api/src/modules/profiler/providers/redis-observer.provider.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import * as Redis from 'ioredis'; +import Redis from 'ioredis'; import { mockLogFile, mockRedisShardObserver, MockType, @@ -15,7 +15,7 @@ nodeClient.monitor = jest.fn(); nodeClient.status = 'ready'; nodeClient.disconnect = jest.fn(); nodeClient.duplicate = jest.fn(); -nodeClient.send_command = jest.fn(); +nodeClient.call = jest.fn(); describe('RedisObserverProvider', () => { let service: RedisObserverProvider; @@ -52,7 +52,7 @@ describe('RedisObserverProvider', () => { redisService.getClientInstance.mockReturnValue({ ...mockRedisClientInstance, client: nodeClient }); redisService.isClientConnected.mockReturnValue(true); databaseService.connectToInstance.mockResolvedValue(nodeClient); - nodeClient.send_command.mockResolvedValue('OK'); + nodeClient.call.mockResolvedValue('OK'); nodeClient.duplicate.mockReturnValue(nodeClient); nodeClient.monitor.mockReturnValue(mockRedisShardObserver); }); diff --git a/redisinsight/api/src/modules/profiler/providers/redis-observer.provider.ts b/redisinsight/api/src/modules/profiler/providers/redis-observer.provider.ts index 3323a4a2be..70d0c00fe2 100644 --- a/redisinsight/api/src/modules/profiler/providers/redis-observer.provider.ts +++ b/redisinsight/api/src/modules/profiler/providers/redis-observer.provider.ts @@ -1,4 +1,4 @@ -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common'; import { RedisObserver } from 'src/modules/profiler/models/redis.observer'; import { RedisObserverStatus } from 'src/modules/profiler/constants'; diff --git a/redisinsight/api/src/modules/pub-sub/model/redis-client.spec.ts b/redisinsight/api/src/modules/pub-sub/model/redis-client.spec.ts index 4c3916e957..15194ee58d 100644 --- a/redisinsight/api/src/modules/pub-sub/model/redis-client.spec.ts +++ b/redisinsight/api/src/modules/pub-sub/model/redis-client.spec.ts @@ -1,4 +1,4 @@ -import * as Redis from 'ioredis'; +import Redis from 'ioredis'; import { RedisClient } from 'src/modules/pub-sub/model/redis-client'; import { RedisClientEvents, RedisClientStatus } from 'src/modules/pub-sub/constants'; diff --git a/redisinsight/api/src/modules/pub-sub/model/user-session.spec.ts b/redisinsight/api/src/modules/pub-sub/model/user-session.spec.ts index 9f2cacbcf1..29bb7a2ae3 100644 --- a/redisinsight/api/src/modules/pub-sub/model/user-session.spec.ts +++ b/redisinsight/api/src/modules/pub-sub/model/user-session.spec.ts @@ -1,4 +1,4 @@ -import * as Redis from 'ioredis'; +import Redis from 'ioredis'; import { mockSocket } from 'src/__mocks__'; import { UserSession } from 'src/modules/pub-sub/model/user-session'; import { UserClient } from 'src/modules/pub-sub/model/user-client'; diff --git a/redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts b/redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts index f3e3be8bf8..8ed241fb0a 100644 --- a/redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts +++ b/redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import * as Redis from 'ioredis'; +import Redis from 'ioredis'; import { // mockLogFile, // mockRedisShardObserver, diff --git a/redisinsight/api/src/modules/shared/services/base/command.telemetry.base.service.spec.ts b/redisinsight/api/src/modules/shared/services/base/command.telemetry.base.service.spec.ts new file mode 100644 index 0000000000..b4f9091ba7 --- /dev/null +++ b/redisinsight/api/src/modules/shared/services/base/command.telemetry.base.service.spec.ts @@ -0,0 +1,107 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { CommandType } from 'src/constants'; +import { CommandTelemetryBaseService } from 'src/modules/shared/services/base/command.telemetry.base.service'; +import { CommandsService } from 'src/modules/commands/commands.service'; +import { MockType } from 'src/__mocks__'; + +class Service extends CommandTelemetryBaseService { + constructor( + protected eventEmitter: EventEmitter2, + protected readonly commandsService: CommandsService, + ) { + super(eventEmitter, commandsService); + } +} + +const mockCommandsService = { + getCommandsGroups: jest.fn(), +}; + +describe('CommandTelemetryBaseService', () => { + let service; + let eventEmitter: EventEmitter2; + let commandsService: MockType; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: EventEmitter2, + useFactory: () => ({ + emit: jest.fn(), + }), + }, + { + provide: CommandsService, + useFactory: () => mockCommandsService, + }, + ], + }).compile(); + + eventEmitter = await module.get(EventEmitter2); + commandsService = await module.get(CommandsService); + service = new Service(eventEmitter, commandsService as unknown as CommandsService); + commandsService.getCommandsGroups.mockResolvedValue({ + main: { + SET: { + summary: 'Set the string value of a key', + since: '1.0.0', + group: 'string', + complexity: 'O(1)', + acl_categories: [ + '@write', + '@string', + '@slow', + ], + }, + }, + redisbloom: { + 'BF.RESERVE': { + summary: 'Creates a new Bloom Filter', + complexity: 'O(1)', + since: '1.0.0', + group: 'bf', + }, + }, + custommodule: { + 'CUSTOM.COMMAND': { + summary: 'Creates a new Bloom Filter', + complexity: 'O(1)', + since: '1.0.0', + }, + }, + }); + }); + + describe('getCommandAdditionalInfo', () => { + it('should get command additional info (core module)', async () => { + expect(await service.getCommandAdditionalInfo('set')).toEqual({ + commandType: CommandType.Core, + moduleName: 'n/a', + capability: 'string', + }); + }); + it('should get command additional info (known module)', async () => { + expect(await service.getCommandAdditionalInfo('BF.RESErve')).toEqual({ + commandType: CommandType.Module, + moduleName: 'redisbloom', + capability: 'bf', + }); + }); + it('should get command additional info (known module w\\o cap.)', async () => { + expect(await service.getCommandAdditionalInfo('CUSTOM.COMMAND')).toEqual({ + commandType: CommandType.Module, + moduleName: 'custommodule', + capability: 'n/a', + }); + }); + it('should get command additional info (custom module)', async () => { + expect(await service.getCommandAdditionalInfo('some.cmd')).toEqual({ + commandType: CommandType.Module, + moduleName: 'custom', + capability: 'n/a', + }); + }); + }); +}); diff --git a/redisinsight/api/src/modules/shared/services/base/command.telemetry.base.service.ts b/redisinsight/api/src/modules/shared/services/base/command.telemetry.base.service.ts new file mode 100644 index 0000000000..e19ba2f501 --- /dev/null +++ b/redisinsight/api/src/modules/shared/services/base/command.telemetry.base.service.ts @@ -0,0 +1,43 @@ +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { CommandType } from 'src/constants'; +import { TelemetryBaseService } from 'src/modules/shared/services/base/telemetry.base.service'; +import { CommandsService } from 'src/modules/commands/commands.service'; +import { forEach } from 'lodash'; + +export abstract class CommandTelemetryBaseService extends TelemetryBaseService { + protected constructor( + protected eventEmitter: EventEmitter2, + protected readonly commandsService: CommandsService, + ) { + super(eventEmitter); + } + + protected async getCommandAdditionalInfo(command: string): Promise { + try { + const result = { + commandType: CommandType.Module, + moduleName: 'custom', + capability: 'n/a', + }; + + if (!command) { + return {}; + } + + const modules = await this.commandsService.getCommandsGroups(); + + const commandToFind = command.toUpperCase(); + forEach(modules, (module, moduleName) => { + if (module[commandToFind]) { + result.commandType = moduleName === 'main' ? CommandType.Core : CommandType.Module; + result.moduleName = moduleName === 'main' ? 'n/a' : moduleName; + result.capability = module[commandToFind]?.group ? module[commandToFind]?.group : 'n/a'; + } + }); + + return result; + } catch (e) { + return {}; + } + } +} diff --git a/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.ts b/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.ts index 3a8a6e3109..db7a45c5e8 100644 --- a/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.ts +++ b/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.ts @@ -1,4 +1,4 @@ -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; import { v4 as uuidv4 } from 'uuid'; import { AppTool, ReplyError, IRedisConsumer } from 'src/models'; import { diff --git a/redisinsight/api/src/modules/shared/services/base/redis-tool.service.ts b/redisinsight/api/src/modules/shared/services/base/redis-tool.service.ts index f181b5ba52..925e44856c 100644 --- a/redisinsight/api/src/modules/shared/services/base/redis-tool.service.ts +++ b/redisinsight/api/src/modules/shared/services/base/redis-tool.service.ts @@ -1,6 +1,6 @@ import { Logger } from '@nestjs/common'; import * as Redis from 'ioredis'; -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; import { v4 as uuidv4 } from 'uuid'; import { AppTool, ReplyError } from 'src/models'; import ERROR_MESSAGES from 'src/constants/error-messages'; @@ -47,7 +47,7 @@ export class RedisToolService extends RedisConsumerAbstractService { clientOptions: IFindRedisClientInstanceByOptions, toolCommand: string, args: Array, - replyEncoding?: string, + replyEncoding?: BufferEncoding, ): Promise { const client = await this.getRedisClient(clientOptions); this.logger.log(`Execute command '${toolCommand}', connectionName: ${getConnectionName(client)}`); @@ -64,7 +64,7 @@ export class RedisToolService extends RedisConsumerAbstractService { toolCommand: string, args: Array, nodeRole: ClusterNodeRole, - replyEncoding?: string, + replyEncoding?: BufferEncoding, ): Promise { const [command, ...commandArgs] = toolCommand.split(' '); const nodes: IORedis.Redis[] = await this.getClusterNodes( @@ -108,7 +108,7 @@ export class RedisToolService extends RedisConsumerAbstractService { args: Array, nodeRole: ClusterNodeRole, nodeAddress: string, - replyEncoding?: string, + replyEncoding?: BufferEncoding, ): Promise { const [command, ...commandArgs] = toolCommand.split(' '); const nodes: IORedis.Redis[] = await this.getClusterNodes( diff --git a/redisinsight/api/src/modules/shared/services/configuration-business/configuration-business.service.spec.ts b/redisinsight/api/src/modules/shared/services/configuration-business/configuration-business.service.spec.ts index c41ce9bc88..696818f47a 100644 --- a/redisinsight/api/src/modules/shared/services/configuration-business/configuration-business.service.spec.ts +++ b/redisinsight/api/src/modules/shared/services/configuration-business/configuration-business.service.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import * as Redis from 'ioredis'; +import Redis from 'ioredis'; import { when } from 'jest-when'; import { IRedisClusterNode, RedisClusterNodeLinkState, ReplyError } from 'src/models'; import { @@ -18,8 +18,10 @@ import { ConfigurationBusinessService } from './configuration-business.service'; const mockClient = Object.create(Redis.prototype); const mockClusterNode1 = Object.create(Redis.prototype); const mockClusterNode2 = Object.create(Redis.prototype); -mockClusterNode1.send_command = jest.fn(); -mockClusterNode2.send_command = jest.fn(); +mockClusterNode1.call = jest.fn(); +mockClusterNode2.call = jest.fn(); +mockClusterNode1.info = jest.fn(); +mockClusterNode2.info = jest.fn(); const mockCluster = Object.create(Redis.Cluster.prototype); const mockRedisClusterNodesDto: IRedisClusterNode[] = [ @@ -86,13 +88,15 @@ describe('ConfigurationBusinessService', () => { service = await module.get( ConfigurationBusinessService, ); - mockClient.send_command = jest.fn(); + mockClient.call = jest.fn(); + mockClient.info = jest.fn(); + mockClient.cluster = jest.fn(); }); describe('checkClusterConnection', () => { it('cluster connection ok', async () => { - when(mockClient.send_command) - .calledWith('cluster', ['info']) + when(mockClient.cluster) + .calledWith('INFO') .mockResolvedValue(mockRedisClusterOkInfoResponse); const result = await service.checkClusterConnection(mockClient); @@ -101,8 +105,8 @@ describe('ConfigurationBusinessService', () => { }); it('cluster connection ok', async () => { - when(mockClient.send_command) - .calledWith('cluster', ['info']) + when(mockClient.cluster) + .calledWith('INFO') .mockResolvedValue(mockRedisClusterFailInfoResponse); const result = await service.checkClusterConnection(mockClient); @@ -115,7 +119,7 @@ describe('ConfigurationBusinessService', () => { message: 'ERR This instance has cluster support disabled', command: 'CLUSTER', }; - when(mockClient.send_command) + when(mockClient.call) .calledWith('cluster', ['info']) .mockRejectedValue(replyError); @@ -127,7 +131,7 @@ describe('ConfigurationBusinessService', () => { describe('checkSentinelConnection', () => { it('sentinel connection ok', async () => { - when(mockClient.send_command) + when(mockClient.call) .calledWith('sentinel', ['masters']) .mockResolvedValue(mockRedisSentinelMasterResponse); @@ -141,7 +145,7 @@ describe('ConfigurationBusinessService', () => { message: 'Unknown command `sentinel`', command: 'SENTINEL', }; - when(mockClient.send_command) + when(mockClient.call) .calledWith('sentinel', ['masters']) .mockRejectedValue(replyError); @@ -153,7 +157,7 @@ describe('ConfigurationBusinessService', () => { describe('getRedisClusterNodes', () => { it('should return nodes in a defined format', async () => { - when(mockClient.send_command) + when(mockClient.call) .calledWith('cluster', ['nodes']) .mockResolvedValue(mockRedisClusterNodesResponse); @@ -167,7 +171,7 @@ describe('ConfigurationBusinessService', () => { message: 'ERR This instance has cluster support disabled', command: 'CLUSTER', }; - when(mockClient.send_command) + when(mockClient.call) .calledWith('cluster', ['nodes']) .mockRejectedValue(replyError); @@ -182,7 +186,7 @@ describe('ConfigurationBusinessService', () => { describe('getDatabasesCount', () => { it('get databases count', async () => { - when(mockClient.send_command) + when(mockClient.call) .calledWith('config', ['get', 'databases']) .mockResolvedValue(['databases', '16']); @@ -191,7 +195,7 @@ describe('ConfigurationBusinessService', () => { expect(result).toBe(16); }); it('get databases count for limited redis db', async () => { - when(mockClient.send_command) + when(mockClient.call) .calledWith('config', ['get', 'databases']) .mockResolvedValue([]); @@ -200,7 +204,7 @@ describe('ConfigurationBusinessService', () => { expect(result).toBe(1); }); it('failed to get databases config', async () => { - when(mockClient.send_command) + when(mockClient.call) .calledWith('config', ['get', 'databases']) .mockRejectedValue(new Error("unknown command 'config'")); @@ -212,13 +216,13 @@ describe('ConfigurationBusinessService', () => { describe('getLoadedModulesList', () => { it('get modules by using MODULE LIST command', async () => { - when(mockClient.send_command) + when(mockClient.call) .calledWith('module', ['list']) .mockResolvedValue(mockRedisModuleList); const result = await service.getLoadedModulesList(mockClient); - expect(mockClient.send_command).not.toHaveBeenCalledWith('command', expect.anything()); + expect(mockClient.call).not.toHaveBeenCalledWith('command', expect.anything()); expect(result).toEqual([ { name: RedisModules.RedisAI, version: 10000, semanticVersion: '1.0.0' }, { name: RedisModules.RedisGraph, version: 10000, semanticVersion: '1.0.0' }, @@ -231,10 +235,10 @@ describe('ConfigurationBusinessService', () => { ]); }); it('detect all modules by using COMMAND INFO command', async () => { - when(mockClient.send_command) + when(mockClient.call) .calledWith('module', ['list']) .mockRejectedValue(mockUnknownCommandModule); - when(mockClient.send_command) + when(mockClient.call) .calledWith('command', expect.anything()) .mockResolvedValue([ null, @@ -243,7 +247,7 @@ describe('ConfigurationBusinessService', () => { const result = await service.getLoadedModulesList(mockClient); - expect(mockClient.send_command).toHaveBeenCalledTimes(REDIS_MODULES_COMMANDS.size + 1); + expect(mockClient.call).toHaveBeenCalledTimes(REDIS_MODULES_COMMANDS.size + 1); expect(result).toEqual([ { name: RedisModules.RedisAI }, { name: RedisModules.RedisGraph }, @@ -255,25 +259,25 @@ describe('ConfigurationBusinessService', () => { ]); }); it('detect only RediSearch module by using COMMAND INFO command', async () => { - when(mockClient.send_command) + when(mockClient.call) .calledWith('module', ['list']) .mockRejectedValue(mockUnknownCommandModule); - when(mockClient.send_command) + when(mockClient.call) .calledWith('command', ['info', ...REDIS_MODULES_COMMANDS.get(RedisModules.RediSearch)]) .mockResolvedValue([['FT.INFO', -1, ['readonly'], 0, 0, -1, []]]); const result = await service.getLoadedModulesList(mockClient); - expect(mockClient.send_command).toHaveBeenCalledTimes(REDIS_MODULES_COMMANDS.size + 1); + expect(mockClient.call).toHaveBeenCalledTimes(REDIS_MODULES_COMMANDS.size + 1); expect(result).toEqual([ { name: RedisModules.RediSearch }, ]); }); it('should return empty array if MODULE LIST and COMMAND command not allowed', async () => { - when(mockClient.send_command) + when(mockClient.call) .calledWith('module', ['list']) .mockRejectedValue(mockUnknownCommandModule); - when(mockClient.send_command) + when(mockClient.call) .calledWith('command', expect.anything()) .mockRejectedValue(mockUnknownCommandModule); @@ -288,8 +292,8 @@ describe('ConfigurationBusinessService', () => { service.getDatabasesCount = jest.fn().mockResolvedValue(16); }); it('get general info for redis standalone', async () => { - when(mockClient.send_command) - .calledWith('info') + when(mockClient.info) + .calledWith() .mockResolvedValue(mockStandaloneRedisInfoReply); const result = await service.getRedisGeneralInfo(mockClient); @@ -301,7 +305,7 @@ describe('ConfigurationBusinessService', () => { }\r\n${ mockRedisClientsInfoResponse }\r\n`; - when(mockClient.send_command).calledWith('info').mockResolvedValue(reply); + when(mockClient.info).calledWith().mockResolvedValue(reply); const result = await service.getRedisGeneralInfo(mockClient); @@ -317,11 +321,11 @@ describe('ConfigurationBusinessService', () => { mockCluster.nodes = jest .fn() .mockReturnValue([mockClusterNode1, mockClusterNode2]); - when(mockClusterNode1.send_command) - .calledWith('info') + when(mockClusterNode1.info) + .calledWith() .mockResolvedValue(mockStandaloneRedisInfoReply); - when(mockClusterNode2.send_command) - .calledWith('info') + when(mockClusterNode2.info) + .calledWith() .mockResolvedValue(mockStandaloneRedisInfoReply); const result = await service.getRedisGeneralInfo(mockCluster); diff --git a/redisinsight/api/src/modules/shared/services/configuration-business/configuration-business.service.ts b/redisinsight/api/src/modules/shared/services/configuration-business/configuration-business.service.ts index b3bc1b58d3..378acd333c 100644 --- a/redisinsight/api/src/modules/shared/services/configuration-business/configuration-business.service.ts +++ b/redisinsight/api/src/modules/shared/services/configuration-business/configuration-business.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; import { get, isNil } from 'lodash'; import { convertBulkStringsToObject, @@ -20,7 +20,7 @@ import { RedisModuleDto } from 'src/modules/instances/dto/database-instance.dto' export class ConfigurationBusinessService { public async checkClusterConnection(client: IORedis.Redis): Promise { try { - const reply = await client.send_command('cluster', ['info']); + const reply = await client.cluster('INFO'); const clusterInfo: IRedisClusterInfo = convertBulkStringsToObject(reply); return clusterInfo?.cluster_state === 'ok'; } catch (e) { @@ -32,7 +32,7 @@ export class ConfigurationBusinessService { client: IORedis.Redis, ): Promise { try { - await client.send_command('sentinel', ['masters']); + await client.call('sentinel', ['masters']); return true; } catch (e) { return false; @@ -42,7 +42,7 @@ export class ConfigurationBusinessService { public async getRedisClusterNodes( client: IORedis.Redis, ): Promise { - const nodes: any = await client.send_command('cluster', ['nodes']); + const nodes: any = await client.call('cluster', ['nodes']); return parseClusterNodes(nodes); } @@ -57,7 +57,7 @@ export class ConfigurationBusinessService { public async getDatabasesCount(client: any): Promise { try { - const reply = await client.send_command('config', ['get', 'databases']); + const reply = await client.call('config', ['get', 'databases']); return reply.length ? parseInt(reply[1], 10) : 1; } catch (e) { return 1; @@ -66,7 +66,7 @@ export class ConfigurationBusinessService { public async getLoadedModulesList(client: any): Promise { try { - const reply = await client.send_command('module', ['list']); + const reply = await client.call('module', ['list']); const modules = reply.map((module: any[]) => convertStringsArrayToObject(module)); return this.convertRedisModules(modules); } catch (e) { @@ -79,7 +79,7 @@ export class ConfigurationBusinessService { client: IORedis.Redis, ): Promise { const info = convertRedisInfoReplyToObject( - await client.send_command('info'), + await client.info(), ); const serverInfo = info['server']; const memoryInfo = info['memory']; @@ -160,7 +160,7 @@ export class ConfigurationBusinessService { const modules: RedisModuleDto[] = []; await Promise.all(Array.from(REDIS_MODULES_COMMANDS, async ([moduleName, commands]) => { try { - let commandsInfo = await client.send_command('command', ['info', ...commands]); + let commandsInfo = await client.call('command', ['info', ...commands]); commandsInfo = commandsInfo.filter((info) => !isNil(info)); if (commandsInfo.length) { modules.push({ name: moduleName }); diff --git a/redisinsight/api/src/modules/shared/services/instances-business/auto-discovery.service.ts b/redisinsight/api/src/modules/shared/services/instances-business/auto-discovery.service.ts index 950d231f31..bb4ab6d98e 100644 --- a/redisinsight/api/src/modules/shared/services/instances-business/auto-discovery.service.ts +++ b/redisinsight/api/src/modules/shared/services/instances-business/auto-discovery.service.ts @@ -80,7 +80,7 @@ export class AutoDiscoveryService implements OnModuleInit { ); const info = convertRedisInfoReplyToObject( - await client.send_command('info'), + await client.info(), ); if (info?.server?.redis_mode === 'standalone') { diff --git a/redisinsight/api/src/modules/shared/services/instances-business/instances-business.service.ts b/redisinsight/api/src/modules/shared/services/instances-business/instances-business.service.ts index e9dde61dcc..7f069fc799 100644 --- a/redisinsight/api/src/modules/shared/services/instances-business/instances-business.service.ts +++ b/redisinsight/api/src/modules/shared/services/instances-business/instances-business.service.ts @@ -9,7 +9,7 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { Repository } from 'typeorm'; -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; import { find, omit } from 'lodash'; import { AppRedisInstanceEvents, RedisErrorCodes } from 'src/constants'; import ERROR_MESSAGES from 'src/constants/error-messages'; diff --git a/redisinsight/api/src/modules/shared/services/instances-business/overview.service.spec.ts b/redisinsight/api/src/modules/shared/services/instances-business/overview.service.spec.ts index bdacd4de7f..84fa4c24e0 100644 --- a/redisinsight/api/src/modules/shared/services/instances-business/overview.service.spec.ts +++ b/redisinsight/api/src/modules/shared/services/instances-business/overview.service.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import * as Redis from 'ioredis'; +import Redis from 'ioredis'; import { when } from 'jest-when'; import { mockStandaloneDatabaseEntity, @@ -80,14 +80,15 @@ describe('OverviewService', () => { service = await module.get(OverviewService); spyGetNodeInfo = jest.spyOn(service, 'getNodeInfo'); - mockClient.send_command = jest.fn(); + mockClient.call = jest.fn(); + mockClient.info = jest.fn(); }); describe('getOverview', () => { describe('Standalone', () => { it('should return proper overview', async () => { - when(mockClient.send_command) - .calledWith('info') + when(mockClient.info) + .calledWith() .mockResolvedValue(mockStandaloneRedisInfoReply); const result = await service.getOverview(databaseId, mockClient); diff --git a/redisinsight/api/src/modules/shared/services/instances-business/overview.service.ts b/redisinsight/api/src/modules/shared/services/instances-business/overview.service.ts index 3c3e7f2433..b604cd2767 100644 --- a/redisinsight/api/src/modules/shared/services/instances-business/overview.service.ts +++ b/redisinsight/api/src/modules/shared/services/instances-business/overview.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; import { get, filter, @@ -61,7 +61,7 @@ export class OverviewService { const { host, port } = client.options; return { ...convertRedisInfoReplyToObject( - await client.send_command('info'), + await client.info(), ), host, port, diff --git a/redisinsight/api/src/modules/shared/services/redis-sentinel-business/redis-sentinel-business.service.spec.ts b/redisinsight/api/src/modules/shared/services/redis-sentinel-business/redis-sentinel-business.service.spec.ts index 405ce6e711..fd820d64b9 100644 --- a/redisinsight/api/src/modules/shared/services/redis-sentinel-business/redis-sentinel-business.service.spec.ts +++ b/redisinsight/api/src/modules/shared/services/redis-sentinel-business/redis-sentinel-business.service.spec.ts @@ -52,13 +52,13 @@ describe('RedisSentinelBusinessService', () => { RedisSentinelBusinessService, ); redisService = await module.get(RedisService); - mockClient.send_command = jest.fn(); + mockClient.call = jest.fn(); mockClient.quit = jest.fn(); }); describe('connectAndGetMasters', () => { it('connect and get sentinel masters', async () => { - mockClient.send_command.mockResolvedValue( + mockClient.call.mockResolvedValue( mockRedisSentinelMasterResponse, ); service.getMasterEndpoints = jest @@ -87,13 +87,13 @@ describe('RedisSentinelBusinessService', () => { service.getMasterEndpoints = jest .fn() .mockResolvedValue([mockConnectionOptions]); - mockClient.send_command.mockResolvedValue( + mockClient.call.mockResolvedValue( [mockSentinelMasterInOkState, mockSentinelMasterInDownState], ); const result = await service.getMasters(mockClient); - expect(mockClient.send_command).toHaveBeenCalledWith('sentinel', [ + expect(mockClient.call).toHaveBeenCalledWith('sentinel', [ 'masters', ]); expect(result).toEqual([ @@ -105,7 +105,7 @@ describe('RedisSentinelBusinessService', () => { ]); }); it('wrong database type', async () => { - mockClient.send_command.mockRejectedValue({ + mockClient.call.mockRejectedValue({ message: 'ERR unknown command `sentinel`, with args beginning with: `masters`', }); @@ -123,7 +123,7 @@ describe('RedisSentinelBusinessService', () => { ...mockRedisNoPermError, command: 'SENTINEL', }; - mockClient.send_command.mockRejectedValue(error); + mockClient.call.mockRejectedValue(error); await expect(service.getMasters(mockClient)).rejects.toThrow( ForbiddenException, @@ -133,18 +133,18 @@ describe('RedisSentinelBusinessService', () => { describe('getMasterEndpoints', () => { it('succeed to get sentinel master endpoints', async () => { const masterName = mockSentinelMasterDto.name; - mockClient.send_command.mockResolvedValue([]); + mockClient.call.mockResolvedValue([]); const result = await service.getMasterEndpoints(mockClient, masterName); - expect(mockClient.send_command).toHaveBeenCalledWith('sentinel', [ + expect(mockClient.call).toHaveBeenCalledWith('sentinel', [ 'sentinels', masterName, ]); expect(result).toEqual([mockConnectionOptions]); }); it('wrong database type', async () => { - mockClient.send_command.mockRejectedValue({ + mockClient.call.mockRejectedValue({ message: 'ERR unknown command `sentinel`, with args beginning with: `masters`', }); @@ -158,7 +158,7 @@ describe('RedisSentinelBusinessService', () => { ...mockRedisNoPermError, command: 'SENTINEL', }; - mockClient.send_command.mockRejectedValue(error); + mockClient.call.mockRejectedValue(error); await expect( service.getMasterEndpoints(mockClient, mockSentinelMasterDto.name), diff --git a/redisinsight/api/src/modules/shared/services/redis-sentinel-business/redis-sentinel-business.service.ts b/redisinsight/api/src/modules/shared/services/redis-sentinel-business/redis-sentinel-business.service.ts index 33994cbb3a..b32db70c40 100644 --- a/redisinsight/api/src/modules/shared/services/redis-sentinel-business/redis-sentinel-business.service.ts +++ b/redisinsight/api/src/modules/shared/services/redis-sentinel-business/redis-sentinel-business.service.ts @@ -4,7 +4,7 @@ import { Injectable, Logger, } from '@nestjs/common'; -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; import { catchAclError, convertStringsArrayToObject, @@ -58,7 +58,9 @@ export class RedisSentinelBusinessService { this.logger.log('Getting sentinel masters.'); let result: SentinelMaster[]; try { - const reply = await client.send_command('sentinel', ['masters']); + const reply = await client.call('sentinel', ['masters']); + // @ts-expect-error + // https://github.com/luin/ioredis/issues/1572 result = reply.map((item) => { const { ip, @@ -102,10 +104,12 @@ export class RedisSentinelBusinessService { this.logger.log('Getting a list of sentinel instances for master.'); let result: EndpointDto[]; try { - const reply = await client.send_command('sentinel', [ + const reply = await client.call('sentinel', [ 'sentinels', masterName, ]); + // @ts-expect-error + // https://github.com/luin/ioredis/issues/1572 result = reply.map((item) => { const { ip, port } = convertStringsArrayToObject(item); return { host: ip, port: parseInt(port, 10) }; diff --git a/redisinsight/api/src/modules/slow-log/slow-log.service.spec.ts b/redisinsight/api/src/modules/slow-log/slow-log.service.spec.ts index a1c583f416..cdee7f5504 100644 --- a/redisinsight/api/src/modules/slow-log/slow-log.service.spec.ts +++ b/redisinsight/api/src/modules/slow-log/slow-log.service.spec.ts @@ -1,4 +1,4 @@ -import * as Redis from 'ioredis'; +import Redis from 'ioredis'; import { Test, TestingModule } from '@nestjs/testing'; import { mockRedisNoPermError, mockStandaloneDatabaseEntity, MockType } from 'src/__mocks__'; import { IFindRedisClientInstanceByOptions, RedisService } from 'src/modules/core/services/redis/redis.service'; @@ -49,10 +49,10 @@ const mockSlowlogConfigReply = [ const mockSlowLogReply = [mockLogReply, mockLogReply]; const mockRedisNode = Object.create(Redis.prototype); -mockRedisNode.send_command = jest.fn(); +mockRedisNode.call = jest.fn(); const mockRedisCluster = Object.create(Redis.Cluster.prototype); -mockRedisCluster.send_command = jest.fn(); +mockRedisCluster.call = jest.fn(); mockRedisCluster.nodes = jest.fn().mockResolvedValue([mockRedisNode, mockRedisNode]); describe('SlowLogService', () => { @@ -95,7 +95,7 @@ describe('SlowLogService', () => { client: mockRedisNode, }); redisService.isClientConnected.mockReturnValue(true); - mockRedisNode.send_command.mockResolvedValue(mockSlowLogReply); + mockRedisNode.call.mockResolvedValue(mockSlowLogReply); databaseService.connectToInstance.mockResolvedValueOnce(mockRedisNode); }); @@ -145,7 +145,7 @@ describe('SlowLogService', () => { describe('reset', () => { it('should reset slowlogs for standalone', async () => { await service.reset(mockClientOptions); - expect(mockRedisNode.send_command).toHaveBeenCalledWith(SlowLogCommands.SlowLog, SlowLogArguments.Reset); + expect(mockRedisNode.call).toHaveBeenCalledWith(SlowLogCommands.SlowLog, SlowLogArguments.Reset); }); it('should reset slowlogs cluster', async () => { redisService.getClientInstance.mockReturnValue({ @@ -153,7 +153,7 @@ describe('SlowLogService', () => { client: mockRedisCluster, }); await service.reset(mockClientOptions); - expect(mockRedisNode.send_command).toHaveBeenCalledWith(SlowLogCommands.SlowLog, SlowLogArguments.Reset); + expect(mockRedisNode.call).toHaveBeenCalledWith(SlowLogCommands.SlowLog, SlowLogArguments.Reset); }); it('should proxy HttpException', async () => { try { @@ -177,13 +177,13 @@ describe('SlowLogService', () => { describe('getConfig', () => { it('should get slowlogs config', async () => { - mockRedisNode.send_command.mockResolvedValueOnce(mockSlowlogConfigReply); + mockRedisNode.call.mockResolvedValueOnce(mockSlowlogConfigReply); const res = await service.getConfig(mockClientOptions); expect(res).toEqual(mockSlowLogConfig); }); it('should get ONLY supported slowlogs config even if there some extra fields in resp', async () => { - mockRedisNode.send_command.mockResolvedValueOnce([ + mockRedisNode.call.mockResolvedValueOnce([ ...mockSlowlogConfigReply, 'slowlog-extra', 12, @@ -214,26 +214,26 @@ describe('SlowLogService', () => { describe('updateConfig', () => { it('should update slowlogs config (1 field)', async () => { - mockRedisNode.send_command.mockResolvedValueOnce(mockSlowlogConfigReply); - mockRedisNode.send_command.mockResolvedValueOnce('OK'); + mockRedisNode.call.mockResolvedValueOnce(mockSlowlogConfigReply); + mockRedisNode.call.mockResolvedValueOnce('OK'); const res = await service.updateConfig(mockClientOptions, { slowlogMaxLen: 128 }); expect(res).toEqual(mockSlowLogConfig); - expect(mockRedisNode.send_command).toHaveBeenCalledTimes(2); + expect(mockRedisNode.call).toHaveBeenCalledTimes(2); }); it('should update slowlogs config (2 fields)', async () => { - mockRedisNode.send_command + mockRedisNode.call .mockResolvedValueOnce(mockSlowlogConfigReply) .mockResolvedValueOnce('OK') .mockResolvedValueOnce('OK'); const res = await service.updateConfig(mockClientOptions, { slowlogMaxLen: 128, slowlogLogSlowerThan: 1 }); expect(res).toEqual({ slowlogMaxLen: 128, slowlogLogSlowerThan: 1 }); - expect(mockRedisNode.send_command).toHaveBeenCalledTimes(3); + expect(mockRedisNode.call).toHaveBeenCalledTimes(3); }); it('should throw an error for cluster', async () => { try { - mockRedisCluster.send_command.mockResolvedValueOnce(mockSlowlogConfigReply); + mockRedisCluster.call.mockResolvedValueOnce(mockSlowlogConfigReply); redisService.getClientInstance.mockReturnValue({ ...mockRedisClientInstance, diff --git a/redisinsight/api/src/modules/slow-log/slow-log.service.ts b/redisinsight/api/src/modules/slow-log/slow-log.service.ts index 153e506eeb..853aac613e 100644 --- a/redisinsight/api/src/modules/slow-log/slow-log.service.ts +++ b/redisinsight/api/src/modules/slow-log/slow-log.service.ts @@ -1,4 +1,4 @@ -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; import { concat } from 'lodash'; import { BadRequestException, HttpException, Injectable, Logger, @@ -53,7 +53,9 @@ export class SlowLogService { * @param dto */ async getNodeSlowLogs(node: IORedis.Redis, dto: GetSlowLogsDto): Promise { - const resp = await node.send_command(SlowLogCommands.SlowLog, [SlowLogArguments.Get, dto.count]); + const resp = await node.call(SlowLogCommands.SlowLog, [SlowLogArguments.Get, dto.count]); + // @ts-expect-error + // https://github.com/luin/ioredis/issues/1572 return resp.map((log) => { const [id, time, durationUs, args, source, client] = log; @@ -81,7 +83,7 @@ export class SlowLogService { const client = await this.getClient(clientOptions); const nodes = await this.getNodes(client); - await Promise.all(nodes.map((node) => node.send_command(SlowLogCommands.SlowLog, SlowLogArguments.Reset))); + await Promise.all(nodes.map((node) => node.call(SlowLogCommands.SlowLog, SlowLogArguments.Reset))); } catch (e) { if (e instanceof HttpException) { throw e; @@ -101,7 +103,7 @@ export class SlowLogService { try { const client = await this.getClient(clientOptions); const resp = convertStringsArrayToObject( - await client.send_command(SlowLogCommands.Config, [SlowLogArguments.Get, 'slowlog*']), + await client.call(SlowLogCommands.Config, [SlowLogArguments.Get, 'slowlog*']), ); return { @@ -165,7 +167,7 @@ export class SlowLogService { if (client instanceof IORedis.Cluster) { return Promise.reject(new BadRequestException('Configuration slowlog for cluster is deprecated')); } - await Promise.all(commands.map((command) => client.send_command( + await Promise.all(commands.map((command) => client.call( command.command, command.args, ).then(command.analytics))); diff --git a/redisinsight/api/src/modules/workbench/dto/create-command-execution.dto.ts b/redisinsight/api/src/modules/workbench/dto/create-command-execution.dto.ts index 9febd90206..ed844b39d6 100644 --- a/redisinsight/api/src/modules/workbench/dto/create-command-execution.dto.ts +++ b/redisinsight/api/src/modules/workbench/dto/create-command-execution.dto.ts @@ -16,6 +16,11 @@ export enum RunQueryMode { ASCII = 'ASCII', } +export enum ResultsMode { + Default = 'DEFAULT', + GroupMode = 'GROUP_MODE', +} + export class CreateCommandExecutionDto { @ApiProperty({ type: String, @@ -38,6 +43,19 @@ export class CreateCommandExecutionDto { }) mode?: RunQueryMode = RunQueryMode.ASCII; + @ApiPropertyOptional({ + description: 'Workbench group mode', + default: ResultsMode.Default, + enum: ResultsMode, + }) + @IsOptional() + @IsEnum(ResultsMode, { + message: `resultsMode must be a valid enum value. Valid values: ${Object.values( + ResultsMode, + )}.`, + }) + resultsMode?: ResultsMode; + @ApiPropertyOptional({ description: 'Execute command for nodes with defined role', default: ClusterNodeRole.All, diff --git a/redisinsight/api/src/modules/workbench/dto/create-command-executions.dto.ts b/redisinsight/api/src/modules/workbench/dto/create-command-executions.dto.ts index ef62918952..aa68fe0d63 100644 --- a/redisinsight/api/src/modules/workbench/dto/create-command-executions.dto.ts +++ b/redisinsight/api/src/modules/workbench/dto/create-command-executions.dto.ts @@ -4,7 +4,7 @@ import { } from 'class-validator'; import { Type } from 'class-transformer'; import { ClusterSingleNodeOptions } from 'src/modules/cli/dto/cli.dto'; -import { ClusterNodeRole, RunQueryMode } from './create-command-execution.dto'; +import { ClusterNodeRole, RunQueryMode, ResultsMode } from './create-command-execution.dto'; export class CreateCommandExecutionsDto { @ApiProperty({ @@ -29,6 +29,14 @@ export class CreateCommandExecutionsDto { }) mode?: RunQueryMode = RunQueryMode.ASCII; + @IsOptional() + @IsEnum(ResultsMode, { + message: `resultsMode must be a valid enum value. Valid values: ${Object.values( + ResultsMode, + )}.`, + }) + resultsMode?: ResultsMode; + @ApiPropertyOptional({ description: 'Execute command for nodes with defined role', default: ClusterNodeRole.All, diff --git a/redisinsight/api/src/modules/workbench/entities/command-execution.entity.ts b/redisinsight/api/src/modules/workbench/entities/command-execution.entity.ts index 19cce13a8c..822e35aaa1 100644 --- a/redisinsight/api/src/modules/workbench/entities/command-execution.entity.ts +++ b/redisinsight/api/src/modules/workbench/entities/command-execution.entity.ts @@ -2,7 +2,7 @@ import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, JoinColumn, Index, } from 'typeorm'; import { DatabaseInstanceEntity } from 'src/modules/core/models/database-instance.entity'; -import { RunQueryMode } from 'src/modules/workbench/dto/create-command-execution.dto'; +import { RunQueryMode, ResultsMode } from 'src/modules/workbench/dto/create-command-execution.dto'; import { Transform } from 'class-transformer'; @Entity('command_execution') @@ -43,6 +43,20 @@ export class CommandExecutionEntity { @Column({ nullable: true }) role?: string; + @Column({ nullable: true }) + resultsMode?: string = ResultsMode.Default; + + @Column({ nullable: true }) + @Transform((object) => JSON.stringify(object), { toClassOnly: true }) + @Transform((string) => { + try { + return JSON.parse(string); + } catch (e) { + return undefined; + } + }, { toPlainOnly: true }) + summary?: string; + @Column({ nullable: true }) @Transform((object) => JSON.stringify(object), { toClassOnly: true }) @Transform((string) => { diff --git a/redisinsight/api/src/modules/workbench/models/command-execution.ts b/redisinsight/api/src/modules/workbench/models/command-execution.ts index 008224422a..d7b989c487 100644 --- a/redisinsight/api/src/modules/workbench/models/command-execution.ts +++ b/redisinsight/api/src/modules/workbench/models/command-execution.ts @@ -1,9 +1,33 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsDefined } from 'class-validator'; import { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result'; -import { ClusterNodeRole, RunQueryMode } from 'src/modules/workbench/dto/create-command-execution.dto'; +import { ClusterNodeRole, RunQueryMode, ResultsMode } from 'src/modules/workbench/dto/create-command-execution.dto'; import { ClusterSingleNodeOptions } from 'src/modules/cli/dto/cli.dto'; import { Expose } from 'class-transformer'; +export class ResultsSummary { + @ApiProperty({ + description: 'Total number of commands executed', + type: Number, + }) + @IsDefined() + total: number; + + @ApiProperty({ + description: 'Total number of successful commands executed', + type: Number, + }) + @IsDefined() + success: number; + + @ApiProperty({ + description: 'Total number of failed commands executed', + type: Number, + }) + @IsDefined() + fail: number; +} + export class CommandExecution { @ApiProperty({ description: 'Command execution id', @@ -34,6 +58,21 @@ export class CommandExecution { @Expose() mode?: RunQueryMode = RunQueryMode.ASCII; + @ApiPropertyOptional({ + description: 'Workbench result mode', + default: ResultsMode.Default, + enum: ResultsMode, + }) + @Expose() + resultsMode?: ResultsMode = ResultsMode.Default; + + @ApiPropertyOptional({ + description: 'Workbench executions summary', + type: () => ResultsSummary, + }) + @Expose() + summary?: ResultsSummary; + @ApiProperty({ description: 'Command execution result', type: () => CommandExecutionResult, @@ -42,6 +81,13 @@ export class CommandExecution { @Expose() result: CommandExecutionResult[]; + @ApiPropertyOptional({ + description: 'Result did not stored in db', + type: Boolean, + }) + @Expose() + isNotStored?: boolean; + @ApiPropertyOptional({ description: 'Nodes roles where command was executed', default: ClusterNodeRole.All, diff --git a/redisinsight/api/src/modules/workbench/plugins.service.spec.ts b/redisinsight/api/src/modules/workbench/plugins.service.spec.ts index 7a7c4e6377..f88ab75db1 100644 --- a/redisinsight/api/src/modules/workbench/plugins.service.spec.ts +++ b/redisinsight/api/src/modules/workbench/plugins.service.spec.ts @@ -7,6 +7,7 @@ import { ClusterNodeRole, CreateCommandExecutionDto, RunQueryMode, + ResultsMode, } from 'src/modules/workbench/dto/create-command-execution.dto'; import { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; @@ -34,6 +35,7 @@ const mockCreateCommandExecutionDto: CreateCommandExecutionDto = { }, role: ClusterNodeRole.All, mode: RunQueryMode.ASCII, + resultsMode: ResultsMode.Default, }; const mockCommandExecutionResults: CommandExecutionResult[] = [ diff --git a/redisinsight/api/src/modules/workbench/providers/command-execution.provider.spec.ts b/redisinsight/api/src/modules/workbench/providers/command-execution.provider.spec.ts index 131db4b9f8..d3e0ac6810 100644 --- a/redisinsight/api/src/modules/workbench/providers/command-execution.provider.spec.ts +++ b/redisinsight/api/src/modules/workbench/providers/command-execution.provider.spec.ts @@ -105,17 +105,17 @@ describe('CommandExecutionProvider', () => { describe('create', () => { it('should process new entity', async () => { - repository.save.mockReturnValueOnce(mockCommandExecutionEntity); + repository.save.mockReturnValueOnce([mockCommandExecutionEntity]); encryptionService.encrypt.mockReturnValue(mockEncryptResult); - expect(await service.create(mockCommandExecutionPartial)).toEqual(new CommandExecution({ + expect(await service.createMany([mockCommandExecutionPartial])).toEqual([new CommandExecution({ ...mockCommandExecutionPartial, id: mockCommandExecutionEntity.id, createdAt: mockCommandExecutionEntity.createdAt, - })); + })]); }); it('should return full result even if size limit exceeded', async () => { - repository.save.mockReturnValueOnce(mockCommandExecutionEntity); + repository.save.mockReturnValueOnce([mockCommandExecutionEntity]); encryptionService.encrypt.mockReturnValue(mockEncryptResult); const executionResult = [new CommandExecutionResult({ @@ -123,15 +123,15 @@ describe('CommandExecutionProvider', () => { response: `${Buffer.alloc(WORKBENCH_CONFIG.maxResultSize, 'a').toString()}`, })]; - expect(await service.create({ + expect(await service.createMany([{ ...mockCommandExecutionPartial, result: executionResult, - })).toEqual(new CommandExecution({ + }])).toEqual([new CommandExecution({ ...mockCommandExecutionPartial, id: mockCommandExecutionEntity.id, createdAt: mockCommandExecutionEntity.createdAt, result: executionResult, - })); + })]); expect(encryptionService.encrypt).toHaveBeenLastCalledWith(JSON.stringify([ new CommandExecutionResult({ @@ -140,6 +140,33 @@ describe('CommandExecutionProvider', () => { }), ])); }); + it('should return with flag isNotStored="true" even if size limit exceeded', async () => { + repository.save.mockReturnValueOnce([{ ...mockCommandExecutionEntity, isNotStored: true }]); + encryptionService.encrypt.mockReturnValue(mockEncryptResult); + + const executionResult = [new CommandExecutionResult({ + status: CommandExecutionStatus.Success, + response: `${Buffer.alloc(WORKBENCH_CONFIG.maxResultSize, 'a').toString()}`, + })]; + + expect(await service.createMany([{ + ...mockCommandExecutionPartial, + result: executionResult, + }])).toEqual([new CommandExecution({ + ...mockCommandExecutionPartial, + id: mockCommandExecutionEntity.id, + createdAt: mockCommandExecutionEntity.createdAt, + result: executionResult, + isNotStored: true, + })]); + + expect(encryptionService.encrypt).toHaveBeenLastCalledWith(JSON.stringify([ + new CommandExecutionResult({ + status: CommandExecutionStatus.Success, + response: 'Results have been deleted since they exceed 1 MB. Re-run the command to see new results.', + }), + ])); + }) }); describe('getList', () => { it('should return list (2) of command execution', async () => { @@ -178,7 +205,7 @@ describe('CommandExecutionProvider', () => { }); describe('getOne', () => { it('should return decrypted and transformed command execution', async () => { - repository.findOne.mockResolvedValueOnce(mockCommandExecutionEntity); + repository.findOneBy.mockResolvedValueOnce(mockCommandExecutionEntity); encryptionService.decrypt.mockReturnValueOnce(mockCreateCommandExecutionDto.command); encryptionService.decrypt.mockReturnValueOnce(JSON.stringify([mockCommandExecutionResult])); @@ -193,7 +220,7 @@ describe('CommandExecutionProvider', () => { ); }); it('should return null fields in case of decryption errors', async () => { - repository.findOne.mockResolvedValueOnce(mockCommandExecutionEntity); + repository.findOneBy.mockResolvedValueOnce(mockCommandExecutionEntity); encryptionService.decrypt.mockReturnValueOnce(mockCreateCommandExecutionDto.command); encryptionService.decrypt.mockRejectedValueOnce(new KeytarDecryptionErrorException()); @@ -209,7 +236,7 @@ describe('CommandExecutionProvider', () => { ); }); it('should return not found exception', async () => { - repository.findOne.mockResolvedValueOnce(null); + repository.findOneBy.mockResolvedValueOnce(null); try { await service.getOne(mockStandaloneDatabaseEntity.id, mockCommandExecutionEntity.id); diff --git a/redisinsight/api/src/modules/workbench/providers/command-execution.provider.ts b/redisinsight/api/src/modules/workbench/providers/command-execution.provider.ts index 2ed766b7f3..ee2d35fc09 100644 --- a/redisinsight/api/src/modules/workbench/providers/command-execution.provider.ts +++ b/redisinsight/api/src/modules/workbench/providers/command-execution.provider.ts @@ -25,40 +25,53 @@ export class CommandExecutionProvider { ) {} /** - * Encrypt command execution and save entire entity + * Encrypt command executions and save entire entities * Should always throw and error in case when unable to encrypt for some reason - * @param commandExecution + * @param commandExecutions */ - async create(commandExecution: Partial): Promise { - const entity = plainToClass(CommandExecutionEntity, commandExecution); + async createMany(commandExecutions: Partial[]): Promise { + // todo: limit by 30 max to insert + let entities = await Promise.all(commandExecutions.map(async (commandExecution, idx) => { + const entity = plainToClass(CommandExecutionEntity, commandExecution); - // Do not store command execution result that exceeded limitation - if (JSON.stringify(entity.result).length > WORKBENCH_CONFIG.maxResultSize) { - entity.result = JSON.stringify([ + // Do not store command execution result that exceeded limitation + if (JSON.stringify(entity.result).length > WORKBENCH_CONFIG.maxResultSize) { + entity.result = JSON.stringify([ + { + status: CommandExecutionStatus.Success, + response: ERROR_MESSAGES.WORKBENCH_RESPONSE_TOO_BIG(), + }, + ]); + // Hack, do not store isNotStored. Send once to show warning + entity['isNotStored'] = true; + } + + return this.encryptEntity(entity); + })); + + entities = await this.commandExecutionRepository.save(entities); + + const response = await Promise.all( + entities.map((entity, idx) => classToClass( + CommandExecution, { - status: CommandExecutionStatus.Success, - response: ERROR_MESSAGES.WORKBENCH_RESPONSE_TOO_BIG(), + ...entity, + command: commandExecutions[idx].command, + mode: commandExecutions[idx].mode, + result: commandExecutions[idx].result, + nodeOptions: commandExecutions[idx].nodeOptions, + summary: commandExecutions[idx].summary, }, - ]); - } - - const response = await classToClass( - CommandExecution, - { - ...await this.commandExecutionRepository.save(await this.encryptEntity(entity)), - command: commandExecution.command, - mode: commandExecution.mode, - result: commandExecution.result, - nodeOptions: commandExecution.nodeOptions, - }, + )), ); // cleanup history and ignore error if any try { - await this.cleanupDatabaseHistory(entity.databaseId); + await this.cleanupDatabaseHistory(entities[0].databaseId); } catch (e) { this.logger.error('Error when trying to cleanup history after insert', e); } + return response; } @@ -71,7 +84,18 @@ export class CommandExecutionProvider { const entities = await this.commandExecutionRepository .createQueryBuilder('e') .where({ databaseId }) - .select(['e.id', 'e.command', 'e.databaseId', 'e.createdAt', 'e.encryption', 'e.role', 'e.nodeOptions', 'e.mode']) + .select([ + 'e.id', + 'e.command', + 'e.databaseId', + 'e.createdAt', + 'e.encryption', + 'e.role', + 'e.nodeOptions', + 'e.mode', + 'e.summary', + 'e.resultsMode', + ]) .orderBy('e.createdAt', 'DESC') .limit(WORKBENCH_CONFIG.maxItemsPerDb) .getMany(); @@ -101,7 +125,7 @@ export class CommandExecutionProvider { async getOne(databaseId: string, id: string): Promise { this.logger.log('Getting command executions'); - const entity = await this.commandExecutionRepository.findOne({ id, databaseId }); + const entity = await this.commandExecutionRepository.findOneBy({ id, databaseId }); if (!entity) { this.logger.error(`Command execution with id:${id} and databaseId:${databaseId} was not Found`); diff --git a/redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.spec.ts b/redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.spec.ts index 5d96cfa72c..d44dda2ae5 100644 --- a/redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.spec.ts +++ b/redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import * as Redis from 'ioredis'; +import Redis from 'ioredis'; import { mockRedisCommandReply, mockStandaloneDatabaseEntity, @@ -30,7 +30,7 @@ describe('PluginCommandsWhitelistProvider', () => { service = await module.get(PluginCommandsWhitelistProvider); mockRedisTool.getRedisClient.mockResolvedValue(mockClient); - mockClient.send_command = jest.fn(); + mockClient.call = jest.fn(); }); describe('getWhitelistCommands', () => { @@ -58,27 +58,27 @@ describe('PluginCommandsWhitelistProvider', () => { }); describe('calculateWhiteListCommands', () => { it('should return 2 readonly commands', async () => { - mockClient.send_command.mockResolvedValueOnce(mockRedisCommandReply); - mockClient.send_command.mockResolvedValueOnce([]); - mockClient.send_command.mockResolvedValueOnce([]); + mockClient.call.mockResolvedValueOnce(mockRedisCommandReply); + mockClient.call.mockResolvedValueOnce([]); + mockClient.call.mockResolvedValueOnce([]); const result = await service.calculateWhiteListCommands(mockClient); expect(result).toEqual(mockWhitelistCommandsResponse); }); it('should return 1 readonly commands excluded by dangerous filter', async () => { - mockClient.send_command.mockResolvedValueOnce(mockRedisCommandReply); - mockClient.send_command.mockResolvedValueOnce(['custom.command']); - mockClient.send_command.mockResolvedValueOnce([]); + mockClient.call.mockResolvedValueOnce(mockRedisCommandReply); + mockClient.call.mockResolvedValueOnce(['custom.command']); + mockClient.call.mockResolvedValueOnce([]); const result = await service.calculateWhiteListCommands(mockClient); expect(result).toEqual(['get']); }); it('should return 1 readonly commands excluded by blocking filter', async () => { - mockClient.send_command.mockResolvedValueOnce(mockRedisCommandReply); - mockClient.send_command.mockResolvedValueOnce([]); - mockClient.send_command.mockResolvedValueOnce(['custom.command']); + mockClient.call.mockResolvedValueOnce(mockRedisCommandReply); + mockClient.call.mockResolvedValueOnce([]); + mockClient.call.mockResolvedValueOnce(['custom.command']); const result = await service.calculateWhiteListCommands(mockClient); diff --git a/redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.ts b/redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.ts index 97f68b9748..26ae121937 100644 --- a/redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.ts +++ b/redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.ts @@ -47,7 +47,7 @@ export class PluginCommandsWhitelistProvider { async calculateWhiteListCommands(client: any): Promise { let pluginWhiteListCommands = []; try { - const availableCommands = await client.send_command('command'); + const availableCommands = await client.call('command'); const readOnlyCommands = map(filter(availableCommands, ( command, ) => get(command, [2], []) @@ -55,14 +55,14 @@ export class PluginCommandsWhitelistProvider { const blackListCommands = [...pluginUnsupportedCommands, ...pluginBlockingCommands]; try { - const dangerousCommands = await client.send_command('acl', ['cat', 'dangerous']); + const dangerousCommands = await client.call('acl', ['cat', 'dangerous']); blackListCommands.push(...dangerousCommands); } catch (e) { // ignore error as acl cat available since Redis 6.0 } try { - const blockingCommands = await client.send_command('acl', ['cat', 'blocking']); + const blockingCommands = await client.call('acl', ['cat', 'blocking']); blackListCommands.push(...blockingCommands); } catch (e) { // ignore error as acl cat available since Redis 6.0 diff --git a/redisinsight/api/src/modules/workbench/providers/plugin-state.provider.spec.ts b/redisinsight/api/src/modules/workbench/providers/plugin-state.provider.spec.ts index 61928b45bf..254eb326c8 100644 --- a/redisinsight/api/src/modules/workbench/providers/plugin-state.provider.spec.ts +++ b/redisinsight/api/src/modules/workbench/providers/plugin-state.provider.spec.ts @@ -90,13 +90,13 @@ describe('PluginStateProvider', () => { }); describe('getOne', () => { it('should return decrypted and transformed state', async () => { - repository.findOne.mockResolvedValueOnce(mockPluginStateEntity); + repository.findOneBy.mockResolvedValueOnce(mockPluginStateEntity); encryptionService.decrypt.mockReturnValueOnce(JSON.stringify(mockPluginState.state)); expect(await service.getOne(mockVisualizationId, mockCommandExecutionId)).toEqual(mockPluginState); }); it('should return null fields in case of decryption errors', async () => { - repository.findOne.mockResolvedValueOnce(mockPluginStateEntity); + repository.findOneBy.mockResolvedValueOnce(mockPluginStateEntity); encryptionService.decrypt.mockRejectedValueOnce(new KeytarDecryptionErrorException()); const result = await service.getOne(mockVisualizationId, mockCommandExecutionId); @@ -108,7 +108,7 @@ describe('PluginStateProvider', () => { }); }); it('should return not found exception', async () => { - repository.findOne.mockResolvedValueOnce(null); + repository.findOneBy.mockResolvedValueOnce(null); try { await service.getOne(mockVisualizationId, mockCommandExecutionId); diff --git a/redisinsight/api/src/modules/workbench/providers/plugin-state.provider.ts b/redisinsight/api/src/modules/workbench/providers/plugin-state.provider.ts index 6b74868fcc..3c7a6aeeaa 100644 --- a/redisinsight/api/src/modules/workbench/providers/plugin-state.provider.ts +++ b/redisinsight/api/src/modules/workbench/providers/plugin-state.provider.ts @@ -46,7 +46,7 @@ export class PluginStateProvider { async getOne(visualizationId: string, commandExecutionId: string): Promise { this.logger.log('Getting plugin state'); - const entity = await this.repository.findOne({ visualizationId, commandExecutionId }); + const entity = await this.repository.findOneBy({ visualizationId, commandExecutionId }); if (!entity) { this.logger.error(`Plugin state ${commandExecutionId}:${visualizationId} was not Found`); diff --git a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts index be45bdb177..02fc794839 100644 --- a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts +++ b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts @@ -21,8 +21,8 @@ import { WrongDatabaseTypeError, } from 'src/modules/cli/constants/errors'; import { ICliExecResultFromNode, RedisToolService } from 'src/modules/shared/services/base/redis-tool.service'; -import { CommandsService } from 'src/modules/commands/commands.service'; -import { FormatterManager, IFormatterStrategy, FormatterTypes } from 'src/common/transformers';import { WorkbenchAnalyticsService } from '../services/workbench-analytics/workbench-analytics.service'; +import { FormatterManager, IFormatterStrategy, FormatterTypes } from 'src/common/transformers'; +import { WorkbenchAnalyticsService } from '../services/workbench-analytics/workbench-analytics.service'; const MOCK_ERROR_MESSAGE = 'Some error'; @@ -37,10 +37,6 @@ const mockCliTool = () => ({ formatterManager: jest.fn(), }); -const mockCommandsService = () => ({ - getCommandsGroups: jest.fn(), -}); - const mockNodeEndpoint = { host: '127.0.0.1', port: 6379, @@ -52,8 +48,9 @@ const mockCliNodeResponse: ICliExecResultFromNode = { status: CommandExecutionStatus.Success, }; +const mockSetCommand = 'set'; const mockCreateCommandExecutionDto: CreateCommandExecutionDto = { - command: 'set foo bar', + command: `${mockSetCommand} foo bar`, nodeOptions: { ...mockNodeEndpoint, enableRedirection: true, @@ -70,14 +67,17 @@ const mockCommandExecutionResult: CommandExecutionResult = { }, }; +const mockAnalyticsService = mockWorkbenchAnalyticsService(); + describe('WorkbenchCommandsExecutor', () => { let service: WorkbenchCommandsExecutor; let cliTool; - let commandsService: CommandsService; let utf8Formatter: IFormatterStrategy; let asciiFormatter: IFormatterStrategy; beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ providers: [ WorkbenchCommandsExecutor, @@ -87,18 +87,14 @@ describe('WorkbenchCommandsExecutor', () => { }, { provide: WorkbenchAnalyticsService, - useFactory: mockWorkbenchAnalyticsService, - }, - { - provide: CommandsService, - useFactory: mockCommandsService, + useFactory: () => mockAnalyticsService, }, ], }).compile(); service = module.get(WorkbenchCommandsExecutor); cliTool = module.get(RedisToolService); - commandsService = module.get(CommandsService); + const formatterManager: FormatterManager = get( service, 'formatterManager', @@ -125,6 +121,20 @@ describe('WorkbenchCommandsExecutor', () => { response: mockCommandExecutionResult.response, status: mockCommandExecutionResult.status, }]); + + expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( + mockClientOptions.instanceId, + [ + { + response: mockCommandExecutionResult.response, + status: CommandExecutionStatus.Success, + }, + ], + { + command: mockSetCommand, + rawMode: false, + }, + ); }); it('should return fail status in case of unsupported command error', async () => { cliTool.execCommand.mockRejectedValueOnce(new CommandNotSupportedError(MOCK_ERROR_MESSAGE)); @@ -138,6 +148,19 @@ describe('WorkbenchCommandsExecutor', () => { response: MOCK_ERROR_MESSAGE, status: CommandExecutionStatus.Fail, }]); + + expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: MOCK_ERROR_MESSAGE, + error: new CommandNotSupportedError(MOCK_ERROR_MESSAGE), + status: CommandExecutionStatus.Fail, + }, + { + command: mockSetCommand, + rawMode: false, + }, + ); }); it('should return fail status when replyError happened', async () => { const replyError: Error = { @@ -156,6 +179,19 @@ describe('WorkbenchCommandsExecutor', () => { response: MOCK_ERROR_MESSAGE, status: CommandExecutionStatus.Fail, }]); + + expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: MOCK_ERROR_MESSAGE, + error: replyError, + status: CommandExecutionStatus.Fail, + }, + { + command: mockSetCommand, + rawMode: false, + }, + ); }); it('should successfully execute command and return ascii response', async () => { const formatSpy = jest.spyOn(asciiFormatter, 'format'); @@ -172,6 +208,20 @@ describe('WorkbenchCommandsExecutor', () => { status: mockCommandExecutionResult.status, }]); expect(formatSpy).toHaveBeenCalled(); + + expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( + mockClientOptions.instanceId, + [ + { + response: mockCommandExecutionResult.response, + status: CommandExecutionStatus.Success, + }, + ], + { + command: mockSetCommand, + rawMode: false, + }, + ); }); it('should successfully execute command and return raw response', async () => { const formatSpy = jest.spyOn(utf8Formatter, 'format'); @@ -188,8 +238,22 @@ describe('WorkbenchCommandsExecutor', () => { status: mockCommandExecutionResult.status, }]); expect(formatSpy).toHaveBeenCalled(); + + expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( + mockClientOptions.instanceId, + [ + { + response: mockCommandExecutionResult.response, + status: CommandExecutionStatus.Success, + }, + ], + { + command: mockSetCommand, + rawMode: true, + }, + ); }); - it('should throw an error when unexpected error happened', async () => { + it('should throw an error when on unexpected error', async () => { cliTool.execCommand.mockRejectedValueOnce(new ServiceUnavailableException(MOCK_ERROR_MESSAGE)); try { @@ -201,6 +265,19 @@ describe('WorkbenchCommandsExecutor', () => { } catch (e) { expect(e).toBeInstanceOf(InternalServerErrorException); expect(e.message).toEqual(MOCK_ERROR_MESSAGE); + + expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: MOCK_ERROR_MESSAGE, + error: new ServiceUnavailableException(MOCK_ERROR_MESSAGE), + status: CommandExecutionStatus.Fail, + }, + { + command: mockSetCommand, + rawMode: false, + }, + ); } }); }); @@ -213,6 +290,19 @@ describe('WorkbenchCommandsExecutor', () => { expect(result).toEqual([{ ...mockCommandExecutionResult, }]); + + expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( + mockClientOptions.instanceId, + [ + { + ...mockCommandExecutionResult, + }, + ], + { + command: mockSetCommand, + rawMode: false, + }, + ); }); it('should return failed status when redirection disabled and MOVED response received', async () => { cliTool.execCommandForNode.mockResolvedValueOnce({ @@ -231,6 +321,19 @@ describe('WorkbenchCommandsExecutor', () => { expect(result).toEqual([{ ...mockCommandExecutionResult, }]); + + expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( + mockClientOptions.instanceId, + [ + { + ...mockCommandExecutionResult, + }, + ], + { + command: mockSetCommand, + rawMode: false, + }, + ); }); it('should return success status when redirection enabled and MOVED response received', async () => { cliTool.execCommandForNode.mockResolvedValueOnce({ @@ -248,6 +351,23 @@ describe('WorkbenchCommandsExecutor', () => { slot: 7008, }, }]); + + expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( + mockClientOptions.instanceId, + [ + { + ...mockCommandExecutionResult, + node: { + ...mockCommandExecutionResult.node, + slot: 7008, + }, + }, + ], + { + command: mockSetCommand, + rawMode: false, + }, + ); }); it('should return fail status when command is not supported', async () => { cliTool.execCommandForNode.mockRejectedValueOnce(new CommandNotSupportedError(MOCK_ERROR_MESSAGE)); @@ -258,6 +378,19 @@ describe('WorkbenchCommandsExecutor', () => { response: MOCK_ERROR_MESSAGE, status: CommandExecutionStatus.Fail, }]); + + expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: MOCK_ERROR_MESSAGE, + error: new CommandNotSupportedError(MOCK_ERROR_MESSAGE), + status: CommandExecutionStatus.Fail, + }, + { + command: mockSetCommand, + rawMode: false, + }, + ); }); it('should throw BadRequest when ClusterNodeNotFoundError error received', async () => { cliTool.execCommandForNode.mockRejectedValueOnce(new ClusterNodeNotFoundError(MOCK_ERROR_MESSAGE)); @@ -268,6 +401,19 @@ describe('WorkbenchCommandsExecutor', () => { } catch (e) { expect(e).toBeInstanceOf(BadRequestException); expect(e.message).toEqual(MOCK_ERROR_MESSAGE); + + expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: MOCK_ERROR_MESSAGE, + error: new ClusterNodeNotFoundError(MOCK_ERROR_MESSAGE), + status: CommandExecutionStatus.Fail, + }, + { + command: mockSetCommand, + rawMode: false, + }, + ); } }); it('should throw an error when unexpected error happened', async () => { @@ -279,6 +425,19 @@ describe('WorkbenchCommandsExecutor', () => { } catch (e) { expect(e).toBeInstanceOf(InternalServerErrorException); expect(e.message).toEqual(MOCK_ERROR_MESSAGE); + + expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: MOCK_ERROR_MESSAGE, + error: new ServiceUnavailableException(MOCK_ERROR_MESSAGE), + status: CommandExecutionStatus.Fail, + }, + { + command: mockSetCommand, + rawMode: false, + }, + ); } }); }); @@ -305,6 +464,21 @@ describe('WorkbenchCommandsExecutor', () => { status: CommandExecutionStatus.Fail, }, ]); + + expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( + mockClientOptions.instanceId, + [ + mockCommandExecutionResult, + { + ...mockCommandExecutionResult, + status: CommandExecutionStatus.Fail, + }, + ], + { + command: mockSetCommand, + rawMode: false, + }, + ); }); it('should return fail status when command is not supported', async () => { cliTool.execCommandForNodes.mockRejectedValueOnce(new CommandNotSupportedError(MOCK_ERROR_MESSAGE)); @@ -319,6 +493,19 @@ describe('WorkbenchCommandsExecutor', () => { response: MOCK_ERROR_MESSAGE, status: CommandExecutionStatus.Fail, }]); + + expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: MOCK_ERROR_MESSAGE, + error: new CommandNotSupportedError(MOCK_ERROR_MESSAGE), + status: CommandExecutionStatus.Fail, + }, + { + command: mockSetCommand, + rawMode: false, + }, + ); }); it('should throw BadRequest when WrongDatabaseTypeError error received', async () => { cliTool.execCommandForNodes.mockRejectedValueOnce(new WrongDatabaseTypeError(MOCK_ERROR_MESSAGE)); @@ -333,6 +520,19 @@ describe('WorkbenchCommandsExecutor', () => { } catch (e) { expect(e).toBeInstanceOf(BadRequestException); expect(e.message).toEqual(MOCK_ERROR_MESSAGE); + + expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: MOCK_ERROR_MESSAGE, + error: new WrongDatabaseTypeError(MOCK_ERROR_MESSAGE), + status: CommandExecutionStatus.Fail, + }, + { + command: mockSetCommand, + rawMode: false, + }, + ); } }); it('should throw an error when unexpected error happened', async () => { @@ -348,6 +548,19 @@ describe('WorkbenchCommandsExecutor', () => { } catch (e) { expect(e).toBeInstanceOf(InternalServerErrorException); expect(e.message).toEqual(MOCK_ERROR_MESSAGE); + + expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( + mockClientOptions.instanceId, + { + response: MOCK_ERROR_MESSAGE, + error: new ServiceUnavailableException(MOCK_ERROR_MESSAGE), + status: CommandExecutionStatus.Fail, + }, + { + command: mockSetCommand, + rawMode: false, + }, + ); } }); }); diff --git a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts index f79a4b6063..efe43b0350 100644 --- a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts +++ b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts @@ -12,7 +12,6 @@ import { parseRedirectionError, splitCliCommandLine, } from 'src/utils/cli-helper'; -import { CommandType } from 'src/constants'; import { CommandNotSupportedError, CommandParsingError, @@ -20,7 +19,6 @@ import { WrongDatabaseTypeError, } from 'src/modules/cli/constants/errors'; import { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result'; -import { CommandsService } from 'src/modules/commands/commands.service'; import { CreateCommandExecutionDto, RunQueryMode } from 'src/modules/workbench/dto/create-command-execution.dto'; import { RedisToolService } from 'src/modules/shared/services/base/redis-tool.service'; import { @@ -40,7 +38,6 @@ export class WorkbenchCommandsExecutor { constructor( private redisTool: RedisToolService, private analyticsService: WorkbenchAnalyticsService, - private readonly commandsService: CommandsService, ) { this.formatterManager = new FormatterManager(); this.formatterManager.addStrategy( @@ -53,233 +50,220 @@ export class WorkbenchCommandsExecutor { ); } + /** + * Entrypoint for any CommandExecution + * Will determine type of a command (standalone, per node(s)) and format, and execute it + * Also sis a single place of analytics events invocation + * @param clientOptions + * @param dto + */ public async sendCommand( clientOptions: IFindRedisClientInstanceByOptions, dto: CreateCommandExecutionDto, ): Promise { + let result; + const { - command, role, nodeOptions, mode, + command: commandLine, + role, + nodeOptions, + mode, } = dto; - if (nodeOptions) { - const result = await this.sendCommandForSingleNode( - clientOptions, - command, - role, - mode, - nodeOptions, - ); - - return [result]; - } - - if (role) { - return this.sendCommandForNodes(clientOptions, command, role, mode); - } - - return [await this.sendCommandForStandalone(clientOptions, dto)]; - } - - private async sendCommandForStandalone( - clientOptions: IFindRedisClientInstanceByOptions, - dto: CreateCommandExecutionDto, - ): Promise { - this.logger.log('Executing workbench command.'); - const { command: commandLine, mode } = dto; + const [command, ...commandArgs] = splitCliCommandLine(commandLine); try { - const [command, ...args] = splitCliCommandLine(commandLine); - const formatter = this.getFormatter(mode); - - const replyEncoding = checkHumanReadableCommands(`${command} ${args[0]}`) ? 'utf8' : undefined; - - const response = formatter.format(await this.redisTool.execCommand(clientOptions, command, args, replyEncoding)); - - this.logger.log('Succeed to execute workbench command.'); - - const result = { response, status: CommandExecutionStatus.Success }; - const commandType = await this.checkIsCoreCommand(command) ? CommandType.Core : CommandType.Module; + if (nodeOptions) { + result = [await this.sendCommandForSingleNode( + clientOptions, + command, + commandArgs, + role, + mode, + nodeOptions, + )]; + } else if (role) { + result = await this.sendCommandForNodes( + clientOptions, + command, + commandArgs, + role, + mode, + ); + } else { + result = [await this.sendCommandForStandalone( + clientOptions, + command, + commandArgs, + mode, + )]; + } - this.analyticsService.sendCommandExecutedEvent( + this.analyticsService.sendCommandExecutedEvents( clientOptions.instanceId, result, - { command, commandType, rawMode: mode === RunQueryMode.Raw }, + { command, rawMode: mode === RunQueryMode.Raw }, ); + return result; } catch (error) { this.logger.error('Failed to execute workbench command.', error); - const result = { response: error.message, status: CommandExecutionStatus.Fail }; + const errorResult = { response: error.message, status: CommandExecutionStatus.Fail }; + this.analyticsService.sendCommandExecutedEvent( + clientOptions.instanceId, + { ...errorResult, error }, + { command, rawMode: dto.mode === RunQueryMode.Raw }, + ); + if ( error instanceof CommandParsingError || error instanceof CommandNotSupportedError || error.name === 'ReplyError' ) { - this.analyticsService.sendCommandExecutedEvent( - clientOptions.instanceId, - { ...result, error }, - { rawMode: mode === RunQueryMode.Raw }, - ); - return result; + return [errorResult]; + } + + if (error instanceof WrongDatabaseTypeError || error instanceof ClusterNodeNotFoundError) { + throw new BadRequestException(error.message); } - this.analyticsService.sendCommandExecutedEvent( - clientOptions.instanceId, - { ...result, error }, - { rawMode: mode === RunQueryMode.Raw }, - ); throw new InternalServerErrorException(error.message); } } + /** + * Sends command for standalone instances + * @param clientOptions + * @param command + * @param commandArgs + * @param mode + * @private + */ + private async sendCommandForStandalone( + clientOptions: IFindRedisClientInstanceByOptions, + command: string, + commandArgs: string[], + mode: RunQueryMode, + ): Promise { + this.logger.log('Executing workbench command.'); + + const formatter = this.getFormatter(mode); + + const replyEncoding = checkHumanReadableCommands(`${command} ${commandArgs[0]}`) ? 'utf8' : undefined; + + const response = formatter.format( + await this.redisTool.execCommand(clientOptions, command, commandArgs, replyEncoding), + ); + + this.logger.log('Succeed to execute workbench command.'); + + return { response, status: CommandExecutionStatus.Success }; + } + + /** + * Sends command for a single node in cluster by host and port (nodeOptions) + * @param clientOptions + * @param command + * @param commandArgs + * @param role + * @param mode + * @param nodeOptions + * @private + */ private async sendCommandForSingleNode( clientOptions: IFindRedisClientInstanceByOptions, - commandLine: string, + command: string, + commandArgs: string[], role: ClusterNodeRole = ClusterNodeRole.All, mode: RunQueryMode = RunQueryMode.ASCII, nodeOptions: ClusterSingleNodeOptions, ): Promise { this.logger.log(`Executing redis.cluster CLI command for single node ${JSON.stringify(nodeOptions)}`); - try { - const [command, ...args] = splitCliCommandLine(commandLine); - const formatter = this.getFormatter(mode); - const replyEncoding = checkHumanReadableCommands(`${command} ${args[0]}`) ? 'utf8' : undefined; + const formatter = this.getFormatter(mode); + + const replyEncoding = checkHumanReadableCommands(`${command} ${commandArgs[0]}`) ? 'utf8' : undefined; - const nodeAddress = `${nodeOptions.host}:${nodeOptions.port}`; - let result = await this.redisTool.execCommandForNode( + const nodeAddress = `${nodeOptions.host}:${nodeOptions.port}`; + let result = await this.redisTool.execCommandForNode( + clientOptions, + command, + commandArgs, + role, + nodeAddress, + replyEncoding, + ); + if (result.error && checkRedirectionError(result.error) && nodeOptions.enableRedirection) { + const { slot, address } = parseRedirectionError(result.error); + result = await this.redisTool.execCommandForNode( clientOptions, command, - args, + commandArgs, role, - nodeAddress, + address, replyEncoding, ); - if (result.error && checkRedirectionError(result.error) && nodeOptions.enableRedirection) { - const { slot, address } = parseRedirectionError(result.error); - result = await this.redisTool.execCommandForNode( - clientOptions, - command, - args, - role, - address, - replyEncoding, - ); - result.slot = parseInt(slot, 10); - } - - const commandType = await this.checkIsCoreCommand(command) ? CommandType.Core : CommandType.Module; - - this.analyticsService.sendCommandExecutedEvent( - clientOptions.instanceId, - result, - { command, commandType, rawMode: mode === RunQueryMode.Raw }, - ); - const { - host, port, error, slot, ...rest - } = result; - - return { - ...rest, - response: formatter.format(rest.response), - node: { host, port, slot }, - }; - } catch (error) { - this.logger.error('Failed to execute redis.cluster CLI command.', error); - const result = { response: error.message, status: CommandExecutionStatus.Fail }; - - if (error instanceof CommandParsingError || error instanceof CommandNotSupportedError) { - this.analyticsService.sendCommandExecutedEvent( - clientOptions.instanceId, - { ...result, error }, - { rawMode: mode === RunQueryMode.Raw }, - ); - return result; - } - this.analyticsService.sendCommandExecutedEvent( - clientOptions.instanceId, - { ...result, error }, - { rawMode: mode === RunQueryMode.Raw }, - ); - - if (error instanceof WrongDatabaseTypeError || error instanceof ClusterNodeNotFoundError) { - throw new BadRequestException(error.message); - } - - throw new InternalServerErrorException(error.message); + result.slot = parseInt(slot, 10); } + + const { + host, port, error, slot, ...rest + } = result; + + return { + ...rest, + response: formatter.format(rest.response), + node: { host, port, slot }, + }; } + /** + * Sends commands for multiple nodes in cluster based on their role + * @param clientOptions + * @param command + * @param commandArgs + * @param role + * @param mode + * @private + */ private async sendCommandForNodes( clientOptions: IFindRedisClientInstanceByOptions, - commandLine: string, + command: string, + commandArgs: string[], role: ClusterNodeRole, mode: RunQueryMode = RunQueryMode.ASCII, ): Promise { this.logger.log(`Executing redis.cluster CLI command for [${role}] nodes.`); - try { - const [command, ...args] = splitCliCommandLine(commandLine); - const formatter = this.getFormatter(mode); - - const replyEncoding = checkHumanReadableCommands(`${command} ${args[0]}`) ? 'utf8' : undefined; - const commandType = await this.checkIsCoreCommand(command) ? CommandType.Core : CommandType.Module; - - return ( - await this.redisTool.execCommandForNodes(clientOptions, command, args, role, replyEncoding) - ).map((nodeExecReply) => { - const { - response, status, host, port, - } = nodeExecReply; - const result = { - response: formatter.format(response), - status, - node: { host, port }, - }; - - this.analyticsService.sendCommandExecutedEvent( - clientOptions.instanceId, - result, - { command, commandType, rawMode: mode === RunQueryMode.Raw }, - ); - return result; - }); - } catch (error) { - this.logger.error('Failed to execute redis.cluster CLI command.', error); - const result = { response: error.message, status: CommandExecutionStatus.Fail }; - - if (error instanceof CommandParsingError || error instanceof CommandNotSupportedError) { - this.analyticsService.sendCommandExecutedEvent( - clientOptions.instanceId, - { ...result, error }, - { rawMode: mode === RunQueryMode.Raw }, - ); - return [result]; - } - this.analyticsService.sendCommandExecutedEvent( - clientOptions.instanceId, - { ...result, error }, - { rawMode: mode === RunQueryMode.Raw }, - ); - if (error instanceof WrongDatabaseTypeError) { - throw new BadRequestException(error.message); - } - throw new InternalServerErrorException(error.message); - } - } + const formatter = this.getFormatter(mode); - private async checkIsCoreCommand(command: string) { - const commands = await this.commandsService.getCommandsGroups(); + const replyEncoding = checkHumanReadableCommands(`${command} ${commandArgs[0]}`) ? 'utf8' : undefined; - return !!commands?.main[command.toUpperCase()]; + return ( + await this.redisTool.execCommandForNodes(clientOptions, command, commandArgs, role, replyEncoding) + ).map((nodeExecReply) => { + const { + response, status, host, port, + } = nodeExecReply; + return { + response: formatter.format(response), + status, + node: { host, port }, + }; + }); } + /** + * Get formatter strategy based on "mode" + * @param mode + * @private + */ private getFormatter(mode: RunQueryMode) { switch (mode) { - case RunQueryMode.ASCII: - return this.formatterManager.getStrategy(FormatterTypes.ASCII); case RunQueryMode.Raw: return this.formatterManager.getStrategy(FormatterTypes.UTF8); + case RunQueryMode.ASCII: default: { return this.formatterManager.getStrategy(FormatterTypes.ASCII); } diff --git a/redisinsight/api/src/modules/workbench/services/workbench-analytics/workbench-analytics.service.spec.ts b/redisinsight/api/src/modules/workbench/services/workbench-analytics/workbench-analytics.service.spec.ts index 449e7a649e..2f9a26c418 100644 --- a/redisinsight/api/src/modules/workbench/services/workbench-analytics/workbench-analytics.service.spec.ts +++ b/redisinsight/api/src/modules/workbench/services/workbench-analytics/workbench-analytics.service.spec.ts @@ -1,11 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ServiceUnavailableException } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { mockRedisWrongTypeError, mockStandaloneDatabaseEntity } from 'src/__mocks__'; -import { TelemetryEvents } from 'src/constants'; +import { mockRedisWrongTypeError, mockStandaloneDatabaseEntity, MockType } from 'src/__mocks__'; +import { CommandType, TelemetryEvents } from 'src/constants'; import { ReplyError } from 'src/models'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { CommandParsingError } from 'src/modules/cli/constants/errors'; +import { CommandsService } from 'src/modules/commands/commands.service'; import { WorkbenchAnalyticsService } from './workbench-analytics.service'; const redisReplyError: ReplyError = { @@ -14,15 +15,26 @@ const redisReplyError: ReplyError = { }; const instanceId = mockStandaloneDatabaseEntity.id; +const mockCommandsService = { + getCommandsGroups: jest.fn(), +}; + describe('WorkbenchAnalyticsService', () => { let service: WorkbenchAnalyticsService; let sendEventMethod: jest.SpyInstance; let sendFailedEventMethod: jest.SpyInstance; + let commandsService: MockType; beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ providers: [ EventEmitter2, + { + provide: CommandsService, + useFactory: () => mockCommandsService, + }, WorkbenchAnalyticsService, ], }).compile(); @@ -36,42 +48,172 @@ describe('WorkbenchAnalyticsService', () => { service, 'sendFailedEvent', ); + + commandsService = module.get(CommandsService); + commandsService.getCommandsGroups.mockResolvedValue({ + main: { + SET: { + summary: 'Set the string value of a key', + since: '1.0.0', + group: 'string', + complexity: 'O(1)', + acl_categories: [ + '@write', + '@string', + '@slow', + ], + }, + }, + redisbloom: { + 'BF.RESERVE': { + summary: 'Creates a new Bloom Filter', + complexity: 'O(1)', + since: '1.0.0', + group: 'bf', + }, + }, + custommodule: { + 'CUSTOM.COMMAND': { + summary: 'Creates a new Bloom Filter', + complexity: 'O(1)', + since: '1.0.0', + }, + }, + }); }); + describe('sendCommandExecutedEvents', () => { + it('should emit multiple events', async () => { + await service.sendCommandExecutedEvents( + instanceId, + [ + { response: 'OK', status: CommandExecutionStatus.Success }, + { response: 'OK', status: CommandExecutionStatus.Success }, + ], + { command: 'set' }, + ); + + expect(sendEventMethod).toHaveBeenCalledTimes(2); + expect(sendEventMethod).toHaveBeenCalledWith( + TelemetryEvents.WorkbenchCommandExecuted, + { + databaseId: instanceId, + command: 'set', + commandType: CommandType.Core, + moduleName: 'n/a', + capability: 'string', + }, + ); + }); + }); describe('sendCommandExecutedEvent', () => { - it('should emit WorkbenchCommandExecuted event', () => { - service.sendCommandExecutedEvent( + it('should emit WorkbenchCommandExecuted event', async () => { + await service.sendCommandExecutedEvent( instanceId, { response: 'OK', status: CommandExecutionStatus.Success }, - { command: 'info' }, + { command: 'set' }, ); expect(sendEventMethod).toHaveBeenCalledWith( TelemetryEvents.WorkbenchCommandExecuted, { databaseId: instanceId, - command: 'info', + command: 'set', + commandType: CommandType.Core, + moduleName: 'n/a', + capability: 'string', + }, + ); + }); + it('should emit event if failed to fetch commands groups', async () => { + commandsService.getCommandsGroups.mockRejectedValue(new Error('some error')); + + await service.sendCommandExecutedEvent( + instanceId, + { response: 'OK', status: CommandExecutionStatus.Success }, + { command: 'set' }, + ); + + expect(sendEventMethod).toHaveBeenCalledWith( + TelemetryEvents.WorkbenchCommandExecuted, + { + databaseId: instanceId, + command: 'set', }, ); }); - it('should emit WorkbenchCommandExecuted event without additional data', () => { - service.sendCommandExecutedEvent( + it('should emit WorkbenchCommandExecuted event (module with cap.)', async () => { + await service.sendCommandExecutedEvent( instanceId, { response: 'OK', status: CommandExecutionStatus.Success }, + { command: 'bF.rEsErvE' }, ); expect(sendEventMethod).toHaveBeenCalledWith( TelemetryEvents.WorkbenchCommandExecuted, { databaseId: instanceId, + command: 'bF.rEsErvE', + commandType: CommandType.Module, + moduleName: 'redisbloom', + capability: 'bf', }, ); }); - it('should emit WorkbenchCommandError event', () => { - service.sendCommandExecutedEvent( + it('should emit WorkbenchCommandExecuted event (module w\\o cap.)', async () => { + await service.sendCommandExecutedEvent( + instanceId, + { response: 'OK', status: CommandExecutionStatus.Success }, + { command: 'CUSTOM.COMMAnd' }, + ); + + expect(sendEventMethod).toHaveBeenCalledWith( + TelemetryEvents.WorkbenchCommandExecuted, + { + databaseId: instanceId, + command: 'CUSTOM.COMMAnd', + commandType: CommandType.Module, + moduleName: 'custommodule', + capability: 'n/a', + }, + ); + }); + it('should emit WorkbenchCommandExecuted event (custom module)', async () => { + await service.sendCommandExecutedEvent( + instanceId, + { response: 'OK', status: CommandExecutionStatus.Success }, + { command: 'some.command' }, + ); + + expect(sendEventMethod).toHaveBeenCalledWith( + TelemetryEvents.WorkbenchCommandExecuted, + { + databaseId: instanceId, + command: 'some.command', + commandType: CommandType.Module, + moduleName: 'custom', + capability: 'n/a', + }, + ); + }); + it('should emit WorkbenchCommandExecuted event without additional data', async () => { + await service.sendCommandExecutedEvent( + instanceId, + { response: 'OK', status: CommandExecutionStatus.Success }, + ); + + expect(sendEventMethod).toHaveBeenCalledWith( + TelemetryEvents.WorkbenchCommandExecuted, + { + databaseId: instanceId, + }, + ); + }); + it('should emit WorkbenchCommandError event', async () => { + await service.sendCommandExecutedEvent( instanceId, { response: 'Error', error: redisReplyError, status: CommandExecutionStatus.Fail }, - { data: 'Some data' }, + { command: 'set', data: 'Some data' }, ); expect(sendEventMethod).toHaveBeenCalledWith( @@ -79,13 +221,16 @@ describe('WorkbenchAnalyticsService', () => { { databaseId: instanceId, error: ReplyError.name, - command: 'sadd', + command: 'set', + commandType: CommandType.Core, + moduleName: 'n/a', + capability: 'string', data: 'Some data', }, ); }); - it('should emit WorkbenchCommandError event without additional data', () => { - service.sendCommandExecutedEvent( + it('should emit WorkbenchCommandError event without additional data', async () => { + await service.sendCommandExecutedEvent( instanceId, { response: 'Error', error: redisReplyError, status: CommandExecutionStatus.Fail }, ); @@ -99,9 +244,9 @@ describe('WorkbenchAnalyticsService', () => { }, ); }); - it('should emit WorkbenchCommandError event for custom error', () => { + it('should emit WorkbenchCommandError event for custom error', async () => { const error: any = CommandParsingError; - service.sendCommandExecutedEvent( + await service.sendCommandExecutedEvent( instanceId, { response: 'Error', status: CommandExecutionStatus.Fail, error }, ); @@ -115,9 +260,9 @@ describe('WorkbenchAnalyticsService', () => { }, ); }); - it('should emit WorkbenchCommandError event for HttpException', () => { + it('should emit WorkbenchCommandError event for HttpException', async () => { const error = new ServiceUnavailableException(); - service.sendCommandExecutedEvent( + await service.sendCommandExecutedEvent( instanceId, { response: 'Error', status: CommandExecutionStatus.Fail, error }, ); @@ -129,7 +274,6 @@ describe('WorkbenchAnalyticsService', () => { ); }); }); - describe('sendCommandDeletedEvent', () => { it('should emit WorkbenchCommandDeleted event', () => { service.sendCommandDeletedEvent( diff --git a/redisinsight/api/src/modules/workbench/services/workbench-analytics/workbench-analytics.service.ts b/redisinsight/api/src/modules/workbench/services/workbench-analytics/workbench-analytics.service.ts index c365051164..b85ed82634 100644 --- a/redisinsight/api/src/modules/workbench/services/workbench-analytics/workbench-analytics.service.ts +++ b/redisinsight/api/src/modules/workbench/services/workbench-analytics/workbench-analytics.service.ts @@ -1,9 +1,10 @@ import { HttpException, Injectable } from '@nestjs/common'; -import { EventEmitter2 } from '@nestjs/event-emitter'; import { TelemetryEvents } from 'src/constants'; -import { TelemetryBaseService } from 'src/modules/shared/services/base/telemetry.base.service'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { RedisError, ReplyError } from 'src/models'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { CommandsService } from 'src/modules/commands/commands.service'; +import { CommandTelemetryBaseService } from 'src/modules/shared/services/base/command.telemetry.base.service'; export interface IExecResult { response: any; @@ -12,12 +13,35 @@ export interface IExecResult { } @Injectable() -export class WorkbenchAnalyticsService extends TelemetryBaseService { - constructor(protected eventEmitter: EventEmitter2) { - super(eventEmitter); +export class WorkbenchAnalyticsService extends CommandTelemetryBaseService { + constructor( + protected eventEmitter: EventEmitter2, + protected readonly commandsService: CommandsService, + ) { + super(eventEmitter, commandsService); + } + + public async sendCommandExecutedEvents( + databaseId: string, + results: IExecResult[], + additionalData: object = {}, + ): Promise { + try { + await Promise.all( + results.map( + (result) => this.sendCommandExecutedEvent(databaseId, result, additionalData), + ), + ); + } catch (e) { + // continue regardless of error + } } - sendCommandExecutedEvent(databaseId: string, result: IExecResult, additionalData: object = {}): void { + public async sendCommandExecutedEvent( + databaseId: string, + result: IExecResult, + additionalData: object = {}, + ): Promise { const { status } = result; try { if (status === CommandExecutionStatus.Success) { @@ -25,12 +49,16 @@ export class WorkbenchAnalyticsService extends TelemetryBaseService { TelemetryEvents.WorkbenchCommandExecuted, { databaseId, + ...(await this.getCommandAdditionalInfo(additionalData['command'])), ...additionalData, }, ); } if (status === CommandExecutionStatus.Fail) { - this.sendCommandErrorEvent(databaseId, result.error, additionalData); + this.sendCommandErrorEvent(databaseId, result.error, { + ...(await this.getCommandAdditionalInfo(additionalData['command'])), + ...additionalData, + }); } } catch (e) { // continue regardless of error diff --git a/redisinsight/api/src/modules/workbench/workbench.controller.ts b/redisinsight/api/src/modules/workbench/workbench.controller.ts index 08709379f1..6784417999 100644 --- a/redisinsight/api/src/modules/workbench/workbench.controller.ts +++ b/redisinsight/api/src/modules/workbench/workbench.controller.ts @@ -26,7 +26,7 @@ export class WorkbenchController { }, ], }) - @Post('/commands-execution') + @Post('/command-executions') @UseInterceptors(ClassSerializerInterceptor) @ApiRedisParams() async sendCommands( diff --git a/redisinsight/api/src/modules/workbench/workbench.module.ts b/redisinsight/api/src/modules/workbench/workbench.module.ts index 4c07c75baf..d7600674c2 100644 --- a/redisinsight/api/src/modules/workbench/workbench.module.ts +++ b/redisinsight/api/src/modules/workbench/workbench.module.ts @@ -20,8 +20,8 @@ import { PluginCommandsWhitelistProvider } from 'src/modules/workbench/providers import { PluginsController } from 'src/modules/workbench/plugins.controller'; import { PluginStateProvider } from 'src/modules/workbench/providers/plugin-state.provider'; import { PluginStateEntity } from 'src/modules/workbench/entities/plugin-state.entity'; -import { WorkbenchAnalyticsService } from './services/workbench-analytics/workbench-analytics.service'; import config from 'src/utils/config'; +import { WorkbenchAnalyticsService } from './services/workbench-analytics/workbench-analytics.service'; const COMMANDS_CONFIGS = config.get('commands'); @@ -52,7 +52,7 @@ const COMMANDS_CONFIGS = config.get('commands'); provide: CommandsService, useFactory: () => new CommandsService( COMMANDS_CONFIGS.map(({ name, url }) => new CommandsJsonProvider(name, url)), - ) + ), }, PluginsService, PluginCommandsWhitelistProvider, diff --git a/redisinsight/api/src/modules/workbench/workbench.service.spec.ts b/redisinsight/api/src/modules/workbench/workbench.service.spec.ts index b22ae1e1bd..337ca8e678 100644 --- a/redisinsight/api/src/modules/workbench/workbench.service.spec.ts +++ b/redisinsight/api/src/modules/workbench/workbench.service.spec.ts @@ -1,5 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { v4 as uuidv4 } from 'uuid'; +import { when } from 'jest-when'; import { mockStandaloneDatabaseEntity, mockWorkbenchAnalyticsService } from 'src/__mocks__'; import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service'; import { WorkbenchService } from 'src/modules/workbench/workbench.service'; @@ -9,12 +10,14 @@ import { ClusterNodeRole, CreateCommandExecutionDto, RunQueryMode, + ResultsMode, } from 'src/modules/workbench/dto/create-command-execution.dto'; import { CommandExecution } from 'src/modules/workbench/models/command-execution'; import { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { BadRequestException, InternalServerErrorException } from '@nestjs/common'; import ERROR_MESSAGES from 'src/constants/error-messages'; +import { CreateCommandExecutionsDto } from 'src/modules/workbench/dto/create-command-executions.dto'; import { WorkbenchAnalyticsService } from './services/workbench-analytics/workbench-analytics.service'; const mockClientOptions: IFindRedisClientInstanceByOptions = { @@ -30,6 +33,29 @@ const mockCreateCommandExecutionDto: CreateCommandExecutionDto = { }, role: ClusterNodeRole.All, mode: RunQueryMode.ASCII, + resultsMode: ResultsMode.Default, +}; + +const mockCommands = ["set 1 1", "get 1"]; + +const mockCreateCommandExecutionDtoWithGroupMode: CreateCommandExecutionsDto = { + commands: mockCommands, + nodeOptions: { + host: '127.0.0.1', + port: 7002, + enableRedirection: true, + }, + role: ClusterNodeRole.All, + mode: RunQueryMode.ASCII, + resultsMode: ResultsMode.GroupMode, +}; + +const mockCreateCommandExecutionsDto: CreateCommandExecutionsDto = { + commands: [ + mockCreateCommandExecutionDto.command, + mockCreateCommandExecutionDto.command, + ], + ...mockCreateCommandExecutionDto, }; const mockCommandExecutionResults: CommandExecutionResult[] = [ @@ -43,16 +69,35 @@ const mockCommandExecutionResults: CommandExecutionResult[] = [ }, }), ]; -const mockCommandExecution: CommandExecution = new CommandExecution({ +const mockCommandExecutionToRun: CommandExecution = new CommandExecution({ ...mockCreateCommandExecutionDto, databaseId: mockStandaloneDatabaseEntity.id, +}); + +const mockCommandExecution: CommandExecution = new CommandExecution({ + ...mockCommandExecutionToRun, id: uuidv4(), createdAt: new Date(), result: mockCommandExecutionResults, }); +const mockSendCommandResultSuccess = { response: "1", status: "success" }; +const mockSendCommandResultFail = { response: "error", status: "fail" }; + +const mockCommandExecutionWithGroupMode = { + mode: "ASCII", + commands: mockCommands, + resultsMode: "GROUP_MODE", + databaseId: "d05043d0 - 0d12- 4ce1-9ca3 - 30c6d7e391ea", + summary: { "total": 2, "success": 1, "fail": 1 }, + command: "set 1 1\r\nget 1", + result: [{ + "status": "success", "response": [{ "response": "OK", "status": "success", "command": "set 1 1" }, { "response": "error", "status": "fail", "command": "get 1" }] + }] +} + const mockCommandExecutionProvider = () => ({ - create: jest.fn(), + createMany: jest.fn(), getList: jest.fn(), getOne: jest.fn(), delete: jest.fn(), @@ -91,13 +136,8 @@ describe('WorkbenchService', () => { describe('createCommandExecution', () => { it('should successfully execute command and save it', async () => { - workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce(mockCommandExecutionResults); - commandExecutionProvider.create.mockResolvedValueOnce(mockCommandExecution); - - const result = await service.createCommandExecution(mockClientOptions, mockCreateCommandExecutionDto); - - expect(result).toBeInstanceOf(CommandExecution); - expect(result).toEqual(mockCommandExecution); + expect(await service.createCommandExecution(mockClientOptions, mockCreateCommandExecutionDto)) + .toEqual(mockCommandExecutionToRun); }); it('should save result as unsupported command message', async () => { workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce(mockCommandExecutionResults); @@ -108,9 +148,7 @@ describe('WorkbenchService', () => { mode: RunQueryMode.ASCII, }; - await service.createCommandExecution(mockClientOptions, dto); - - expect(commandExecutionProvider.create).toHaveBeenCalledWith({ + expect(await service.createCommandExecution(mockClientOptions, dto)).toEqual({ ...dto, databaseId: mockClientOptions.instanceId, result: [ @@ -137,18 +175,70 @@ describe('WorkbenchService', () => { expect(e).toBeInstanceOf(BadRequestException); } }); - it('should throw an error from command execution provider (create)', async () => { - workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce(mockCommandExecutionResults); - commandExecutionProvider.create.mockRejectedValueOnce(new InternalServerErrorException('db error')); + }); - const dto = { - ...mockCommandExecutionResults, - command: 'scan 0', - mode: RunQueryMode.ASCII, - }; + describe('createCommandExecutions', () => { + it('should successfully execute commands and save them', async () => { + workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce( + [mockCommandExecutionResults, mockCommandExecutionResults], + ); + commandExecutionProvider.createMany.mockResolvedValueOnce([mockCommandExecution, mockCommandExecution]); + + const result = await service.createCommandExecutions(mockClientOptions, mockCreateCommandExecutionsDto); + + expect(result).toEqual([mockCommandExecution, mockCommandExecution]); + }); + + it('should successfully execute commands and save in group mode view', async () => { + when(workbenchCommandsExecutor.sendCommand) + .calledWith(mockClientOptions, expect.anything()) + .mockResolvedValue([mockSendCommandResultSuccess]); + + commandExecutionProvider.createMany.mockResolvedValueOnce([mockCommandExecutionWithGroupMode]); + + const result = await service.createCommandExecutions( + mockClientOptions, + mockCreateCommandExecutionDtoWithGroupMode, + ); + + expect(result).toEqual([mockCommandExecutionWithGroupMode]); + }); + + it('should successfully execute commands with error and save summary', async () => { + when(workbenchCommandsExecutor.sendCommand) + .calledWith(mockClientOptions, {...mockCreateCommandExecutionDtoWithGroupMode, command: mockCommands[0]}) + .mockResolvedValue([mockSendCommandResultSuccess]); + + when(workbenchCommandsExecutor.sendCommand) + .calledWith(mockClientOptions, {...mockCreateCommandExecutionDtoWithGroupMode, command: mockCommands[1]}) + .mockResolvedValue([mockSendCommandResultFail]); + + commandExecutionProvider.createMany.mockResolvedValueOnce([mockCommandExecutionWithGroupMode]); + + const result = await service.createCommandExecutions( + mockClientOptions, + mockCreateCommandExecutionDtoWithGroupMode, + ); + + expect(result).toEqual([mockCommandExecutionWithGroupMode]); + }); + + it('should throw an error when command execution failed', async () => { + workbenchCommandsExecutor.sendCommand.mockRejectedValueOnce(new BadRequestException('error')); try { - await service.createCommandExecution(mockClientOptions, dto); + await service.createCommandExecutions(mockClientOptions, mockCreateCommandExecutionsDto); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); + it('should throw an error from command execution provider (create)', async () => { + workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce([mockCommandExecutionResults]); + commandExecutionProvider.createMany.mockRejectedValueOnce(new InternalServerErrorException('db error')); + + try { + await service.createCommandExecutions(mockClientOptions, mockCreateCommandExecutionsDto); fail(); } catch (e) { expect(e).toBeInstanceOf(InternalServerErrorException); diff --git a/redisinsight/api/src/modules/workbench/workbench.service.ts b/redisinsight/api/src/modules/workbench/workbench.service.ts index 3b2ed11e7f..cdd99278d1 100644 --- a/redisinsight/api/src/modules/workbench/workbench.service.ts +++ b/redisinsight/api/src/modules/workbench/workbench.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@nestjs/common'; +import { omit } from 'lodash'; import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service'; import { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor'; import { CommandExecutionProvider } from 'src/modules/workbench/providers/command-execution.provider'; import { CommandExecution } from 'src/modules/workbench/models/command-execution'; -import { CreateCommandExecutionDto } from 'src/modules/workbench/dto/create-command-execution.dto'; +import { CreateCommandExecutionDto, ResultsMode } from 'src/modules/workbench/dto/create-command-execution.dto'; import { CreateCommandExecutionsDto } from 'src/modules/workbench/dto/create-command-executions.dto'; import { getBlockingCommands, multilineCommandToOneLine } from 'src/utils/cli-helper'; import ERROR_MESSAGES from 'src/constants/error-messages'; @@ -29,9 +30,9 @@ export class WorkbenchService { async createCommandExecution( clientOptions: IFindRedisClientInstanceByOptions, dto: CreateCommandExecutionDto, - ): Promise { + ): Promise> { const commandExecution: Partial = { - ...dto, + ...omit(dto, 'commands'), databaseId: clientOptions.instanceId, }; @@ -48,7 +49,59 @@ export class WorkbenchService { commandExecution.result = await this.commandsExecutor.sendCommand(clientOptions, { ...dto, command }); } - return this.commandExecutionProvider.create(commandExecution); + return commandExecution; + } + + /** + * Send redis command from workbench and save history + * + * @param clientOptions + * @param dto + */ + async createCommandsExecution( + clientOptions: IFindRedisClientInstanceByOptions, + dto: Partial, + commands: string[], + ): Promise> { + const commandExecution: Partial = { + ...dto, + databaseId: clientOptions.instanceId, + }; + + const executionResults = await Promise.all(commands.map(async (singleCommand) => { + const command = multilineCommandToOneLine(singleCommand); + const deprecatedCommand = this.findCommandInBlackList(command); + if (deprecatedCommand) { + return ({ + command, + response: ERROR_MESSAGES.WORKBENCH_COMMAND_NOT_SUPPORTED(deprecatedCommand.toUpperCase()), + status: CommandExecutionStatus.Fail, + }); + } + const result = await this.commandsExecutor.sendCommand(clientOptions, { ...dto, command }); + return ({ ...result[0], command }); + })); + + const successCommands = executionResults.filter( + (command) => command.status === CommandExecutionStatus.Success, + ); + const failedCommands = executionResults.filter( + (command) => command.status === CommandExecutionStatus.Fail, + ); + + commandExecution.summary = { + total: executionResults.length, + success: successCommands.length, + fail: failedCommands.length, + }; + + commandExecution.command = commands.join('\r\n'); + commandExecution.result = [{ + status: CommandExecutionStatus.Success, + response: executionResults, + }]; + + return commandExecution; } /** @@ -61,9 +114,20 @@ export class WorkbenchService { clientOptions: IFindRedisClientInstanceByOptions, dto: CreateCommandExecutionsDto, ): Promise { - return Promise.all( + if (dto.resultsMode === ResultsMode.GroupMode) { + return this.commandExecutionProvider.createMany( + [await this.createCommandsExecution(clientOptions, dto, dto.commands)], + ); + } + // todo: rework to support pipeline + // prepare and execute commands + const commandExecutions = await Promise.all( dto.commands.map(async (command) => await this.createCommandExecution(clientOptions, { ...dto, command })), ); + + // save history + // todo: rework + return this.commandExecutionProvider.createMany(commandExecutions); } /** diff --git a/redisinsight/api/src/utils/converter.spec.ts b/redisinsight/api/src/utils/converter.spec.ts index 5999d2902d..28123d3aec 100644 --- a/redisinsight/api/src/utils/converter.spec.ts +++ b/redisinsight/api/src/utils/converter.spec.ts @@ -1,5 +1,9 @@ import { flatMap } from 'lodash'; -import { convertStringsArrayToObject, convertIntToSemanticVersion } from './converter'; +import { + convertStringsArrayToObject, + convertIntToSemanticVersion, + convertStringToNumber, +} from './converter'; describe('convertStringsArrayToObject', () => { it('should return appropriate value', () => { @@ -42,3 +46,23 @@ describe('convertIntToSemanticVersionTests', () => { }); }); }); + +const convertStringToNumberTests: Record[] = [ + { input: ['1'], output: 1 }, + { input: [1], output: 1 }, + { input: [{ some: 'obj' }], output: undefined }, + { input: [{ some: 'obj' }, 11], output: 11 }, + { input: ['asd45', 11], output: 11 }, + { input: ['123.123', 11], output: 123.123 }, + { input: [undefined, 11], output: 11 }, +]; + +describe('convertStringToNumber', () => { + convertStringToNumberTests.forEach((test) => { + it(`should be output: ${test.output} for input: ${JSON.stringify(test.input)}`, () => { + const result = convertStringToNumber.call(this, ...test.input); + + expect(result).toEqual(test.output); + }); + }); +}); diff --git a/redisinsight/api/src/utils/converter.ts b/redisinsight/api/src/utils/converter.ts index 5be0361dce..94327b6f96 100644 --- a/redisinsight/api/src/utils/converter.ts +++ b/redisinsight/api/src/utils/converter.ts @@ -1,4 +1,6 @@ -import { chunk, isInteger } from 'lodash'; +import { + chunk, isInteger, isString, isNumber, isNaN, +} from 'lodash'; export const convertStringsArrayToObject = (input: string[]): { [key: string]: any } => chunk( input, @@ -24,3 +26,21 @@ export const convertIntToSemanticVersion = (input: number): string => { return undefined; } }; + +export const convertStringToNumber = (value: any, defaultValue?: number): number => { + if (isNumber(value)) { + return value; + } + + if (!isString(value) || !value) { + return defaultValue; + } + + const num = parseFloat(value); + + if (isNaN(num)) { + return defaultValue; + } + + return num; +}; diff --git a/redisinsight/api/src/utils/redis-connection-helper.spec.ts b/redisinsight/api/src/utils/redis-connection-helper.spec.ts index aecf9aab1e..1b727d39c0 100644 --- a/redisinsight/api/src/utils/redis-connection-helper.spec.ts +++ b/redisinsight/api/src/utils/redis-connection-helper.spec.ts @@ -1,4 +1,5 @@ -import * as Redis from 'ioredis'; +import * as IORedis from 'ioredis'; +import * as Redis from 'ioredis-mock'; import { CONNECTION_NAME_GLOBAL_PREFIX } from 'src/constants'; import { generateRedisConnectionName, @@ -8,17 +9,22 @@ import { const CLIENT_ID = '235e72f4-601f-4d01-8399-b5c51b617dc4'; -const mockClient = Object.create(Redis.prototype); -mockClient.options = { - ...mockClient.options, +const mockOptions = { host: 'localhost', port: 6379, connectionName: `${CONNECTION_NAME_GLOBAL_PREFIX}-common-235e72f4`, }; -const mockCluster = Object.create(Redis.Cluster.prototype); +const mockClient = new Redis('redis://localhost:6379', { lazyConnect: true }); +mockClient.options = { + ...mockClient.options, + ...mockOptions, +}; + +const mockCluster = Object.create(IORedis.Cluster.prototype); +mockCluster.isCluster = true; mockCluster.options = { - redisOptions: mockClient.options, + redisOptions: mockOptions, }; const generateRedisConnectionNameTests = [ diff --git a/redisinsight/api/src/utils/redis-connection-helper.ts b/redisinsight/api/src/utils/redis-connection-helper.ts index 7b49d1c095..88cbc8e9e1 100644 --- a/redisinsight/api/src/utils/redis-connection-helper.ts +++ b/redisinsight/api/src/utils/redis-connection-helper.ts @@ -1,4 +1,4 @@ -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; import { get } from 'lodash'; import { CONNECTION_NAME_GLOBAL_PREFIX } from 'src/constants'; @@ -12,7 +12,7 @@ export const generateRedisConnectionName = (namespace: string, id: string, separ export const getConnectionName = (client: IORedis.Redis | IORedis.Cluster): string => { try { - if (client instanceof IORedis.Cluster) { + if (client.isCluster) { return get(client, 'options.redisOptions.connectionName', CONNECTION_NAME_GLOBAL_PREFIX); } return get(client, 'options.connectionName', CONNECTION_NAME_GLOBAL_PREFIX); diff --git a/redisinsight/api/test/api/cli/POST-instance-id-cli-uuid-send_command.test.ts b/redisinsight/api/test/api/cli/POST-instance-id-cli-uuid-send_command.test.ts index 26a85ed1d7..bcac450d31 100644 --- a/redisinsight/api/test/api/cli/POST-instance-id-cli-uuid-send_command.test.ts +++ b/redisinsight/api/test/api/cli/POST-instance-id-cli-uuid-send_command.test.ts @@ -446,10 +446,10 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { expect(body.response).to.have.string('"OK"'); }, before: async () => { - expect(await rte.client.send_command('ft._list')).to.not.include(constants.TEST_SEARCH_HASH_INDEX_1); + expect(await rte.client.call('ft._list')).to.not.include(constants.TEST_SEARCH_HASH_INDEX_1); }, after: async () => { - expect(await rte.client.send_command(`ft._list`)).to.include(constants.TEST_SEARCH_HASH_INDEX_1); + expect(await rte.client.call(`ft._list`)).to.include(constants.TEST_SEARCH_HASH_INDEX_1); }, }, { @@ -518,7 +518,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { }, responseSchema, after: async () => { - expect(await rte.client.send_command('ft._list')).to.not.include(constants.TEST_SEARCH_HASH_INDEX_1); + expect(await rte.client.call('ft._list')).to.not.include(constants.TEST_SEARCH_HASH_INDEX_1); } }, ].map(mainCheckFn); @@ -544,10 +544,10 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { expect(body.response).to.have.string('"OK"'); }, before: async () => { - expect(await rte.client.send_command('ft._list')).to.not.include(constants.TEST_SEARCH_JSON_INDEX_1); + expect(await rte.client.call('ft._list')).to.not.include(constants.TEST_SEARCH_JSON_INDEX_1); }, after: async () => { - expect(await rte.client.send_command(`ft._list`)).to.include(constants.TEST_SEARCH_JSON_INDEX_1); + expect(await rte.client.call(`ft._list`)).to.include(constants.TEST_SEARCH_JSON_INDEX_1); }, }, { @@ -577,7 +577,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { responseSchema, before: async () => { for (let i = 0; i < 10; i++) { - await rte.client.send_command( + await rte.client.call( 'json.set', [`${constants.TEST_SEARCH_JSON_KEY_PREFIX_1}${i}`, '$', `{"user":{"name":"John Smith${i}"}}`] ) @@ -606,7 +606,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { }, responseSchema, after: async () => { - expect(await rte.client.send_command('ft._list')).to.not.include(constants.TEST_SEARCH_JSON_INDEX_1); + expect(await rte.client.call('ft._list')).to.not.include(constants.TEST_SEARCH_JSON_INDEX_1); } }, ].map(mainCheckFn); @@ -629,14 +629,14 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { before: async () => { let errorMessage = ''; try { - await rte.client.send_command('ft.info', [constants.TEST_SEARCH_HASH_INDEX_1]) + await rte.client.call('ft.info', [constants.TEST_SEARCH_HASH_INDEX_1]) } catch ({message}) { errorMessage = message; } expect(errorMessage).to.eql('Unknown Index name') }, after: async () => { - expect(await rte.client.send_command('ft.info', [constants.TEST_SEARCH_HASH_INDEX_1])) + expect(await rte.client.call('ft.info', [constants.TEST_SEARCH_HASH_INDEX_1])) .to.include(constants.TEST_SEARCH_HASH_INDEX_1) }, }, @@ -665,7 +665,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { responseSchema, before: async () => { for (let i = 0; i < 10; i++) { - await rte.client.send_command( + await rte.client.call( 'ft.add', [constants.TEST_SEARCH_HASH_INDEX_1, `${constants.TEST_SEARCH_HASH_KEY_PREFIX_1}${i}`, '1.0', 'FIELDS', 'title', 'hello world'] ) @@ -685,7 +685,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { after: async () => { let errorMessage = ''; try { - await rte.client.send_command('ft.info', [constants.TEST_SEARCH_HASH_INDEX_1]) + await rte.client.call('ft.info', [constants.TEST_SEARCH_HASH_INDEX_1]) } catch ({message}) { errorMessage = message; } diff --git a/redisinsight/api/test/api/cluster-monitor/GET-instance-id-cluster_details.test.ts b/redisinsight/api/test/api/cluster-monitor/GET-instance-id-cluster_details.test.ts new file mode 100644 index 0000000000..af5e4e08a9 --- /dev/null +++ b/redisinsight/api/test/api/cluster-monitor/GET-instance-id-cluster_details.test.ts @@ -0,0 +1,144 @@ +import { describe, it, deps, validateApiCall, before, expect, requirements, getMainCheckFn } from '../deps'; +import { Joi } from '../../helpers/test'; +const { localDb, request, server, constants, rte } = deps; + +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).get(`/instance/${instanceId}/cluster-details`); + +const nodeSchema = Joi.object().keys({ + id: Joi.string().required(), + version: Joi.string().required(), + mode: Joi.string().required(), + host: Joi.string().required(), + port: Joi.number(), + role: Joi.string().required(), + slots: Joi.array().items(Joi.string()).required(), + health: Joi.string().required(), + totalKeys: Joi.number().allow(null), + usedMemory: Joi.number().allow(null), + opsPerSecond: Joi.number().allow(null), + connectionsReceived: Joi.number().allow(null), + connectedClients: Joi.number().allow(null), + commandsProcessed: Joi.number().allow(null), + networkInKbps: Joi.number().allow(null), + networkOutKbps: Joi.number().allow(null), + cacheHitRatio: Joi.number().allow(null), + replicationOffset: Joi.number().allow(null), + uptimeSec: Joi.number().allow(null), + replicas: Joi.array().items(this), +}).required(); + +const responseSchema = Joi.object().keys({ + user: Joi.string(), + version: Joi.string().required(), + mode: Joi.string().required(), + state: Joi.string().required(), + slotsAssigned: Joi.number().allow(null), + slotsOk: Joi.number().allow(null), + slotsPFail: Joi.number().allow(null), + slotsFail: Joi.number().allow(null), + slotsUnassigned: Joi.number().allow(null), + statsMessagesSent: Joi.number().allow(null), + statsMessagesReceived: Joi.number().allow(null), + currentEpoch: Joi.number().allow(null), + myEpoch: Joi.number().allow(null), + size: Joi.number().allow(null), + knownNodes: Joi.number().allow(null), + uptimeSec: Joi.number().allow(null), + nodes: Joi.array().items(nodeSchema).min(0).required(), +}).required(); + +const mainCheckFn = getMainCheckFn(endpoint); + +describe('GET /instance/:instanceId/cluster-details', () => { + before(localDb.createDatabaseInstances); + + describe('Common', () => { + [ + { + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), + name: 'Should not connect to a database due to misconfiguration', + statusCode: 503, + responseBody: { + statusCode: 503, + error: 'Service Unavailable' + }, + }, + { + name: 'Should return NotFound error if instance id does not exists', + endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID), + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Invalid database instance id.', + }, + }, + ].map(mainCheckFn); + + }); + + describe('Any non-cluster', () => { + requirements('rte.type<>CLUSTER'); + [ + { + name: 'Should return BadRequest for non-cluster databases', + statusCode: 400, + responseBody: { + statusCode: 400, + error: 'Bad Request', + message: 'Current database is not in a cluster mode' + }, + }, + ].map(mainCheckFn); + }); + + describe('Cluster', () => { + requirements('rte.type=CLUSTER'); + [ + { + name: 'Should get cluster details', + responseSchema, + checkFn: ({body}) => { + expect(body.version).to.eql(rte.env.version); + } + }, + ].map(mainCheckFn); + + describe('ACL', () => { + requirements('rte.acl'); + before(async () => rte.data.setAclUserRules('~* +@all')); + + [ + { + name: 'Should return details in positive case', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + responseSchema, + checkFn: ({body}) => { + expect(body.version).to.eql(rte.env.version); + }, + }, + { + before: () => rte.data.setAclUserRules('~* +@all -cluster'), + name: 'Should throw error if no permissions for "cluster" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + }, + { + before: () => rte.data.setAclUserRules('~* +@all -info'), + name: 'Should throw error if no permissions for "info" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + }, + ].map(mainCheckFn); + }); + }); +}); diff --git a/redisinsight/api/test/api/instance/DELETE-instance-id.test.ts b/redisinsight/api/test/api/instance/DELETE-instance-id.test.ts index e0ef1c51c3..5a23a59b8d 100644 --- a/redisinsight/api/test/api/instance/DELETE-instance-id.test.ts +++ b/redisinsight/api/test/api/instance/DELETE-instance-id.test.ts @@ -39,10 +39,10 @@ describe('DELETE /instance/:id', () => { name: 'Should remove single database', endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), before: async () => { - expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2)).to.not.eql(undefined) + expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2)).to.be.an('object') }, after: async () => { - expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2)).to.eql(undefined) + expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2)).to.eql(null) }, }, { @@ -55,7 +55,7 @@ describe('DELETE /instance/:id', () => { error: 'Not Found' }, before: async () => { - expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2)).to.eql(undefined) + expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2)).to.eql(null) }, }, ].map(mainCheckFn); diff --git a/redisinsight/api/test/api/instance/DELETE-instance.test.ts b/redisinsight/api/test/api/instance/DELETE-instance.test.ts index d6c734d101..39fcbb54c6 100644 --- a/redisinsight/api/test/api/instance/DELETE-instance.test.ts +++ b/redisinsight/api/test/api/instance/DELETE-instance.test.ts @@ -62,12 +62,12 @@ describe('DELETE /instance', () => { affected: 2, }, before: async () => { - expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2)).to.not.eql(undefined) - expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_3)).to.not.eql(undefined) + expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2)).to.be.an('object') + expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_3)).to.be.an('object') }, after: async () => { - expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2)).to.eql(undefined) - expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_3)).to.eql(undefined) + expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2)).to.eql(null) + expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_3)).to.eql(null) }, }, { @@ -79,8 +79,8 @@ describe('DELETE /instance', () => { affected: 0, }, before: async () => { - expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2)).to.eql(undefined) - expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_3)).to.eql(undefined) + expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_2)).to.eql(null) + expect(await localDb.getInstanceByName(constants.TEST_INSTANCE_NAME_3)).to.eql(null) }, }, ].map(mainCheckFn); diff --git a/redisinsight/api/test/api/instance/POST-instance-sentinel_masters.test.ts b/redisinsight/api/test/api/instance/POST-instance-sentinel_masters.test.ts index d81291beef..f73781fc14 100644 --- a/redisinsight/api/test/api/instance/POST-instance-sentinel_masters.test.ts +++ b/redisinsight/api/test/api/instance/POST-instance-sentinel_masters.test.ts @@ -43,7 +43,9 @@ describe('POST /instance/sentinel-masters', () => { expect(body[0].status).to.eql('success'); expect(body[0].message).to.eql('Added'); - const db: any = await (await localDb.getRepository(localDb.repositories.INSTANCE)).findOne(body[0].id); + const db: any = await (await localDb.getRepository(localDb.repositories.INSTANCE)).findOneBy({ + id: body[0].id, + }); expect(db.password).to.eql(localDb.encryptData(constants.TEST_REDIS_PASSWORD)); expect(db.sentinelMasterPassword).to.eql(localDb.encryptData(constants.TEST_SENTINEL_MASTER_PASS)); @@ -79,7 +81,9 @@ describe('POST /instance/sentinel-masters', () => { expect(body[0].status).to.eql('success'); expect(body[0].message).to.eql('Added'); - const db: any = await (await localDb.getRepository(localDb.repositories.INSTANCE)).findOne(body[0].id); + const db: any = await (await localDb.getRepository(localDb.repositories.INSTANCE)).findOneBy({ + id: body[0].id, + }); expect(db.password).to.eql(constants.TEST_REDIS_PASSWORD); expect(db.sentinelMasterPassword).to.eql(constants.TEST_SENTINEL_MASTER_PASS); diff --git a/redisinsight/api/test/api/instance/POST-instance.test.ts b/redisinsight/api/test/api/instance/POST-instance.test.ts index 4d10ec4805..f05cd38ef0 100644 --- a/redisinsight/api/test/api/instance/POST-instance.test.ts +++ b/redisinsight/api/test/api/instance/POST-instance.test.ts @@ -156,7 +156,7 @@ describe('POST /instance', () => { const dbName = constants.getRandomString(); // preconditions - expect(await localDb.getInstanceByName(dbName)).to.eql(undefined); + expect(await localDb.getInstanceByName(dbName)).to.eql(null); await validateApiCall({ endpoint, @@ -187,7 +187,7 @@ describe('POST /instance', () => { it('Create standalone instance using tls without CA verify', async () => { const dbName = constants.getRandomString(); // preconditions - expect(await localDb.getInstanceByName(dbName)).to.eql(undefined); + expect(await localDb.getInstanceByName(dbName)).to.eql(null); await validateApiCall({ endpoint, @@ -218,7 +218,7 @@ describe('POST /instance', () => { const dbName = constants.getRandomString(); const newCaName = constants.getRandomString(); // preconditions - expect(await localDb.getInstanceByName(dbName)).to.eql(undefined); + expect(await localDb.getInstanceByName(dbName)).to.eql(null); await validateApiCall({ endpoint, @@ -253,7 +253,7 @@ describe('POST /instance', () => { const dbName = constants.getRandomString(); // preconditions - expect(await localDb.getInstanceByName(dbName)).to.eql(undefined); + expect(await localDb.getInstanceByName(dbName)).to.eql(null); await validateApiCall({ endpoint, @@ -274,12 +274,12 @@ describe('POST /instance', () => { }, }); - expect(await localDb.getInstanceByName(dbName)).to.eql(undefined); + expect(await localDb.getInstanceByName(dbName)).to.eql(null); }); it('Should throw an error with invalid CA cert', async () => { const dbName = constants.getRandomString(); // preconditions - expect(await localDb.getInstanceByName(dbName)).to.eql(undefined); + expect(await localDb.getInstanceByName(dbName)).to.eql(null); await validateApiCall({ endpoint, @@ -304,7 +304,7 @@ describe('POST /instance', () => { }, }); - expect(await localDb.getInstanceByName(dbName)).to.eql(undefined); + expect(await localDb.getInstanceByName(dbName)).to.eql(null); }); }); describe('Create standalone instance tls with certificate auth', function () { @@ -320,7 +320,7 @@ describe('POST /instance', () => { const newCaName = constants.getRandomString(); const newClientCertName = existingClientCertName = constants.getRandomString(); // preconditions - expect(await localDb.getInstanceByName(dbName)).to.eql(undefined); + expect(await localDb.getInstanceByName(dbName)).to.eql(null); const { body } = await validateApiCall({ endpoint, @@ -354,12 +354,12 @@ describe('POST /instance', () => { }, checkFn: async ({ body }) => { const ca: any = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)) - .findOne(body.tls.caCertId); + .findOneBy({ id: body.tls.caCertId }); expect(ca.certificate).to.eql(localDb.encryptData(constants.TEST_REDIS_TLS_CA)); const clientPair: any = await (await localDb.getRepository(localDb.repositories.CLIENT_CERT_REPOSITORY)) - .findOne(body.tls.clientCertPairId); + .findOneBy({ id: body.tls.clientCertPairId }); expect(clientPair.certificate).to.eql(localDb.encryptData(constants.TEST_USER_TLS_CERT)); expect(clientPair.key).to.eql(localDb.encryptData(constants.TEST_USER_TLS_KEY)); @@ -377,7 +377,7 @@ describe('POST /instance', () => { const dbName = constants.getRandomString(); // preconditions - expect(await localDb.getInstanceByName(dbName)).to.eql(undefined); + expect(await localDb.getInstanceByName(dbName)).to.eql(null); await validateApiCall({ endpoint, @@ -413,7 +413,7 @@ describe('POST /instance', () => { const newCaName = constants.getRandomString(); // preconditions - expect(await localDb.getInstanceByName(dbName)).to.eql(undefined); + expect(await localDb.getInstanceByName(dbName)).to.eql(null); await validateApiCall({ endpoint, @@ -442,7 +442,7 @@ describe('POST /instance', () => { }, }); - expect(await localDb.getInstanceByName(dbName)).to.eql(undefined); + expect(await localDb.getInstanceByName(dbName)).to.eql(null); }); it('Create standalone instance and verify users certs (new certificates !do not encrypt)', async () => { await localDb.setAgreements({ @@ -453,7 +453,7 @@ describe('POST /instance', () => { const newCaName = constants.getRandomString(); const newClientCertName = constants.getRandomString(); // preconditions - expect(await localDb.getInstanceByName(dbName)).to.eql(undefined); + expect(await localDb.getInstanceByName(dbName)).to.eql(null); await validateApiCall({ endpoint, @@ -487,12 +487,12 @@ describe('POST /instance', () => { }, checkFn: async ({ body }) => { const ca: any = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)) - .findOne(body.tls.caCertId); + .findOneBy({ id: body.tls.caCertId }); expect(ca.certificate).to.eql(constants.TEST_REDIS_TLS_CA); const clientPair: any = await (await localDb.getRepository(localDb.repositories.CLIENT_CERT_REPOSITORY)) - .findOne(body.tls.clientCertPairId); + .findOneBy({ id: body.tls.clientCertPairId }); expect(clientPair.certificate).to.eql(constants.TEST_USER_TLS_CERT); expect(clientPair.key).to.eql(constants.TEST_USER_TLS_KEY); diff --git a/redisinsight/api/test/api/instance/PUT-instance-id.test.ts b/redisinsight/api/test/api/instance/PUT-instance-id.test.ts index 672f18d8e3..ab4b9076e6 100644 --- a/redisinsight/api/test/api/instance/PUT-instance-id.test.ts +++ b/redisinsight/api/test/api/instance/PUT-instance-id.test.ts @@ -66,7 +66,7 @@ describe('PUT /instance/:id', () => { port: constants.TEST_REDIS_PORT, }, before: async () => { - expect(await localDb.getInstanceByName('new name')).to.eql(undefined) + expect(await localDb.getInstanceByName('new name')).to.eql(null) }, after: async () => { const newDb = await localDb.getInstanceByName('new name'); diff --git a/redisinsight/api/test/api/plugins/POST-instance-id-plugins-command_executions.test.ts b/redisinsight/api/test/api/plugins/POST-instance-id-plugins-command_executions.test.ts index f6944a4ac6..32c3957060 100644 --- a/redisinsight/api/test/api/plugins/POST-instance-id-plugins-command_executions.test.ts +++ b/redisinsight/api/test/api/plugins/POST-instance-id-plugins-command_executions.test.ts @@ -21,6 +21,7 @@ const dataSchema = Joi.object({ command: Joi.string().required(), role: Joi.string().valid('ALL', 'MASTER', 'SLAVE').allow(null), mode: Joi.string().valid('RAW', 'ASCII').allow(null), + resultsMode: Joi.string().valid('DEFAULT', 'GROUP_MODE').allow(null), nodeOptions: Joi.object().keys({ host: Joi.string().required(), // todo: fix BE transform to avoid handle boolean as number @@ -37,6 +38,7 @@ const validInputData = { command: 'set foo bar', role: 'ALL', mode: 'ASCII', + resultsMode: 'DEFAULT', nodeOptions: { host: 'localhost', port: 6379, @@ -58,6 +60,7 @@ const responseSchema = Joi.object().keys({ })), role: Joi.string().allow(null), mode: Joi.string().required(), + resultsMode: Joi.string().required(), nodeOptions: Joi.object().keys({ host: Joi.string().required(), port: Joi.number().required(), @@ -105,6 +108,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: 'get foo', mode: 'ASCII', + resultsMode: 'DEFAULT', }, responseBody: { statusCode: 404, @@ -117,6 +121,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `get ${constants.TEST_STRING_KEY_1}`, mode: 'ASCII', + resultsMode: 'DEFAULT', }, responseSchema, checkFn: async ({ body }) => { @@ -139,6 +144,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `ft.info ${constants.TEST_STRING_KEY_1}`, mode: 'ASCII', + resultsMode: 'DEFAULT', }, responseSchema, }, @@ -151,6 +157,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `monitor`, mode: 'ASCII', + resultsMode: 'DEFAULT', }, }, { @@ -158,6 +165,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `subscribe`, mode: 'ASCII', + resultsMode: 'DEFAULT', }, }, { @@ -165,6 +173,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `psubscribe`, mode: 'ASCII', + resultsMode: 'DEFAULT', }, }, { @@ -172,6 +181,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `sync`, mode: 'ASCII', + resultsMode: 'DEFAULT', }, }, { @@ -179,6 +189,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `psync`, mode: 'ASCII', + resultsMode: 'DEFAULT', }, }, { @@ -186,6 +197,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `script debug`, mode: 'ASCII', + resultsMode: 'DEFAULT', }, }, { @@ -193,6 +205,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `blpop key`, mode: 'ASCII', + resultsMode: 'DEFAULT', }, }, { @@ -200,6 +213,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `set string_key value`, mode: 'ASCII', + resultsMode: 'DEFAULT', }, }, ].map((testCase) => mainCheckFn({ @@ -226,6 +240,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `get ${constants.TEST_STRING_KEY_2}`, mode: 'ASCII', + resultsMode: 'DEFAULT', }, responseSchema, checkFn: async ({ body }) => { @@ -253,6 +268,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { command: `get ${constants.TEST_STRING_KEY_1}`, role: 'ALL', mode: 'ASCII', + resultsMode: 'DEFAULT', }, statusCode: 400, responseBody: { @@ -266,6 +282,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `get ${constants.TEST_STRING_KEY_1}`, mode: 'ASCII', + resultsMode: 'DEFAULT', nodeOptions: { host: 'localhost', port: 6379, @@ -290,7 +307,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { let nodes; before(async () => { - database = await (await localDb.getRepository(localDb.repositories.INSTANCE)).findOne({ + database = await (await localDb.getRepository(localDb.repositories.INSTANCE)).findOneBy({ id: constants.TEST_INSTANCE_ID, }); nodes = JSON.parse(database.nodes); @@ -304,6 +321,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { command: `get ${constants.TEST_STRING_KEY_1}`, role: 'ALL', mode: 'ASCII', + resultsMode: 'DEFAULT', }, responseSchema, before: async () => { @@ -355,6 +373,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { command: `get ${constants.TEST_STRING_KEY_1}`, role: 'MASTER', mode: 'ASCII', + resultsMode: 'DEFAULT', }, responseSchema, checkFn: async ({ body }) => { @@ -401,6 +420,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { command: `get ${constants.TEST_STRING_KEY_1}`, role: 'SLAVE', mode: 'ASCII', + resultsMode: 'DEFAULT', }, responseSchema, checkFn: async ({ body }) => { @@ -450,6 +470,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `get ${constants.TEST_STRING_KEY_1}`, mode: 'ASCII', + resultsMode: 'DEFAULT', nodeOptions: { host: 'unreachable', port: 6380, @@ -474,6 +495,7 @@ describe('POST /instance/:instanceId/plugins/command-executions', () => { data: { command: `get ${constants.TEST_STRING_KEY_1}`, mode: 'ASCII', + resultsMode: 'DEFAULT', nodeOptions: { ...nodes[0], enableRedirection: true, diff --git a/redisinsight/api/test/api/plugins/POST-instance-id-plugins-id-command_executions-id-state.test.ts b/redisinsight/api/test/api/plugins/POST-instance-id-plugins-id-command_executions-id-state.test.ts index a5d36a362c..9a1c4a0e01 100644 --- a/redisinsight/api/test/api/plugins/POST-instance-id-plugins-id-command_executions-id-state.test.ts +++ b/redisinsight/api/test/api/plugins/POST-instance-id-plugins-id-command_executions-id-state.test.ts @@ -1,6 +1,5 @@ import { expect, - before, describe, it, Joi, @@ -88,7 +87,7 @@ describe('POST /instance/:instanceId/plugins/:vId/command-executions/:id/state', checkFn: async ({ body }) => { expect(body).to.eql({}); const entity: any = await (await localDb.getRepository(localDb.repositories.PLUGIN_STATE)) - .findOne({ + .findOneBy({ commandExecutionId: constants.TEST_COMMAND_EXECUTION_ID_1, visualizationId: constants.TEST_PLUGIN_VISUALIZATION_ID_1, }); @@ -111,7 +110,7 @@ describe('POST /instance/:instanceId/plugins/:vId/command-executions/:id/state', checkFn: async ({ body }) => { expect(body).to.eql({}); const entity: any = await (await localDb.getRepository(localDb.repositories.PLUGIN_STATE)) - .findOne({ + .findOneBy({ commandExecutionId: constants.TEST_COMMAND_EXECUTION_ID_1, visualizationId: constants.TEST_PLUGIN_VISUALIZATION_ID_1, }); @@ -134,7 +133,7 @@ describe('POST /instance/:instanceId/plugins/:vId/command-executions/:id/state', checkFn: async ({ body }) => { expect(body).to.eql({}); const entity: any = await (await localDb.getRepository(localDb.repositories.PLUGIN_STATE)) - .findOne({ + .findOneBy({ commandExecutionId: constants.TEST_COMMAND_EXECUTION_ID_1, visualizationId: constants.TEST_PLUGIN_VISUALIZATION_ID_1, }); diff --git a/redisinsight/api/test/api/slowlog/DELETE-instance-id-slow_logs.test.ts b/redisinsight/api/test/api/slowlog/DELETE-instance-id-slow_logs.test.ts index 4429a1f58f..b84f11be10 100644 --- a/redisinsight/api/test/api/slowlog/DELETE-instance-id-slow_logs.test.ts +++ b/redisinsight/api/test/api/slowlog/DELETE-instance-id-slow_logs.test.ts @@ -47,10 +47,10 @@ describe('DELETE /instance/:instanceId/slow-logs', () => { name: 'Check that slowlog cleaned up', before: async () => { await rte.data.executeCommandAll('config', ['set', 'slowlog-log-slower-than', 10000000000]); - expect((await rte.client.send_command('slowlog', 'get')).length).to.gt(0); + expect((await rte.client.call('slowlog', 'get')).length).to.gt(0); }, after: async () => { - expect((await rte.client.send_command('slowlog', 'get')).length).to.eq(0); + expect((await rte.client.call('slowlog', 'get')).length).to.eq(0); } }, { diff --git a/redisinsight/api/test/api/workbench/DELETE-instance-id-workbench-command_executions-id.test.ts b/redisinsight/api/test/api/workbench/DELETE-instance-id-workbench-command_executions-id.test.ts index 4859cedcf8..777a1b0e34 100644 --- a/redisinsight/api/test/api/workbench/DELETE-instance-id-workbench-command_executions-id.test.ts +++ b/redisinsight/api/test/api/workbench/DELETE-instance-id-workbench-command_executions-id.test.ts @@ -59,7 +59,7 @@ describe('DELETE /instance/:instanceId/workbench/command-executions/:commandExec }, after: async () => { expect(await (await (localDb.getRepository(localDb.repositories.COMMAND_EXECUTION))) - .findOne({ id: constants.TEST_COMMAND_EXECUTION_ID_1 })).to.eql(undefined); + .findOneBy({ id: constants.TEST_COMMAND_EXECUTION_ID_1 })).to.eql(null); }, }, ].map(mainCheckFn); diff --git a/redisinsight/api/test/api/workbench/GET-instance-id-workbench-command_executions-id.test.ts b/redisinsight/api/test/api/workbench/GET-instance-id-workbench-command_executions-id.test.ts index f8d5f5d36d..d4a27df402 100644 --- a/redisinsight/api/test/api/workbench/GET-instance-id-workbench-command_executions-id.test.ts +++ b/redisinsight/api/test/api/workbench/GET-instance-id-workbench-command_executions-id.test.ts @@ -30,6 +30,8 @@ const responseSchema = Joi.object().keys({ })).allow(null), role: Joi.string().allow(null), mode: Joi.string().required(), + summary: Joi.string().allow(null), + resultsMode: Joi.string().allow(null), nodeOptions: Joi.object().keys({ host: Joi.string().required(), port: Joi.number().required(), diff --git a/redisinsight/api/test/api/workbench/GET-instance-id-workbench-command_executions.test.ts b/redisinsight/api/test/api/workbench/GET-instance-id-workbench-command_executions.test.ts index 4772cd94df..6f00833819 100644 --- a/redisinsight/api/test/api/workbench/GET-instance-id-workbench-command_executions.test.ts +++ b/redisinsight/api/test/api/workbench/GET-instance-id-workbench-command_executions.test.ts @@ -18,6 +18,8 @@ const responseSchema = Joi.array().items(Joi.object().keys({ command: Joi.string().required(), role: Joi.string().allow(null), mode: Joi.string().required(), + summary: Joi.string().allow(null), + resultsMode: Joi.string().allow(null), nodeOptions: Joi.object().keys({ host: Joi.string().required(), port: Joi.number().required(), diff --git a/redisinsight/api/test/api/workbench/POST-instance-id-workbench-command_executions.test.ts b/redisinsight/api/test/api/workbench/POST-instance-id-workbench-command_executions.test.ts index ea238bd521..e12cef4aa2 100644 --- a/redisinsight/api/test/api/workbench/POST-instance-id-workbench-command_executions.test.ts +++ b/redisinsight/api/test/api/workbench/POST-instance-id-workbench-command_executions.test.ts @@ -584,10 +584,10 @@ describe('POST /instance/:instanceId/workbench/command-executions', () => { // ]); // }, // before: async () => { - // expect(await rte.client.send_command('ft._list')).to.not.include(constants.TEST_SEARCH_HASH_INDEX_1); + // expect(await rte.client.call('ft._list')).to.not.include(constants.TEST_SEARCH_HASH_INDEX_1); // }, // after: async () => { - // expect(await rte.client.send_command(`ft._list`)).to.include(constants.TEST_SEARCH_HASH_INDEX_1); + // expect(await rte.client.call(`ft._list`)).to.include(constants.TEST_SEARCH_HASH_INDEX_1); // }, // }, // { @@ -675,7 +675,7 @@ describe('POST /instance/:instanceId/workbench/command-executions', () => { // }, // responseSchema, // after: async () => { - // expect(await rte.client.send_command('ft._list')).to.not.include(constants.TEST_SEARCH_HASH_INDEX_1); + // expect(await rte.client.call('ft._list')).to.not.include(constants.TEST_SEARCH_HASH_INDEX_1); // } // }, // ].map(mainCheckFn); @@ -705,10 +705,10 @@ describe('POST /instance/:instanceId/workbench/command-executions', () => { // ]); // }, // before: async () => { - // expect(await rte.client.send_command('ft._list')).to.not.include(constants.TEST_SEARCH_JSON_INDEX_1); + // expect(await rte.client.call('ft._list')).to.not.include(constants.TEST_SEARCH_JSON_INDEX_1); // }, // after: async () => { - // expect(await rte.client.send_command(`ft._list`)).to.include(constants.TEST_SEARCH_JSON_INDEX_1); + // expect(await rte.client.call(`ft._list`)).to.include(constants.TEST_SEARCH_JSON_INDEX_1); // }, // }, // { @@ -738,7 +738,7 @@ describe('POST /instance/:instanceId/workbench/command-executions', () => { // responseSchema, // before: async () => { // for (let i = 0; i < 10; i++) { - // await rte.client.send_command( + // await rte.client.call( // 'json.set', // [`${constants.TEST_SEARCH_JSON_KEY_PREFIX_1}${i}`, '$', `{"user":{"name":"John Smith${i}"}}`] // ) @@ -784,7 +784,7 @@ describe('POST /instance/:instanceId/workbench/command-executions', () => { // }, // responseSchema, // after: async () => { - // expect(await rte.client.send_command('ft._list')).to.not.include(constants.TEST_SEARCH_JSON_INDEX_1); + // expect(await rte.client.call('ft._list')).to.not.include(constants.TEST_SEARCH_JSON_INDEX_1); // } // }, // ].map(mainCheckFn); diff --git a/redisinsight/api/test/helpers/data/redis.ts b/redisinsight/api/test/helpers/data/redis.ts index 8bac194036..457b45073c 100644 --- a/redisinsight/api/test/helpers/data/redis.ts +++ b/redisinsight/api/test/helpers/data/redis.ts @@ -17,11 +17,11 @@ export const initDataHelper = (rte) => { const executeCommand = async (...args: string[]): Promise => { return client.nodes ? Promise.all(client.nodes('master').map(async (node) => { try { - return node.send_command(...args); + return node.call(...args); } catch (e) { return null; } - })) : client.send_command(args.shift(), ...args); + })) : client.call(args.shift(), ...args); }; const waitForInfoSync = async () => { @@ -51,11 +51,11 @@ export const initDataHelper = (rte) => { const executeCommandAll = async (...args: string[]): Promise => { return client.nodes ? Promise.all(client.nodes().map(async (node) => { try { - return node.send_command(...args); + return node.call(...args); } catch (e) { return null; } - })) : client.send_command(args.shift(), ...args); + })) : client.call(args.shift(), ...args); }; const setAclUserRules = async ( diff --git a/redisinsight/api/test/helpers/local-db.ts b/redisinsight/api/test/helpers/local-db.ts index ff015f8048..ce5c869bbf 100644 --- a/redisinsight/api/test/helpers/local-db.ts +++ b/redisinsight/api/test/helpers/local-db.ts @@ -296,17 +296,17 @@ export const createAclInstance = async (rte, server): Promise => { export const getInstanceByName = async (name: string) => { const rep = await getRepository(repositories.INSTANCE); - return rep.findOne({ where: { name } }); + return rep.findOneBy({ name }); } export const getInstanceById = async (id: string) => { const rep = await getRepository(repositories.INSTANCE); - return rep.findOne({ where: { id } }); + return rep.findOneBy({ id }); } export const applyEulaAgreement = async () => { const rep = await getRepository(repositories.AGREEMENTS); - const agreements: any = await rep.findOne(); + const agreements: any = await rep.findOneBy({}); agreements.version = '1.0.0'; agreements.data = JSON.stringify({eula: true, encryption: true}); @@ -317,7 +317,7 @@ export const setAgreements = async (agreements = {}) => { const defaultAgreements = {eula: true, encryption: true}; const rep = await getRepository(repositories.AGREEMENTS); - const entity: any = await rep.findOne(); + const entity: any = await rep.findOneBy({}); entity.version = '1.0.0'; entity.data = JSON.stringify({ ...defaultAgreements, ...agreements }); @@ -327,7 +327,7 @@ export const setAgreements = async (agreements = {}) => { const resetAgreements = async () => { const rep = await getRepository(repositories.AGREEMENTS); - const agreements: any = await rep.findOne(); + const agreements: any = await rep.findOneBy({}); agreements.version = null; agreements.data = null; @@ -336,7 +336,7 @@ const resetAgreements = async () => { export const initAgreements = async () => { const rep = await getRepository(repositories.AGREEMENTS); - const agreements: any = await rep.findOne(); + const agreements: any = await rep.findOneBy({}); agreements.version = constants.TEST_AGREEMENTS_VERSION; agreements.data = JSON.stringify({ eula: true, @@ -349,7 +349,7 @@ export const initAgreements = async () => { export const resetSettings = async () => { await resetAgreements(); const rep = await getRepository(repositories.SETTINGS); - const settings: any = await rep.findOne(); + const settings: any = await rep.findOneBy({}); settings.data = null; await rep.save(settings); @@ -358,7 +358,7 @@ export const resetSettings = async () => { export const initSettings = async () => { await initAgreements(); const rep = await getRepository(repositories.SETTINGS); - const settings: any = await rep.findOne(); + const settings: any = await rep.findOneBy({}); settings.data = null; await rep.save(settings); @@ -366,7 +366,7 @@ export const initSettings = async () => { export const setAppSettings = async (data: object) => { const rep = await getRepository(repositories.SETTINGS); - const settings: any = await rep.findOne(); + const settings: any = await rep.findOneBy({}); settings.data = JSON.stringify({ ...JSON.parse(settings.data), ...data diff --git a/redisinsight/api/test/helpers/redis.ts b/redisinsight/api/test/helpers/redis.ts index bd44806f65..4bb74d4e8e 100644 --- a/redisinsight/api/test/helpers/redis.ts +++ b/redisinsight/api/test/helpers/redis.ts @@ -1,5 +1,5 @@ import * as Redis from 'ioredis'; -import IORedis from 'ioredis'; +import * as IORedis from 'ioredis'; import * as semverCompare from 'node-version-compare'; import { constants } from './constants'; import { parseReplToObject, parseClusterNodesResponse } from './utils'; @@ -82,11 +82,13 @@ const getClient = async ( // check for cluster try { const clusterInfo = parseReplToObject( - await standaloneClient.send_command('cluster', ['info']), + await standaloneClient.cluster('INFO'), ); if (clusterInfo.cluster_state === 'ok') { const nodes = parseClusterNodesResponse( - await standaloneClient.send_command('cluster', ['nodes']), + // https://github.com/luin/ioredis/issues/1572 + // @ts-expect-error + await standaloneClient.cluster('NODES'), ) .filter((node) => node.linkState === 'connected') .map(({ host, port }) => { @@ -104,7 +106,7 @@ const getClient = async ( // check for sentinel try { - const masterGroups = await standaloneClient.send_command('sentinel', ['masters']); + const masterGroups = await standaloneClient.call('sentinel', ['masters']); if (!masterGroups?.length) { throw new Error('Invalid sentinel configuration') } @@ -167,7 +169,7 @@ export const initRTE = async () => { rte = await getClient(options); } - const info = parseReplToObject(await rte.client.send_command('info')); + const info = parseReplToObject(await rte.client.info()); rte.env = { name: constants.TEST_RUN_NAME, @@ -203,7 +205,7 @@ export const initRTE = async () => { const determineModulesInstalled = async (client) => { const modules = {}; try { - (await client.send_command('module', 'list')) + (await client.call('module', 'list')) .map(module => { modules[module[1].toLowerCase()] = { version: module[3] || -1 }; }); diff --git a/redisinsight/api/yarn.lock b/redisinsight/api/yarn.lock index 17bb181251..beaebe7663 100644 --- a/redisinsight/api/yarn.lock +++ b/redisinsight/api/yarn.lock @@ -2,58 +2,39 @@ # yarn lockfile v1 -"@angular-devkit/core@11.2.4": - version "11.2.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-11.2.4.tgz#4404b86d8dbdb41a0e3f94cb08ff8604e0c49a2e" - integrity sha512-98mGDV4XtKWiQ/2D6yzvOHrnJovXchaAN9AjscAHd2an8Fkiq72d9m2wREpk+2J40NWTDB6J5iesTh3qbi8+CA== - dependencies: - ajv "6.12.6" - fast-json-stable-stringify "2.1.0" - magic-string "0.25.7" - rxjs "6.6.3" - source-map "0.7.3" - -"@angular-devkit/core@11.2.6": - version "11.2.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-11.2.6.tgz#f90dfcc7204cdc58dfcb9901ce265c5c9c0a5dfa" - integrity sha512-3dA0Z6sIIxCDjZS/DucgmIKti7EZ/LgHoHgCO72Q50H5ZXbUSNBz5wGl5hVq2+gzrnFgU/0u40MIs6eptk30ZA== - dependencies: - ajv "6.12.6" - fast-json-stable-stringify "2.1.0" - magic-string "0.25.7" - rxjs "6.6.3" - source-map "0.7.3" - -"@angular-devkit/schematics-cli@0.1102.6": - version "0.1102.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics-cli/-/schematics-cli-0.1102.6.tgz#51b9012913be94b6e8063a2f8839f7e4b652057b" - integrity sha512-86PmafA9mYDeM08cNWHcJCEY1Yqo5aq/YaBzCak93luByDQ4Ao4Jqts9l/xBCZBGUdVrczCNzcdwr/Y/6JPPzA== - dependencies: - "@angular-devkit/core" "11.2.6" - "@angular-devkit/schematics" "11.2.6" - "@schematics/schematics" "0.1102.6" - ansi-colors "4.1.1" - inquirer "7.3.3" - minimist "1.2.5" - symbol-observable "3.0.0" +"@angular-devkit/core@14.2.1": + version "14.2.1" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-14.2.1.tgz#7ce14efdb5fce687bb4f13bef03d4b67e971b22e" + integrity sha512-lW8oNGuJqr4r31FWBjfWQYkSXdiOHBGOThIEtHvUVBKfPF/oVrupLueCUgBPel+NvxENXdo93uPsqHN7bZbmsQ== + dependencies: + ajv "8.11.0" + ajv-formats "2.1.1" + jsonc-parser "3.1.0" + rxjs "6.6.7" + source-map "0.7.4" -"@angular-devkit/schematics@11.2.4": - version "11.2.4" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-11.2.4.tgz#ba67ee835ceb210777f1feece86195f28c1b2e96" - integrity sha512-M9Ike1TYawOIHzenlZS1ufQbsS+Z11/doj5w/UrU0q2OEKc6U375t5qVGgKo3PLHHS8osb9aW9xYwBfVlKrryQ== +"@angular-devkit/schematics-cli@14.2.1": + version "14.2.1" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics-cli/-/schematics-cli-14.2.1.tgz#cbea1e4a47880ee4e3eb4816d25a104b4f9fcc10" + integrity sha512-JyyVvxxnZyh9gzN7Ee8c3/BuswIFEwfnJ0EMT7goMSpEkvFn0HDln3Oi/FY0INmNeWN6EfRre0/eoeS9NtV1DA== dependencies: - "@angular-devkit/core" "11.2.4" - ora "5.3.0" - rxjs "6.6.3" + "@angular-devkit/core" "14.2.1" + "@angular-devkit/schematics" "14.2.1" + ansi-colors "4.1.3" + inquirer "8.2.4" + symbol-observable "4.0.0" + yargs-parser "21.1.1" -"@angular-devkit/schematics@11.2.6": - version "11.2.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-11.2.6.tgz#5908daef60af2e5d98fd75ac3fe77c02ab144fa3" - integrity sha512-bhi2+5xtVAjtr3bsXKT8pnoBamQrArd/Y20ueA4Od7cd38YT97nzTA1wyHBFG0vWd0HMyg42ZS0aycNBuOebaA== +"@angular-devkit/schematics@14.2.1": + version "14.2.1" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-14.2.1.tgz#9d66080e60ab32d1b44c854cabc8f5cbb421d877" + integrity sha512-0U18FwDYt4zROBPrvewH6iBTkf2ozVHN4/gxUb9jWrqVw8mPU5AWc/iYxQLHBSinkr2Egjo1H/i9aBqgJSeh3g== dependencies: - "@angular-devkit/core" "11.2.6" - ora "5.3.0" - rxjs "6.6.3" + "@angular-devkit/core" "14.2.1" + jsonc-parser "3.1.0" + magic-string "0.26.2" + ora "5.4.1" + rxjs "6.6.7" "@babel/code-frame@7.12.11": version "7.12.11" @@ -62,13 +43,20 @@ dependencies: "@babel/highlight" "^7.10.4" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.8.3": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.0.tgz#0dfc80309beec8411e65e706461c408b0bb9b431" integrity sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA== dependencies: "@babel/highlight" "^7.16.0" +"@babel/code-frame@^7.16.7": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + "@babel/compat-data@^7.16.0": version "7.16.4" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.16.4.tgz#081d6bbc336ec5c2435c6346b2ae1fb98b5ac68e" @@ -189,6 +177,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== +"@babel/helper-validator-identifier@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" + integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== + "@babel/helper-validator-option@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3" @@ -212,6 +205,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.0", "@babel/parser@^7.16.5": version "7.16.6" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.6.tgz#8f194828193e8fa79166f34a4b4e52f3e769a314" @@ -354,6 +356,11 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + "@dabh/diagnostics@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.2.tgz#290d08f7b381b8f94607dc8f471a12c675f9db31" @@ -378,6 +385,11 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@gar/promisify@^1.0.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== + "@hapi/hoek@^9.0.0": version "9.2.1" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.1.tgz#9551142a1980503752536b5050fd99f4a7f13b17" @@ -404,6 +416,16 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@ioredis/as-callback@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@ioredis/as-callback/-/as-callback-3.0.0.tgz#b96c9b05e6701e85ec6a5e62fa254071b0aec97f" + integrity sha512-Kqv1rZ3WbgOrS+hgzJ5xG5WQuhvzzSTRYvNeyPMLOAM78MHSnuKI20JeJGbpuAt//LCuP0vsexZcorqW7kWhJg== + +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -591,142 +613,198 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" + integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@mapbox/node-pre-gyp@^1.0.0": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc" + integrity sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + "@mochajs/json-file-reporter@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@mochajs/json-file-reporter/-/json-file-reporter-1.3.0.tgz#63a53bcda93d75f5c5c74af60e45da063931370b" integrity sha512-evIxpeP8EOixo/T2xh5xYEIzwbEHk8YNJfRUm1KeTs8F3bMjgNn2580Ogze9yisXNlTxu88JiJJYzXjjg5NdLA== -"@nestjs/cli@^7.5.4": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-7.6.0.tgz#661f99b578284f9124307a8077f004a091b25e77" - integrity sha512-lW1px2gSHkRoBpKSxzP6IJNQscRKs97OAaVyV46OAP6oUR996E0EPkIslIaa16kKLJ3SFOUeZo5xl5nYbqp43g== +"@nestjs/cli@^9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-9.1.2.tgz#12d24ef44dd034bcfece8750e5bb7122cfe3e300" + integrity sha512-J/mFYM/L03//MiKcYX113MkTbA88cjb+4pNdLjeOpVRCbfbQh1zArehGAOvas9+SNRUzUYj31vI1snYKXc0j1g== dependencies: - "@angular-devkit/core" "11.2.6" - "@angular-devkit/schematics" "11.2.6" - "@angular-devkit/schematics-cli" "0.1102.6" - "@nestjs/schematics" "^7.3.0" + "@angular-devkit/core" "14.2.1" + "@angular-devkit/schematics" "14.2.1" + "@angular-devkit/schematics-cli" "14.2.1" + "@nestjs/schematics" "^9.0.0" chalk "3.0.0" - chokidar "3.5.1" - cli-table3 "0.5.1" + chokidar "3.5.3" + cli-table3 "0.6.2" commander "4.1.1" - fork-ts-checker-webpack-plugin "6.2.0" + fork-ts-checker-webpack-plugin "7.2.13" inquirer "7.3.3" - node-emoji "1.10.0" - ora "5.4.0" - os-name "4.0.0" + node-emoji "1.11.0" + ora "5.4.1" + os-name "4.0.1" rimraf "3.0.2" - shelljs "0.8.4" + shelljs "0.8.5" + source-map-support "0.5.21" tree-kill "1.2.2" - tsconfig-paths "3.9.0" - tsconfig-paths-webpack-plugin "3.5.1" - typescript "4.2.3" - webpack "5.28.0" - webpack-node-externals "2.5.2" - -"@nestjs/common@^7.6.15": - version "7.6.18" - resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-7.6.18.tgz#d89e6d248985eec13af60507a8725cb2142d660a" - integrity sha512-BUJQHNhWzwWOkS4Ryndzd4HTeRObcAWV2Fh+ermyo3q3xYQQzNoEWclJVL/wZec8AONELwIJ+PSpWI53VP0leg== - dependencies: - axios "0.21.1" + tsconfig-paths "4.1.0" + tsconfig-paths-webpack-plugin "4.0.0" + typescript "4.7.4" + webpack "5.74.0" + webpack-node-externals "3.0.0" + +"@nestjs/common@^9.0.11": + version "9.0.11" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-9.0.11.tgz#5747cfbb5d94d909bc2894bf2a5bec83fca809c5" + integrity sha512-oYLIcOal3QOwcqt6goXovRNg8ZkalyOMjH0oYYzfJLrait6P7c6nAeWHu4qFDThY7GoZHEanLgji1qlqVEW09g== + dependencies: iterare "1.2.1" - tslib "2.2.0" + tslib "2.4.0" uuid "8.3.2" -"@nestjs/core@^7.0.0": - version "7.6.18" - resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-7.6.18.tgz#36448f0ae7f7d08f032e1e7e53b4a4c82ae844d7" - integrity sha512-CGu20OjIxgFDY7RJT5t1TDGL8wSlTSlbZEkn8U5OlICZEB3WIpi98G7ajJpnRWmEgW8S4aDJmRKGjT+Ntj5U4A== +"@nestjs/core@^9.0.11": + version "9.0.11" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-9.0.11.tgz#1bed969d81685f17d4a01f854c43a222dd3e13ce" + integrity sha512-DYyoiWSGebDAG8WSfG/ue88HBU39kAJTi2YXftWdVSl1LFveV+pwKY83P2qX0ND38TS8WktFYpaMkXslf97BBQ== dependencies: "@nuxtjs/opencollective" "0.3.2" - fast-safe-stringify "2.0.7" + fast-safe-stringify "2.1.1" iterare "1.2.1" - object-hash "2.1.1" + object-hash "3.0.0" path-to-regexp "3.2.0" - tslib "2.2.0" + tslib "2.4.0" uuid "8.3.2" -"@nestjs/event-emitter@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@nestjs/event-emitter/-/event-emitter-1.0.0.tgz#aac176e70ca683cec4e9516a0a2985e173e29464" - integrity sha512-dRAou6G89KKYI2iyYfqSVGE6ZTC4WmHkQkFfgh88GLQg8dBqRk92ZY8CRtL2SK32SSelh9bwEDNQn9561uoypA== +"@nestjs/event-emitter@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@nestjs/event-emitter/-/event-emitter-1.3.1.tgz#20ec79a5c075af7d52af2a4adc06df7aec3d6246" + integrity sha512-AmHkPTe/cP1lbQEm15TIe9IDEAszl5VAR8HjMS2TDtNRuSzwyoJgZUVcRnH7Yk9/2DX5qMtmw6a1MHeR8DD+rw== dependencies: - eventemitter2 "6.4.4" + eventemitter2 "6.4.6" -"@nestjs/mapped-types@0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-0.4.1.tgz#e7fe038f0bdda7b8f858fa79ca8516b8f9069b1a" - integrity sha512-JXrw2LMangSU3vnaXWXVX47GRG1FbbNh4aVBbidDjxT3zlghsoNQY6qyWtT001MCl8lJGo8I6i6+DurBRRxl/Q== +"@nestjs/mapped-types@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-1.1.0.tgz#54a9fa61079635dd6c3c75fd9593f20b2302a55b" + integrity sha512-+2kSly4P1QI+9eGt+/uGyPdEG1hVz7nbpqPHWZVYgoqz8eOHljpXPag+UCVRw9zo2XCu4sgNUIGe8Uk0+OvUQg== -"@nestjs/platform-express@^7.0.0": - version "7.6.18" - resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-7.6.18.tgz#cdf442dfd85948fc7b67bbc4007dddef83cdd4b9" - integrity sha512-Dty2bBhsW7EInMRPS1pkXKJ3GBBusEj6fnEpb0UfkaT3E7asay9c64kCmZE+8hU430qQjY+fhBb1RNWWPnUiwQ== +"@nestjs/platform-express@^9.0.11": + version "9.0.11" + resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-9.0.11.tgz#49110e02944f3ea4b14936b3ffaa1060230e642e" + integrity sha512-Up1Ps08n2Y07AYakTKKU5uofGQoAQoUaRyfXdH0G54OnICCUiqcFH0QveNYLCkHoMP4iFs6vMr3xhvO6y91NBQ== dependencies: - body-parser "1.19.0" + body-parser "1.20.0" cors "2.8.5" - express "4.17.1" - multer "1.4.2" - tslib "2.2.0" - -"@nestjs/platform-socket.io@^8.2.3": - version "8.2.4" - resolved "https://registry.yarnpkg.com/@nestjs/platform-socket.io/-/platform-socket.io-8.2.4.tgz#52b59d1715c8490a71854d86f150e267128b19b0" - integrity sha512-k41BoY9NWrLqltvkFli8QnIbfu9j41gBejZ6VxtjthYci7OLyu/XlnY8k4TuzEYGAHvjgNg5k2CKz7F6xzsDhQ== - dependencies: - socket.io "4.4.0" - tslib "2.3.1" - -"@nestjs/schematics@^7.0.0", "@nestjs/schematics@^7.3.0": - version "7.3.1" - resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-7.3.1.tgz#68b559d2e6a8a9ecf6c984f87eaa7d4e37a910be" - integrity sha512-eyBjJstAjecpdzRuBLiqnwomwXIAEV3+kPkpaphOieRUM6nBhjnXCCl3Qf8Dul2QUQK4NOVPd8FFxWtGP5XNlg== - dependencies: - "@angular-devkit/core" "11.2.4" - "@angular-devkit/schematics" "11.2.4" - fs-extra "9.1.0" - jsonc-parser "3.0.0" + express "4.18.1" + multer "1.4.4-lts.1" + tslib "2.4.0" + +"@nestjs/platform-socket.io@^9.0.11": + version "9.0.11" + resolved "https://registry.yarnpkg.com/@nestjs/platform-socket.io/-/platform-socket.io-9.0.11.tgz#c80dfa340f43a3813db72a1a739ff48974a2d780" + integrity sha512-a+d8uo73RJFsTCAVVuChyiilGil1TE5VMNzK3OGJOd4jlWOnanp5BQ0hbSw576n5/Z/YYOXUNaIDBEnBwHS2Cw== + dependencies: + socket.io "4.5.1" + tslib "2.4.0" + +"@nestjs/schematics@^9.0.0", "@nestjs/schematics@^9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-9.0.3.tgz#175218350fb3829c9a903e980046a11950310e24" + integrity sha512-kZrU/lrpVd2cnK8I3ibDb3Wi1ppl3wX3U3lVWoL+DzRRoezWKkh8upEL4q0koKmuXnsmLiu3UPxFeMOrJV7TSA== + dependencies: + "@angular-devkit/core" "14.2.1" + "@angular-devkit/schematics" "14.2.1" + fs-extra "10.1.0" + jsonc-parser "3.2.0" pluralize "8.0.0" -"@nestjs/serve-static@^2.1.3": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@nestjs/serve-static/-/serve-static-2.2.2.tgz#8e9dc2fc6c042ddac5133b957d6bc25d9f8fa225" - integrity sha512-3Mr+Q/npS3N7iGoF3Wd6Lj9QcjMGxbNrSqupi5cviM0IKrZ1BHl5qekW95rWYNATAVqoTmjGROAq+nKKpuUagQ== +"@nestjs/serve-static@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@nestjs/serve-static/-/serve-static-3.0.0.tgz#085e7ebbb3c6d6e35b227ea57c8e988c87847309" + integrity sha512-TpXjgs4136dQqWUjEcONqppqXDsrJhRkmKWzuBMOUAnP4HjHpNmlycvkHnDnWSoG2YD4a7Enh4ViYGWqCfHStA== dependencies: - path-to-regexp "0.1.7" + path-to-regexp "0.2.5" -"@nestjs/swagger@^4.6.1": - version "4.8.2" - resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-4.8.2.tgz#0a0b3ca1b25146e797ca77addd9fa97f82406c1c" - integrity sha512-RSUwcVxrzXF7/b/IZ5lXnYHJ6jIGS9wWRTJKIt1kIaCNWT+0wRfTlAyhQkzs2g35/PTXJEcdIwwY7mBO/bwHzw== +"@nestjs/swagger@^6.1.2": + version "6.1.2" + resolved "https://registry.yarnpkg.com/@nestjs/swagger/-/swagger-6.1.2.tgz#5eb9c134fc976f16c139ecaf80a7f02a5d33da46" + integrity sha512-RU1DeTDyuN/lRXKFWaf7I9LYF34/ale3IIGeY3romAcXL/N9W0+50Ek3ou+Ajd5FqpLqzt7saYhnaQegVuU4UQ== dependencies: - "@nestjs/mapped-types" "0.4.1" + "@nestjs/mapped-types" "1.1.0" + js-yaml "4.1.0" lodash "4.17.21" path-to-regexp "3.2.0" + swagger-ui-dist "4.14.0" -"@nestjs/testing@^7.0.0": - version "7.6.18" - resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-7.6.18.tgz#b4c137b5b6c2fb18c51602d33a083cd97c648283" - integrity sha512-1AVk9vWZlPpx4CmzY6z9z0DHFgGCadfr01QdisGFAN740JwKqZWEqz12cVd+nsXDlYQPFRkp2ICBIS/6k1qZGQ== +"@nestjs/testing@^9.0.11": + version "9.0.11" + resolved "https://registry.yarnpkg.com/@nestjs/testing/-/testing-9.0.11.tgz#8e60107a7aaf9fc7b2bf29d473c2bdb1887de017" + integrity sha512-tT+yj3av7ZJb9Cy09C4+FoUULvzUntf81g5eK5shRVeQ35RWqr7E5Uq77B7ePUF2Er/TictVZk43d7rKq1ClNA== dependencies: - optional "0.1.4" - tslib "2.2.0" + tslib "2.4.0" -"@nestjs/typeorm@^7.1.5": - version "7.1.5" - resolved "https://registry.yarnpkg.com/@nestjs/typeorm/-/typeorm-7.1.5.tgz#50e3bf85ff8cf78d47d8dd19210c5f02b488f5e3" - integrity sha512-utE1FkYM/gyCXUqw3zKYYS0YZ3DfkAnzsCx4T48cNnSDTCeWS+u3yt0FMDFjwSiQSaLrzpiSff/FaxJQvRlYow== +"@nestjs/typeorm@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@nestjs/typeorm/-/typeorm-9.0.1.tgz#f78bfc00e71731ea860288e4a03830107daf3d9c" + integrity sha512-A2BgLIPsMtmMI0bPKEf4bmzgFPsnvHqNBx3KkvaJ7hJrBQy0OqYOb+Rr06ifblKWDWS2tUPNrAFQbZjtk3PI+g== dependencies: - uuid "8.3.1" + uuid "8.3.2" -"@nestjs/websockets@^8.2.3": - version "8.2.4" - resolved "https://registry.yarnpkg.com/@nestjs/websockets/-/websockets-8.2.4.tgz#ff18cf6e4d770e15eee02a0ce7c629fc31b755c2" - integrity sha512-huCP6F+L0eyQ4w1aWn3QKQiEjp5/V4sw+JJ0CINSD1+Ccs3NCe9l53rFyrCD2fbLLCIryh7mkED7wwp2uEcsiw== +"@nestjs/websockets@^9.0.11": + version "9.0.11" + resolved "https://registry.yarnpkg.com/@nestjs/websockets/-/websockets-9.0.11.tgz#b2ee7eb173aa63e64d8265c1a08f3b528c80b245" + integrity sha512-hrUfmiCajNsXS5i2g3siqxETv5whIpzY6BOW8CGSPn6Hm899aRm7BagCeNBAcpI77fHMchBQLTCuVO7h3oexsA== dependencies: iterare "1.2.1" - object-hash "2.2.0" - tslib "2.3.1" + object-hash "3.0.0" + tslib "2.4.0" "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -749,6 +827,22 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@npmcli/fs@^1.0.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" + integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + "@nuxtjs/opencollective@0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz#620ce1044f7ac77185e825e1936115bb38e2681c" @@ -758,14 +852,6 @@ consola "^2.15.0" node-fetch "^2.6.1" -"@schematics/schematics@0.1102.6": - version "0.1102.6" - resolved "https://registry.yarnpkg.com/@schematics/schematics/-/schematics-0.1102.6.tgz#2ce02f7c11558471628eafeb34faaa7f5ab4b22c" - integrity sha512-x77kbJL/HqR4gx0tbt35VCOGLyMvB7jD/x7eB1njhQRF8E/xynEOk3i+7A5VmK67QP5NJxU8BQKlPkJ55tBDmg== - dependencies: - "@angular-devkit/core" "11.2.6" - "@angular-devkit/schematics" "11.2.6" - "@segment/loosely-validate-event@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz#87dfc979e5b4e7b82c5f1d8b722dfd5d77644681" @@ -900,10 +986,10 @@ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== -"@types/eslint-scope@^3.7.0": - version "3.7.2" - resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.2.tgz#11e96a868c67acf65bf6f11d10bb89ea71d5e473" - integrity sha512-TzgYCWoPiTeRg6RQYgtuW7iODtVoKu3RVL72k3WohqhjfaOLK5Mg2T4Tg1o2bSfu0vPkoI48wdQFv5b/Xe04wQ== +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== dependencies: "@types/eslint" "*" "@types/estree" "*" @@ -921,10 +1007,10 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83" integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== -"@types/estree@^0.0.46": - version "0.0.46" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.46.tgz#0fb6bfbbeabd7a30880504993369c4bf1deab1fe" - integrity sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg== +"@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== "@types/express-serve-static-core@^4.17.18": version "4.17.26" @@ -952,13 +1038,6 @@ dependencies: "@types/node" "*" -"@types/ioredis@^4.22.3": - version "4.28.5" - resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.5.tgz#933aa76dd0b66147be48f94967e2571ff848408e" - integrity sha512-bp5mdpzscWZMEE/jLvvzze5TZFYGhynB1am69l/a0XPqZRXWpbswY6lb5buEht57jOnw5pPG5zL9pFUWw1nggw== - dependencies: - "@types/node" "*" - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -986,7 +1065,7 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" -"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8": +"@types/json-schema@*", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== @@ -1093,11 +1172,6 @@ dependencies: "@types/yargs-parser" "*" -"@types/zen-observable@0.8.3": - version "0.8.3" - resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.3.tgz#781d360c282436494b32fe7d9f7f8e64b3118aa3" - integrity sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw== - "@typescript-eslint/eslint-plugin@^4.8.1": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276" @@ -1173,125 +1247,125 @@ resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -"@webassemblyjs/ast@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.0.tgz#a5aa679efdc9e51707a4207139da57920555961f" - integrity sha512-kX2W49LWsbthrmIRMbQZuQDhGtjyqXfEmmHyEi4XWnSZtPmxY0+3anPIzsnRb45VH/J55zlOfWvZuY47aJZTJg== +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== dependencies: - "@webassemblyjs/helper-numbers" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" -"@webassemblyjs/floating-point-hex-parser@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.0.tgz#34d62052f453cd43101d72eab4966a022587947c" - integrity sha512-Q/aVYs/VnPDVYvsCBL/gSgwmfjeCb4LW8+TMrO3cSzJImgv8lxxEPM2JA5jMrivE7LSz3V+PFqtMbls3m1exDA== +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== -"@webassemblyjs/helper-api-error@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.0.tgz#aaea8fb3b923f4aaa9b512ff541b013ffb68d2d4" - integrity sha512-baT/va95eXiXb2QflSx95QGT5ClzWpGaa8L7JnJbgzoYeaA27FCvuBXU758l+KXWRndEmUXjP0Q5fibhavIn8w== +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== -"@webassemblyjs/helper-buffer@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.0.tgz#d026c25d175e388a7dbda9694e91e743cbe9b642" - integrity sha512-u9HPBEl4DS+vA8qLQdEQ6N/eJQ7gT7aNvMIo8AAWvAl/xMrcOSiI2M0MAnMCy3jIFke7bEee/JwdX1nUpCtdyA== +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== -"@webassemblyjs/helper-numbers@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.0.tgz#7ab04172d54e312cc6ea4286d7d9fa27c88cd4f9" - integrity sha512-DhRQKelIj01s5IgdsOJMKLppI+4zpmcMQ3XboFPLwCpSNH6Hqo1ritgHgD0nqHeSYqofA6aBN/NmXuGjM1jEfQ== +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" + integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.0" - "@webassemblyjs/helper-api-error" "1.11.0" + "@webassemblyjs/floating-point-hex-parser" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" "@xtuc/long" "4.2.2" -"@webassemblyjs/helper-wasm-bytecode@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.0.tgz#85fdcda4129902fe86f81abf7e7236953ec5a4e1" - integrity sha512-MbmhvxXExm542tWREgSFnOVo07fDpsBJg3sIl6fSp9xuu75eGz5lz31q7wTLffwL3Za7XNRCMZy210+tnsUSEA== +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== -"@webassemblyjs/helper-wasm-section@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.0.tgz#9ce2cc89300262509c801b4af113d1ca25c1a75b" - integrity sha512-3Eb88hcbfY/FCukrg6i3EH8H2UsD7x8Vy47iVJrP967A9JGqgBVL9aH71SETPx1JrGsOUVLo0c7vMCN22ytJew== +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" + integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-buffer" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - "@webassemblyjs/wasm-gen" "1.11.0" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" -"@webassemblyjs/ieee754@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.0.tgz#46975d583f9828f5d094ac210e219441c4e6f5cf" - integrity sha512-KXzOqpcYQwAfeQ6WbF6HXo+0udBNmw0iXDmEK5sFlmQdmND+tr773Ti8/5T/M6Tl/413ArSJErATd8In3B+WBA== +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== dependencies: "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/leb128@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.0.tgz#f7353de1df38aa201cba9fb88b43f41f75ff403b" - integrity sha512-aqbsHa1mSQAbeeNcl38un6qVY++hh8OpCOzxhixSYgbRfNWcxJNJQwe2rezK9XEcssJbbWIkblaJRwGMS9zp+g== +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== dependencies: "@xtuc/long" "4.2.2" -"@webassemblyjs/utf8@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.0.tgz#86e48f959cf49e0e5091f069a709b862f5a2cadf" - integrity sha512-A/lclGxH6SpSLSyFowMzO/+aDEPU4hvEiooCMXQPcQFPPJaYcPQNKGOCLUySJsYJ4trbpr+Fs08n4jelkVTGVw== - -"@webassemblyjs/wasm-edit@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.0.tgz#ee4a5c9f677046a210542ae63897094c2027cb78" - integrity sha512-JHQ0damXy0G6J9ucyKVXO2j08JVJ2ntkdJlq1UTiUrIgfGMmA7Ik5VdC/L8hBK46kVJgujkBIoMtT8yVr+yVOQ== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-buffer" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - "@webassemblyjs/helper-wasm-section" "1.11.0" - "@webassemblyjs/wasm-gen" "1.11.0" - "@webassemblyjs/wasm-opt" "1.11.0" - "@webassemblyjs/wasm-parser" "1.11.0" - "@webassemblyjs/wast-printer" "1.11.0" - -"@webassemblyjs/wasm-gen@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.0.tgz#3cdb35e70082d42a35166988dda64f24ceb97abe" - integrity sha512-BEUv1aj0WptCZ9kIS30th5ILASUnAPEvE3tVMTrItnZRT9tXCLW2LEXT8ezLw59rqPP9klh9LPmpU+WmRQmCPQ== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - "@webassemblyjs/ieee754" "1.11.0" - "@webassemblyjs/leb128" "1.11.0" - "@webassemblyjs/utf8" "1.11.0" - -"@webassemblyjs/wasm-opt@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.0.tgz#1638ae188137f4bb031f568a413cd24d32f92978" - integrity sha512-tHUSP5F4ywyh3hZ0+fDQuWxKx3mJiPeFufg+9gwTpYp324mPCQgnuVKwzLTZVqj0duRDovnPaZqDwoyhIO8kYg== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-buffer" "1.11.0" - "@webassemblyjs/wasm-gen" "1.11.0" - "@webassemblyjs/wasm-parser" "1.11.0" - -"@webassemblyjs/wasm-parser@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.0.tgz#3e680b8830d5b13d1ec86cc42f38f3d4a7700754" - integrity sha512-6L285Sgu9gphrcpDXINvm0M9BskznnzJTE7gYkjDbxET28shDqp27wpruyx3C2S/dvEwiigBwLA1cz7lNUi0kw== - dependencies: - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/helper-api-error" "1.11.0" - "@webassemblyjs/helper-wasm-bytecode" "1.11.0" - "@webassemblyjs/ieee754" "1.11.0" - "@webassemblyjs/leb128" "1.11.0" - "@webassemblyjs/utf8" "1.11.0" - -"@webassemblyjs/wast-printer@1.11.0": - version "1.11.0" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.0.tgz#680d1f6a5365d6d401974a8e949e05474e1fab7e" - integrity sha512-Fg5OX46pRdTgB7rKIUojkh9vXaVN6sGYCnEiJN1GYkb0RPwShZXp6KTDqmoMdQPKhcroOXh3fEzmkWmCYaKYhQ== - dependencies: - "@webassemblyjs/ast" "1.11.0" +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" + integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-wasm-section" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-opt" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/wast-printer" "1.11.1" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" + integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" + integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" + integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" "@xtuc/long" "4.2.2" "@xtuc/ieee754@^1.2.0": @@ -1322,6 +1396,14 @@ accepts@~1.3.4, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + acorn-globals@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45" @@ -1330,6 +1412,11 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + acorn-jsx@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -1345,23 +1432,37 @@ acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.0.4, acorn@^8.2.4: +acorn@^8.2.4: version "8.6.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== +acorn@^8.5.0, acorn@^8.7.1: + version "8.8.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" + integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + adm-zip@^0.5.9: version "0.5.9" resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.9.tgz#b33691028333821c0cf95c31374c5462f2905a83" integrity sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg== -agent-base@6: +agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" +agentkeepalive@^4.1.3: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" + integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -1370,12 +1471,29 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: +ajv-formats@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@6.12.6, ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: +ajv@8.11.0, ajv@^8.0.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1414,6 +1532,11 @@ ansi-colors@4.1.1, ansi-colors@^4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== +ansi-colors@4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -1421,11 +1544,6 @@ ansi-escapes@^4.2.1: dependencies: type-fest "^0.21.3" -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - ansi-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" @@ -1493,23 +1611,31 @@ append-transform@^2.0.0: dependencies: default-require-extensions "^3.0.0" -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== archy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= -are-we-there-yet@~1.1.2: - version "1.1.7" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" - integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== dependencies: delegates "^1.0.0" - readable-stream "^2.0.6" + readable-stream "^3.6.0" arg@^4.1.0: version "4.1.3" @@ -1583,18 +1709,6 @@ arrify@^1.0.0: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" @@ -1620,26 +1734,11 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== - axios-retry@^3.0.2: version "3.2.4" resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.2.4.tgz#f447a53c3456f5bfeca18f20c3a3272207d082ae" @@ -1648,7 +1747,7 @@ axios-retry@^3.0.2: "@babel/runtime" "^7.15.4" is-retry-allowed "^2.2.0" -axios@*, axios@0.21.1, axios@^0.21.1: +axios@*, axios@^0.21.1: version "0.21.1" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== @@ -1756,13 +1855,6 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -1782,29 +1874,6 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= - dependencies: - inherits "~2.0.0" - -body-parser@1.19.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" - integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== - dependencies: - bytes "3.1.0" - content-type "~1.0.4" - debug "2.6.9" - depd "~1.1.2" - http-errors "1.7.2" - iconv-lite "0.4.24" - on-finished "~2.3.0" - qs "6.7.0" - raw-body "2.4.0" - type-is "~1.6.17" - body-parser@1.19.1, body-parser@^1.19.0: version "1.19.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.1.tgz#1499abbaa9274af3ecc9f6f10396c995943e31d4" @@ -1821,6 +1890,24 @@ body-parser@1.19.1, body-parser@^1.19.0: raw-body "2.4.2" type-is "~1.6.18" +body-parser@1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" + integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.10.3" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -1908,24 +1995,47 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -busboy@^0.2.11: - version "0.2.14" - resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" - integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== dependencies: - dicer "0.2.5" - readable-stream "1.1.x" - -bytes@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" - integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + streamsearch "^1.1.0" bytes@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a" integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg== +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cacache@^15.2.0: + version "15.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + dependencies: + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -1975,9 +2085,9 @@ camelcase@^6.0.0: integrity sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA== caniuse-lite@^1.0.30001286: - version "1.0.30001292" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001292.tgz#4a55f61c06abc9595965cfd77897dc7bc1cdc456" - integrity sha512-jnT4Tq0Q4ma+6nncYQVe7d73kmDmE9C3OGTx3MvW7lBM/eY1S1DZTMBON7dqV481RhNiS5OxD7k9JQvmDOTirw== + version "1.0.30001377" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001377.tgz" + integrity sha512-I5XeHI1x/mRSGl96LFOaSk528LA/yZG3m3iQgImGujjO8gotd/DL8QaI1R1h1dg5ATeI2jqPblMpKq4Tr5iKfQ== capture-exit@^2.0.0: version "2.0.0" @@ -1986,11 +2096,6 @@ capture-exit@^2.0.0: dependencies: rsvp "^4.8.4" -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - chai@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.4.tgz#b55e655b31e1eac7099be4c08c21964fce2e6c49" @@ -2020,7 +2125,7 @@ chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0, chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -2063,10 +2168,10 @@ chokidar@3.5.1: optionalDependencies: fsevents "~2.3.1" -chokidar@^3.4.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" - integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== +chokidar@3.5.3, chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -2078,11 +2183,16 @@ chokidar@^3.4.2: optionalDependencies: fsevents "~2.3.2" -chownr@^1.1.1, chownr@^1.1.4: +chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -2163,15 +2273,14 @@ cli-spinners@^2.5.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== -cli-table3@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" - integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== +cli-table3@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.2.tgz#aaf5df9d8b5bf12634dc8b3040806a0c07120d2a" + integrity sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw== dependencies: - object-assign "^4.1.0" - string-width "^2.1.1" + string-width "^4.2.0" optionalDependencies: - colors "^1.1.2" + "@colors/colors" "1.5.0" cli-width@^3.0.0: version "3.0.0" @@ -2220,11 +2329,6 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= - collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" @@ -2270,6 +2374,11 @@ color-string@^1.6.0: color-name "^1.0.0" simple-swizzle "^0.2.2" +color-support@^1.1.2, color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + color@^3.1.3: version "3.2.1" resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" @@ -2278,7 +2387,7 @@ color@^3.1.3: color-convert "^1.9.3" color-string "^1.6.0" -colors@^1.1.2, colors@^1.2.1: +colors@^1.2.1: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== @@ -2291,7 +2400,7 @@ colorspace@1.1.x: color "^3.1.3" text-hex "1.0.x" -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -2363,18 +2472,11 @@ consola@^2.15.0: resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== -console-control-strings@^1.0.0, console-control-strings@~1.1.0: +console-control-strings@^1.0.0, console-control-strings@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= -content-disposition@0.5.3: - version "0.5.3" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" - integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== - dependencies: - safe-buffer "5.1.2" - content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -2399,16 +2501,16 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" - integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== - cookie@0.4.1, cookie@~0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + cookiejar@^2.1.0: version "2.1.3" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" @@ -2419,11 +2521,6 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - core-util-is@~1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" @@ -2437,16 +2534,16 @@ cors@2.8.5, cors@~2.8.5: object-assign "^4" vary "^1" -cosmiconfig@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" - integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== +cosmiconfig@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== dependencies: "@types/parse-json" "^4.0.0" - import-fresh "^3.1.0" + import-fresh "^3.2.1" parse-json "^5.0.0" path-type "^4.0.0" - yaml "^1.7.2" + yaml "^1.10.0" create-require@^1.1.0: version "1.1.1" @@ -2510,13 +2607,6 @@ d@1, d@^1.0.1: es5-ext "^0.10.50" type "^1.0.1" -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b" @@ -2531,6 +2621,11 @@ date-fns@^2.0.1: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.27.0.tgz#e1ff3c3ddbbab8a2eaadbb6106be2929a5a2d92b" integrity sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q== +date-fns@^2.28.0: + version "2.29.2" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.2.tgz#0d4b3d0f3dff0f920820a070920f0d9662c51931" + integrity sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -2552,14 +2647,14 @@ debug@4.3.1: dependencies: ms "2.1.2" -debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: +debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" -debug@^4.3.1: +debug@^4.3.1, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -2586,12 +2681,12 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= -decompress-response@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" - integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== dependencies: - mimic-response "^2.0.0" + mimic-response "^3.1.0" deep-eql@^3.0.1: version "3.0.1" @@ -2668,39 +2763,41 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= -denque@^1.1.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" - integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== +denque@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== -depd@~1.1.2: +depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= -detect-libc@^1.0.2, detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -dicer@0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" - integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= - dependencies: - readable-stream "1.1.x" - streamsearch "0.1.2" - diff-sequences@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" @@ -2754,19 +2851,6 @@ dotenv@^16.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.0.tgz#c619001253be89ebb638d027b609c75c26e47411" integrity sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q== -dotenv@^8.2.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" - integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -2807,6 +2891,13 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +encoding@^0.1.12: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -2836,6 +2927,11 @@ engine.io-parser@~5.0.0: dependencies: "@socket.io/base64-arraybuffer" "~1.0.2" +engine.io-parser@~5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0" + integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg== + engine.io@~6.1.0: version "6.1.2" resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.1.2.tgz#e7b9d546d90c62246ffcba4d88594be980d3855a" @@ -2852,6 +2948,22 @@ engine.io@~6.1.0: engine.io-parser "~5.0.0" ws "~8.2.3" +engine.io@~6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.0.tgz#003bec48f6815926f2b1b17873e576acd54f41d0" + integrity sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg== + dependencies: + "@types/cookie" "^0.4.1" + "@types/cors" "^2.8.12" + "@types/node" ">=10.0.0" + accepts "~1.3.4" + base64id "2.0.0" + cookie "~0.4.1" + cors "~2.8.5" + debug "~4.3.1" + engine.io-parser "~5.0.3" + ws "~8.2.3" + enhanced-resolve@^4.0.0: version "4.5.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" @@ -2861,6 +2973,14 @@ enhanced-resolve@^4.0.0: memory-fs "^0.5.0" tapable "^1.0.0" +enhanced-resolve@^5.10.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6" + integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + enhanced-resolve@^5.7.0: version "5.8.3" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0" @@ -2876,6 +2996,16 @@ enquirer@^2.3.5: dependencies: ansi-colors "^4.1.1" +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + errno@^0.1.3: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -2916,10 +3046,10 @@ es-abstract@^1.19.0, es-abstract@^1.19.1: string.prototype.trimstart "^1.0.4" unbox-primitive "^1.0.1" -es-module-lexer@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.4.1.tgz#dda8c6a14d8f340a24e34331e0fab0cb50438e0e" - integrity sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA== +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== es-to-primitive@^1.2.1: version "1.2.1" @@ -3083,7 +3213,7 @@ eslint-plugin-sonarjs@^0.9.1: resolved "https://registry.yarnpkg.com/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.9.1.tgz#a3c63ab0d267bfb69863159e42c8081b01fd3ac6" integrity sha512-KKFofk1LPjGHWeAZijYWv32c/C4mz+OAeBNVxhxHu1hknrTOhu415MWC8qKdAdsmOlBPShs9evM4mI1o7MNMhw== -eslint-scope@^5.1.1: +eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== @@ -3217,10 +3347,10 @@ event-emitter@^0.3.5: d "1" es5-ext "~0.10.14" -eventemitter2@6.4.4: - version "6.4.4" - resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b" - integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw== +eventemitter2@6.4.6: + version "6.4.6" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.6.tgz#92d56569cc147a4d9b9da9e942e89b20ce236b0a" + integrity sha512-OHqo4wbHX5VbvlbB6o6eDwhYmiTjrpWACjF8Pmof/GTD6rdBNdZFNck3xlhqOiQFGCOoq3uzHvA0cQpFHIGVAQ== events@^3.2.0: version "3.3.0" @@ -3295,38 +3425,39 @@ expect@^26.6.2: jest-message-util "^26.6.2" jest-regex-util "^26.0.0" -express@4.17.1: - version "4.17.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" - integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== +express@4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" + integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== dependencies: - accepts "~1.3.7" + accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.19.0" - content-disposition "0.5.3" + body-parser "1.20.0" + content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.4.0" + cookie "0.5.0" cookie-signature "1.0.6" debug "2.6.9" - depd "~1.1.2" + depd "2.0.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "~1.1.2" + finalhandler "1.2.0" fresh "0.5.2" + http-errors "2.0.0" merge-descriptors "1.0.1" methods "~1.1.2" - on-finished "~2.3.0" + on-finished "2.4.1" parseurl "~1.3.3" path-to-regexp "0.1.7" - proxy-addr "~2.0.5" - qs "6.7.0" + proxy-addr "~2.0.7" + qs "6.10.3" range-parser "~1.2.1" - safe-buffer "5.1.2" - send "0.17.1" - serve-static "1.14.1" - setprototypeof "1.1.1" - statuses "~1.5.0" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" type-is "~1.6.18" utils-merge "1.0.1" vary "~1.1.2" @@ -3389,7 +3520,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.2: +extend@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -3417,16 +3548,6 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" - integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -3443,7 +3564,7 @@ fast-glob@^3.1.1: merge2 "^1.3.0" micromatch "^4.0.4" -fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -3453,12 +3574,7 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -fast-safe-stringify@2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" - integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== - -fast-safe-stringify@^2.1.1: +fast-safe-stringify@2.1.1, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -3482,7 +3598,7 @@ fecha@^4.2.0: resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce" integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== -fengari-interop@^0.1.2: +fengari-interop@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/fengari-interop/-/fengari-interop-0.1.3.tgz#3ad37a90e7430b69b365441e9fc0ba168942a146" integrity sha512-EtZ+oTu3kEwVJnoymFPBVLIbQcCoy9uWCVnMA6h3M/RqHkUBsLYp29+RRHf9rKr6GwjubWREU1O7RretFIXjHw== @@ -3534,9 +3650,22 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -finalhandler@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== dependencies: debug "2.6.9" @@ -3632,28 +3761,23 @@ foreground-child@^2.0.0: cross-spawn "^7.0.0" signal-exit "^3.0.2" -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -fork-ts-checker-webpack-plugin@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.2.0.tgz#d13af02e24d1b17f769af6bdf41c1e849e1615cc" - integrity sha512-DTNbOhq6lRdjYprukX54JMeYJgQ0zMow+R5BMLwWxEX2NAXthIkwnV8DBmsWjwNLSUItKZM4TCCJbtgrtKBu2Q== +fork-ts-checker-webpack-plugin@7.2.13: + version "7.2.13" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.13.tgz#51ffd6a2f96f03ab64b92f8aedf305dbf3dee0f1" + integrity sha512-fR3WRkOb4bQdWB/y7ssDUlVdrclvwtyCUIHCfivAoYxq9dF7XfrDKbMdZIfwJ7hxIAqkYSGeU7lLJE6xrxIBdg== dependencies: - "@babel/code-frame" "^7.8.3" - "@types/json-schema" "^7.0.5" - chalk "^4.1.0" - chokidar "^3.4.2" - cosmiconfig "^6.0.0" + "@babel/code-frame" "^7.16.7" + chalk "^4.1.2" + chokidar "^3.5.3" + cosmiconfig "^7.0.1" deepmerge "^4.2.2" - fs-extra "^9.0.0" - memfs "^3.1.2" + fs-extra "^10.0.0" + memfs "^3.4.1" minimatch "^3.0.4" - schema-utils "2.7.0" - semver "^7.3.2" - tapable "^1.0.0" + node-abort-controller "^3.0.1" + schema-utils "^3.1.1" + semver "^7.3.5" + tapable "^2.2.1" form-data@^2.3.1: version "2.5.1" @@ -3673,15 +3797,6 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - formidable@^1.2.0: version "1.2.6" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" @@ -3714,12 +3829,11 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@9.1.0, fs-extra@^9.0.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== +fs-extra@10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== dependencies: - at-least-node "^1.0.0" graceful-fs "^4.2.0" jsonfile "^6.0.1" universalify "^2.0.0" @@ -3733,14 +3847,14 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" -fs-minipass@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" - integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== dependencies: - minipass "^2.6.0" + minipass "^3.0.0" -fs-monkey@1.0.3: +fs-monkey@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== @@ -3755,16 +3869,6 @@ fsevents@^2.1.2, fsevents@~2.3.1, fsevents@~2.3.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== -fstream@^1.0.0, fstream@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" - integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -3775,19 +3879,34 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== dependencies: - aproba "^1.0.3" + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" + has-unicode "^2.0.1" + signal-exit "^3.0.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.5" gensync@^1.0.0-beta.2: version "1.0.0-beta.2" @@ -3850,13 +3969,6 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" @@ -3886,7 +3998,7 @@ glob@7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -3898,6 +4010,18 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, gl once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.2.0: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + globals@^11.1.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" @@ -3932,6 +4056,11 @@ graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== +graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -3942,19 +4071,6 @@ growly@^1.3.0: resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - has-bigints@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" @@ -3987,7 +4103,7 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" -has-unicode@^2.0.0: +has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= @@ -4065,16 +4181,10 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -http-errors@1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" - integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" +http-cache-semantics@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== http-errors@1.8.1: version "1.8.1" @@ -4087,16 +4197,16 @@ http-errors@1.8.1: statuses ">= 1.5.0 < 2" toidentifier "1.0.1" -http-errors@~1.7.2: - version "1.7.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" - integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== dependencies: - depd "~1.1.2" + depd "2.0.0" inherits "2.0.4" - setprototypeof "1.1.1" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" http-proxy-agent@^4.0.1: version "4.0.1" @@ -4107,15 +4217,6 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - https-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" @@ -4129,25 +4230,32 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore-walk@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" - integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ== - dependencies: - minimatch "^3.0.4" - ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" @@ -4158,7 +4266,7 @@ ignore@^5.1.4, ignore@^5.1.8: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== -import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: +import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -4184,6 +4292,11 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -4192,16 +4305,11 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" - integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= - ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -4226,6 +4334,27 @@ inquirer@7.3.3: strip-ansi "^6.0.0" through "^2.3.6" +inquirer@8.2.4: + version "8.2.4" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" + integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + wrap-ansi "^7.0.0" + internal-slot@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" @@ -4240,33 +4369,36 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== -ioredis-mock@^5.5.4: - version "5.8.1" - resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-5.8.1.tgz#5221260a7165c1d0a6db40f7245d7eb8996cd9d2" - integrity sha512-YWUoE7ZZLzo2fJMWLjeh3F/TkgHqeazdOeExZskit+/2ZSA0bsFPkXiKMOUHZxjOk2JskOP9iuYvf/iO3mhMZg== +ioredis-mock@^8.2.2: + version "8.2.2" + resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-8.2.2.tgz#9bae98378a396d3ba748fab604ede1b90c53eadf" + integrity sha512-XyJfcF6pqcLHwAYtldkzaLtjRxPw7d8U0FUfjgQ5U/d0vVhFxiXbqsILR4FEOp+ygzyZgBA8xye+uPKu74IH1A== dependencies: + "@ioredis/as-callback" "^3.0.0" + "@ioredis/commands" "^1.1.1" fengari "^0.1.4" - fengari-interop "^0.1.2" - lodash "^4.17.21" - standard-as-callback "^2.1.0" + fengari-interop "^0.1.3" -ioredis@^4.27.1: - version "4.28.5" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.5.tgz#5c149e6a8d76a7f8fa8a504ffc85b7d5b6797f9f" - integrity sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A== +ioredis@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.2.2.tgz#212467e04f6779b4e0e800cece7bb7d3d7b546d2" + integrity sha512-wryKc1ur8PcCmNwfcGkw5evouzpbDXxxkMkzPK8wl4xQfQf7lHe11Jotell5ikMVAtikXJEu/OJVaoV51BggRQ== dependencies: + "@ioredis/commands" "^1.1.1" cluster-key-slot "^1.1.0" - debug "^4.3.1" - denque "^1.1.0" + debug "^4.3.4" + denque "^2.0.1" lodash.defaults "^4.2.0" - lodash.flatten "^4.4.0" lodash.isarguments "^3.1.0" - p-map "^2.1.0" - redis-commands "1.7.0" redis-errors "^1.2.0" redis-parser "^3.0.0" standard-as-callback "^2.1.0" +ip@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" + integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -4403,13 +4535,6 @@ is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" @@ -4437,6 +4562,11 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== + is-negative-zero@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" @@ -4525,7 +4655,7 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" -is-typedarray@^1.0.0, is-typedarray@~1.0.0: +is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= @@ -4554,11 +4684,6 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -4581,11 +4706,6 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.0.0-alpha.1, istanbul-lib-coverage@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" @@ -5032,10 +5152,10 @@ jest-worker@^26.6.2: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^27.4.1: - version "27.4.5" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.4.5.tgz#d696e3e46ae0f24cff3fa7195ffba22889262242" - integrity sha512-f2s8kEdy15cv9r7q4KkzGXvlY0JTcmCbMHZBfSQDwW77REr45IDWwd0lksDFeVHH2jJ5pqb90T77XscrjeGzzg== +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== dependencies: "@types/node" "*" merge-stream "^2.0.0" @@ -5078,6 +5198,13 @@ js-yaml@4.0.0: dependencies: argparse "^2.0.1" +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" @@ -5086,18 +5213,6 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - jsdom@^16.4.0: version "16.7.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.7.0.tgz#918ae71965424b197c819f8183a754e18977b710" @@ -5136,12 +5251,12 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: +json-parse-better-errors@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== -json-parse-even-better-errors@^2.3.0: +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== @@ -5156,21 +5271,11 @@ json-schema-traverse@^1.0.0: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - json5@2.x, json5@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" @@ -5185,10 +5290,20 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" -jsonc-parser@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" - integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== +json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +jsonc-parser@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.1.0.tgz#73b8f0e5c940b83d03476bc2e51a20ef0932615d" + integrity sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg== + +jsonc-parser@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== jsonfile@^6.0.1: version "6.1.0" @@ -5199,23 +5314,13 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - -keytar@^7.7.0: - version "7.7.0" - resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.7.0.tgz#3002b106c01631aa79b1aa9ee0493b94179bbbd2" - integrity sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A== +keytar@^7.9.0: + version "7.9.0" + resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.9.0.tgz#4c6225708f51b50cbf77c5aae81721964c2918cb" + integrity sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ== dependencies: - node-addon-api "^3.0.0" - prebuild-install "^6.0.0" + node-addon-api "^4.3.0" + prebuild-install "^7.0.1" kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" @@ -5326,11 +5431,6 @@ lodash.defaults@^4.2.0: resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== - lodash.flattendeep@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" @@ -5351,11 +5451,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.toarray@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" - integrity sha1-JMS/zWsvuji/0FlNsRedjptlZWE= - lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" @@ -5373,7 +5468,7 @@ log-symbols@4.0.0: dependencies: chalk "^4.0.0" -log-symbols@^4.0.0, log-symbols@^4.1.0: +log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== @@ -5406,19 +5501,19 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" -macos-release@^2.2.0: +macos-release@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.0.tgz#067c2c88b5f3fb3c56a375b2ec93826220fa1ff2" integrity sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g== -magic-string@0.25.7: - version "0.25.7" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" - integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== +magic-string@0.26.2: + version "0.26.2" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.2.tgz#5331700e4158cd6befda738bb6b0c7b93c0d4432" + integrity sha512-NzzlXpclt5zAbmo6h6jNc8zl2gNRGHvmsZW4IvZhTC4W7k4OlLP+S5YLussa/r3ixNT66KOQfNORlXHSOy/X4A== dependencies: - sourcemap-codec "^1.4.4" + sourcemap-codec "^1.4.8" -make-dir@^3.0.0, make-dir@^3.0.2: +make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -5430,6 +5525,28 @@ make-error@1.x, make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== +make-fetch-happen@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.2.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.2" + promise-retry "^2.0.1" + socks-proxy-agent "^6.0.0" + ssri "^8.0.0" + makeerror@1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" @@ -5463,12 +5580,12 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -memfs@^3.1.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.0.tgz#8bc12062b973be6b295d4340595736a656f0a257" - integrity sha512-o/RfP0J1d03YwsAxyHxAYs2kyJp55AFkMazlFAZFR2I2IXkxiUTXRabJ6RmNNCQ83LAD2jy52Khj0m3OffpNdA== +memfs@^3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.7.tgz#e5252ad2242a724f938cb937e3c4f7ceb1f70e5a" + integrity sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw== dependencies: - fs-monkey "1.0.3" + fs-monkey "^1.0.3" memoizee@^0.4.15: version "0.4.15" @@ -5544,13 +5661,25 @@ mime-db@1.51.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.51.0.tgz#d9ff62451859b18342d960850dc3cfb77e63fb0c" integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.19, mime-types@~2.1.24: +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.24: version "2.1.34" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.34.tgz#5a712f9ec1503511a945803640fafe09d3793c24" integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== dependencies: mime-db "1.51.0" +mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime@1.6.0, mime@^1.4.1: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -5561,10 +5690,10 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -mimic-response@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" - integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== minimatch@3.0.4, minimatch@^3.0.4: version "3.0.4" @@ -5573,25 +5702,76 @@ minimatch@3.0.4, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@1.2.5, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minipass@^2.6.0, minipass@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" - integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== +minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" + minipass "^3.0.0" -minizlib@^1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" - integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== +minipass-fetch@^1.3.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== dependencies: - minipass "^2.9.0" + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: + version "3.3.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.4.tgz#ca99f95dd77c43c7a76bf51e6d200025eee0ffae" + integrity sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw== + dependencies: + yallist "^4.0.0" + +minizlib@^2.0.0, minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" mixin-deep@^1.2.0: version "1.3.2" @@ -5606,18 +5786,25 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@1.x, mkdirp@^1.0.4: +mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.1: +mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: minimist "^1.2.5" +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + mocha-junit-reporter@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-2.0.2.tgz#d521689b651dc52f52044739f8ffb368be415731" @@ -5678,11 +5865,6 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -5693,17 +5875,16 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -multer@1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a" - integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg== +multer@1.4.4-lts.1: + version "1.4.4-lts.1" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.4-lts.1.tgz#24100f701a4611211cfae94ae16ea39bb314e04d" + integrity sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg== dependencies: append-field "^1.0.0" - busboy "^0.2.11" + busboy "^1.0.0" concat-stream "^1.5.2" - mkdirp "^0.5.1" + mkdirp "^0.5.4" object-assign "^4.1.1" - on-finished "^2.3.0" type-is "^1.6.4" xtend "^4.0.0" @@ -5753,20 +5934,16 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -needle@^2.2.1: - version "2.9.1" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.9.1.tgz#22d1dffbe3490c2b83e301f7709b6736cd8f2684" - integrity sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +negotiator@0.6.3, negotiator@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -5800,49 +5977,52 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-abi@^2.21.0: - version "2.30.1" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf" - integrity sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w== +node-abi@^3.3.0: + version "3.24.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.24.0.tgz#b9d03393a49f2c7e147d0c99f180e680c27c1599" + integrity sha512-YPG3Co0luSu6GwOBsmIdGW6Wx0NyNDLg/hriIyDllVsNwnI6UeqaWShxC3lbH4LtEQUgoLP3XR1ndXiDAWvmRw== dependencies: - semver "^5.4.1" + semver "^7.3.5" -node-addon-api@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" - integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== +node-abort-controller@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" + integrity sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw== -node-emoji@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" - integrity sha512-Yt3384If5H6BYGVHiHwTL+99OzJKHhgp82S8/dktEK73T26BazdgZ4JZh92xSVtGNJvz9UbXdNAc5hcrXV42vw== +node-addon-api@^4.2.0, node-addon-api@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" + integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== + +node-emoji@1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" + integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== dependencies: - lodash.toarray "^4.4.0" + lodash "^4.17.21" -node-fetch@^2.6.1: +node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== dependencies: whatwg-url "^5.0.0" -node-gyp@3.x: - version "3.8.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" - integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== +node-gyp@8.x: + version "8.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" + integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== dependencies: - fstream "^1.0.0" - glob "^7.0.3" - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "^2.87.0" - rimraf "2" - semver "~5.3.0" - tar "^2.0.0" - which "1" + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^9.1.0" + nopt "^5.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" node-int64@^0.4.0: version "0.4.0" @@ -5861,22 +6041,6 @@ node-notifier@^8.0.0: uuid "^8.3.0" which "^2.0.2" -node-pre-gyp@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054" - integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - node-preload@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/node-preload/-/node-preload-0.2.1.tgz#c03043bb327f417a18fee7ab7ee57b408a144301" @@ -5894,20 +6058,12 @@ node-version-compare@^1.0.3: resolved "https://registry.yarnpkg.com/node-version-compare/-/node-version-compare-1.0.3.tgz#ca6d2005e67822fb4dfa259e08f1f6cfaabe2e81" integrity sha512-unO5GpBAh5YqeGULMLpmDT94oanSDMwtZB8KHTKCH/qrGv8bHN0mlDj9xQDAicCYXv2OLnzdi67lidCrcVotVw== -"nopt@2 || 3": - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= - dependencies: - abbrev "1" - -nopt@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" - integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== dependencies: abbrev "1" - osenv "^0.1.4" normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" @@ -5931,27 +6087,6 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-bundled@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" - integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== - dependencies: - npm-normalize-package-bin "^1.0.1" - -npm-normalize-package-bin@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" - integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== - -npm-packlist@^1.1.6: - version "1.4.8" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" - integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - npm-normalize-package-bin "^1.0.1" - npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -5966,20 +6101,25 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.1, npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== + dependencies: + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" nwsapi@^2.2.0: version "2.2.0" @@ -6019,12 +6159,7 @@ nyc@^15.1.0: test-exclude "^6.0.0" yargs "^15.0.2" -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -6043,12 +6178,12 @@ object-diff@^0.0.4: resolved "https://registry.yarnpkg.com/object-diff/-/object-diff-0.0.4.tgz#d883b0444fe8fd6e04e595d7bb665682c916047f" integrity sha1-2IOwRE/o/W4E5ZXXu2ZWgskWBH8= -object-hash@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.1.1.tgz#9447d0279b4fcf80cff3259bf66a1dc73afabe09" - integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ== +object-hash@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== -object-hash@2.2.0, object-hash@^2.0.1: +object-hash@^2.0.1: version "2.2.0" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== @@ -6105,7 +6240,14 @@ object.values@^1.1.5: define-properties "^1.1.3" es-abstract "^1.19.1" -on-finished@^2.3.0, on-finished@~2.3.0: +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= @@ -6133,11 +6275,6 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -optional@0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/optional/-/optional-0.1.4.tgz#cdb1a9bedc737d2025f690ceeb50e049444fd5b3" - integrity sha512-gtvrrCfkE08wKcgXaVwQVgwEQ8vel2dc5DDBn9RLQZ3YtmtkBss6A2HY6BnJH4N/4Ku97Ri/SF8sNWE2225WJw== - optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -6162,24 +6299,10 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" -ora@5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ora/-/ora-5.3.0.tgz#fb832899d3a1372fe71c8b2c534bbfe74961bb6f" - integrity sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g== - dependencies: - bl "^4.0.3" - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-spinners "^2.5.0" - is-interactive "^1.0.0" - log-symbols "^4.0.0" - strip-ansi "^6.0.0" - wcwidth "^1.0.1" - -ora@5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.0.tgz#42eda4855835b9cd14d33864c97a3c95a3f56bf4" - integrity sha512-1StwyXQGoU6gdjYkyVcqOLnVlbKj+6yPNNOxJVgpt9t4eksKjiriiHuxktLYkgllwk+D6MbC4ihH84L1udRXPg== +ora@5.4.1, ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== dependencies: bl "^4.1.0" chalk "^4.1.0" @@ -6191,32 +6314,19 @@ ora@5.4.0: strip-ansi "^6.0.0" wcwidth "^1.0.1" -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-name@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/os-name/-/os-name-4.0.0.tgz#6c05c09c41c15848ea74658d12c9606f0f286599" - integrity sha512-caABzDdJMbtykt7GmSogEat3faTKQhmZf0BS5l/pZGmP0vPWQjXWqOhbLyK+b6j2/DQPmEvYdzLXJXXLJNVDNg== +os-name@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/os-name/-/os-name-4.0.1.tgz#32cee7823de85a8897647ba4d76db46bf845e555" + integrity sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw== dependencies: - macos-release "^2.2.0" + macos-release "^2.5.0" windows-release "^4.0.0" -os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: +os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -osenv@0, osenv@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - p-each-series@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" @@ -6276,11 +6386,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== - p-map@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-3.0.0.tgz#d704d9af8a2ba684e2600d9a215983d4141a979d" @@ -6288,6 +6393,13 @@ p-map@^3.0.0: dependencies: aggregate-error "^3.0.0" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -6405,6 +6517,11 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= +path-to-regexp@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.2.5.tgz#0b426991e387fc4c675de23557f358715eb66fb0" + integrity sha512-l6qtdDPIkmAmzEO6egquYDfqQGPMRNGjYtrU13HAXb3YSRrt7HSb1sJY0pKp6o2bAa86tSB6iwaW2JbthPKr7Q== + path-to-regexp@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" @@ -6420,11 +6537,6 @@ pathval@^1.1.1: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -6469,22 +6581,21 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= -prebuild-install@^6.0.0: - version "6.1.4" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f" - integrity sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ== +prebuild-install@^7.0.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== dependencies: - detect-libc "^1.0.3" + detect-libc "^2.0.0" expand-template "^2.0.3" github-from-package "0.0.0" minimist "^1.2.3" mkdirp-classic "^0.5.3" napi-build-utils "^1.0.1" - node-abi "^2.21.0" - npmlog "^4.0.1" + node-abi "^3.3.0" pump "^3.0.0" rc "^1.2.7" - simple-get "^3.0.3" + simple-get "^4.0.0" tar-fs "^2.0.0" tunnel-agent "^0.6.0" @@ -6525,6 +6636,19 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -6533,7 +6657,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -proxy-addr@~2.0.5, proxy-addr@~2.0.7: +proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== @@ -6546,7 +6670,7 @@ prr@~1.0.1: resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= -psl@^1.1.28, psl@^1.1.33: +psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== @@ -6564,10 +6688,12 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -qs@6.7.0: - version "6.7.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" - integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@6.10.3: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== + dependencies: + side-channel "^1.0.4" qs@6.9.6: version "6.9.6" @@ -6581,11 +6707,6 @@ qs@^6.5.1: dependencies: side-channel "^1.0.4" -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -6603,16 +6724,6 @@ range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" - integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== - dependencies: - bytes "3.1.0" - http-errors "1.7.2" - iconv-lite "0.4.24" - unpipe "1.0.0" - raw-body@2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.2.tgz#baf3e9c21eebced59dd6533ac872b71f7b61cb32" @@ -6623,6 +6734,16 @@ raw-body@2.4.2: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -6666,17 +6787,7 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@1.1.x: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - -readable-stream@^2.0.1, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.5: +readable-stream@^2.0.1, readable-stream@^2.2.2, readable-stream@^2.3.5: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -6689,7 +6800,7 @@ readable-stream@^2.0.1, readable-stream@^2.0.6, readable-stream@^2.2.2, readable string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.1.1, readable-stream@^3.4.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -6724,11 +6835,6 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -redis-commands@1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" - integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== - redis-errors@^1.0.0, redis-errors@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" @@ -6791,32 +6897,6 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= -request@^2.87.0: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -6875,18 +6955,16 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@2, rimraf@^2.6.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -6911,30 +6989,30 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@6.6.3: - version "6.6.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" - integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== - dependencies: - tslib "^1.9.0" - -rxjs@^6.5.2, rxjs@^6.6.0, rxjs@^6.6.7: +rxjs@6.6.7, rxjs@^6.5.2, rxjs@^6.6.0: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== dependencies: tslib "^1.9.0" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +rxjs@^7.5.5, rxjs@^7.5.6: + version "7.5.6" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc" + integrity sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw== + dependencies: + tslib "^2.1.0" -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" @@ -6947,7 +7025,7 @@ safe-stable-stringify@^1.1.0: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz#c8a220ab525cd94e60ebf47ddc404d610dc5d84a" integrity sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw== -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -6967,7 +7045,7 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sax@>=0.6.0, sax@^1.2.4: +sax@>=0.6.0: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -6979,16 +7057,7 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" -schema-utils@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== - dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" - -schema-utils@^3.0.0, schema-utils@^3.1.1: +schema-utils@^3.1.0, schema-utils@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== @@ -6997,7 +7066,7 @@ schema-utils@^3.0.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0: +"semver@2 || 3 || 4 || 5", semver@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -7014,15 +7083,10 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= - -send@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" - integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== +send@0.17.2: + version "0.17.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" + integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== dependencies: debug "2.6.9" depd "~1.1.2" @@ -7031,31 +7095,31 @@ send@0.17.1: escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "~1.7.2" + http-errors "1.8.1" mime "1.6.0" - ms "2.1.1" + ms "2.1.3" on-finished "~2.3.0" range-parser "~1.2.1" statuses "~1.5.0" -send@0.17.2: - version "0.17.2" - resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" - integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== dependencies: debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" + depd "2.0.0" + destroy "1.2.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "1.8.1" + http-errors "2.0.0" mime "1.6.0" ms "2.1.3" - on-finished "~2.3.0" + on-finished "2.4.1" range-parser "~1.2.1" - statuses "~1.5.0" + statuses "2.0.1" serialize-javascript@5.0.1: version "5.0.1" @@ -7071,16 +7135,6 @@ serialize-javascript@^6.0.0: dependencies: randombytes "^2.1.0" -serve-static@1.14.1: - version "1.14.1" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" - integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.17.1" - serve-static@1.14.2: version "1.14.2" resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.2.tgz#722d6294b1d62626d41b43a013ece4598d292bfa" @@ -7091,7 +7145,17 @@ serve-static@1.14.2: parseurl "~1.3.3" send "0.17.2" -set-blocking@^2.0.0, set-blocking@~2.0.0: +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -7106,11 +7170,6 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" -setprototypeof@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" - integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== - setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -7148,10 +7207,10 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shelljs@0.8.4: - version "0.8.4" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" - integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== +shelljs@0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== dependencies: glob "^7.0.0" interpret "^1.0.0" @@ -7176,17 +7235,22 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== +signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + simple-concat@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== -simple-get@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55" - integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA== +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== dependencies: - decompress-response "^4.2.0" + decompress-response "^6.0.0" once "^1.3.1" simple-concat "^1.0.0" @@ -7216,6 +7280,11 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -7251,6 +7320,11 @@ socket.io-adapter@~2.3.3: resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz#4d6111e4d42e9f7646e365b4f578269821f13486" integrity sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ== +socket.io-adapter@~2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz#b50a4a9ecdd00c34d4c8c808224daa1a786152a6" + integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg== + socket.io-client@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.4.1.tgz#b6aa9448149d09b8d0b2bbf3d2fac310631fdec9" @@ -7287,7 +7361,7 @@ socket.io-parser@~4.1.1: "@socket.io/component-emitter" "~3.0.0" debug "~4.3.1" -socket.io@*, socket.io@4.4.0, socket.io@^4.4.0: +socket.io@*, socket.io@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.4.0.tgz#8140a0db2c22235f88a6dceb867e4d5c9bd70507" integrity sha512-bnpJxswR9ov0Bw6ilhCvO38/1WPtE3eA2dtxi2Iq4/sFebiDJQzgKNYA7AuVVdGW09nrESXd90NbZqtDd9dzRQ== @@ -7299,10 +7373,34 @@ socket.io@*, socket.io@4.4.0, socket.io@^4.4.0: socket.io-adapter "~2.3.3" socket.io-parser "~4.0.4" -source-list-map@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" - integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== +socket.io@4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.5.1.tgz#aa7e73f8a6ce20ee3c54b2446d321bbb6b1a9029" + integrity sha512-0y9pnIso5a9i+lJmsCdtmTTgJFFSvNQKDnPQRz28mGNnxbmqYg2QPtJTLFxhymFZhAIn50eHAKzJeiNaKr+yUQ== + dependencies: + accepts "~1.3.4" + base64id "~2.0.0" + debug "~4.3.2" + engine.io "~6.2.0" + socket.io-adapter "~2.4.0" + socket.io-parser "~4.0.4" + +socks-proxy-agent@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce" + integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks@^2.6.2: + version "2.7.0" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.0.tgz#f9225acdb841e874dca25f870e9130990f3913d0" + integrity sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA== + dependencies: + ip "^2.0.0" + smart-buffer "^4.2.0" source-map-resolve@^0.5.0: version "0.5.3" @@ -7315,7 +7413,7 @@ source-map-resolve@^0.5.0: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.17, source-map-support@^0.5.19, source-map-support@^0.5.6, source-map-support@~0.5.20: +source-map-support@0.5.21, source-map-support@^0.5.17, source-map-support@^0.5.19, source-map-support@^0.5.6, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -7328,10 +7426,10 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== -source-map@0.7.3, source-map@^0.7.3, source-map@~0.7.2: - version "0.7.3" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +source-map@0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== source-map@^0.5.0, source-map@^0.5.6: version "0.5.7" @@ -7343,7 +7441,12 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -sourcemap-codec@^1.4.4: +source-map@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +sourcemap-codec@^1.4.8: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== @@ -7408,30 +7511,23 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -sqlite3@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.0.2.tgz#00924adcc001c17686e0a6643b6cbbc2d3965083" - integrity sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA== +sqlite3@^5.0.11: + version "5.0.11" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.0.11.tgz#102c835d70be66da9d95a383fd6ea084a082ef7f" + integrity sha512-4akFOr7u9lJEeAWLJxmwiV43DJcGV7w3ab7SjQFAFaTVyknY3rZjvXTKIVtWqUoY4xwhjwoHKYs2HDW2SoHVsA== dependencies: - node-addon-api "^3.0.0" - node-pre-gyp "^0.11.0" + "@mapbox/node-pre-gyp" "^1.0.0" + node-addon-api "^4.2.0" + tar "^6.1.11" optionalDependencies: - node-gyp "3.x" - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" + node-gyp "8.x" + +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" stack-trace@0.0.x: version "0.0.10" @@ -7458,15 +7554,20 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -streamsearch@0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" - integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== string-length@^4.0.1: version "4.0.2" @@ -7476,16 +7577,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" - -"string-width@^1.0.2 || 2", string-width@^2.1.1: +"string-width@^1.0.2 || 2": version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -7534,11 +7626,6 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= - string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -7546,13 +7633,6 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - strip-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" @@ -7664,6 +7744,11 @@ supports-hyperlinks@^2.0.0: has-flag "^4.0.0" supports-color "^7.0.0" +swagger-ui-dist@4.14.0: + version "4.14.0" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-4.14.0.tgz#e34d807464eb84578c43902e393084a1a6fbda52" + integrity sha512-TBzhheU15s+o54Cgk9qxuYcZMiqSm/SkvKnapoGHOF66kz0Y5aGjpzj5BT/vpBbn6rTPJ9tUYXQxuDWfsjiGMw== + swagger-ui-dist@>=4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-4.1.3.tgz#2be9f9de9b5c19132fa4a5e40933058c151563dc" @@ -7676,10 +7761,10 @@ swagger-ui-express@^4.1.4: dependencies: swagger-ui-dist ">=4.1.3" -symbol-observable@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-3.0.0.tgz#eea8f6478c651018e059044268375c408c15c533" - integrity sha512-6tDOXSHiVjuCaasQSWTmHUWn4PuG7qa3+1WT031yTc/swT7+rLiw3GOrFxaH1E3lLP09dH3bVuVDf2gK5rxG3Q== +symbol-observable@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" + integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== symbol-tree@^3.2.4: version "3.2.4" @@ -7702,7 +7787,7 @@ tapable@^1.0.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -tapable@^2.1.1, tapable@^2.2.0: +tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== @@ -7728,27 +7813,17 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -tar@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" - integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== - dependencies: - block-stream "*" - fstream "^1.0.12" - inherits "2" - -tar@^4: - version "4.4.19" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3" - integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== +tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== dependencies: - chownr "^1.1.4" - fs-minipass "^1.2.7" - minipass "^2.9.0" - minizlib "^1.3.3" - mkdirp "^0.5.5" - safe-buffer "^5.2.1" - yallist "^3.1.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" terminal-link@^2.0.0: version "2.1.1" @@ -7758,24 +7833,25 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" -terser-webpack-plugin@^5.1.1: - version "5.3.0" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz#21641326486ecf91d8054161c816e464435bae9f" - integrity sha512-LPIisi3Ol4chwAaPP8toUJ3L4qCM1G0wao7L3qNv57Drezxj6+VEyySpPw4B1HSO2Eg/hDY/MNF5XihCAoqnsQ== +terser-webpack-plugin@^5.1.3: + version "5.3.6" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz#5590aec31aa3c6f771ce1b1acca60639eab3195c" + integrity sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ== dependencies: - jest-worker "^27.4.1" + "@jridgewell/trace-mapping" "^0.3.14" + jest-worker "^27.4.5" schema-utils "^3.1.1" serialize-javascript "^6.0.0" - source-map "^0.6.1" - terser "^5.7.2" + terser "^5.14.1" -terser@^5.7.2: - version "5.10.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.10.0.tgz#b86390809c0389105eb0a0b62397563096ddafcc" - integrity sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA== +terser@^5.14.1: + version "5.15.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.0.tgz#e16967894eeba6e1091509ec83f0c60e179f2425" + integrity sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA== dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" commander "^2.20.0" - source-map "~0.7.2" source-map-support "~0.5.20" test-exclude@^6.0.0: @@ -7878,11 +7954,6 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" -toidentifier@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" - integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== - toidentifier@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" @@ -7897,14 +7968,6 @@ tough-cookie@^4.0.0: punycode "^2.1.1" universalify "^0.1.2" -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - tr46@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" @@ -7989,14 +8052,14 @@ ts-node@^9.1.1: source-map-support "^0.5.17" yn "3.1.1" -tsconfig-paths-webpack-plugin@3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.1.tgz#e4dbf492a20dca9caab60086ddacb703afc2b726" - integrity sha512-n5CMlUUj+N5pjBhBACLq4jdr9cPTitySCjIosoQm0zwK99gmrcTGAfY9CwxRFT9+9OleNWXPRUcxsKP4AYExxQ== +tsconfig-paths-webpack-plugin@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.0.tgz#84008fc3e3e0658fdb0262758b07b4da6265ff1a" + integrity sha512-fw/7265mIWukrSHd0i+wSwx64kYUSAKPfxRDksjKIYTxSAp9W9/xcZVBF4Kl0eqQd5eBpAQ/oQrc5RyM/0c1GQ== dependencies: chalk "^4.1.0" enhanced-resolve "^5.7.0" - tsconfig-paths "^3.9.0" + tsconfig-paths "^4.0.0" tsconfig-paths-webpack-plugin@^3.3.0: version "3.5.2" @@ -8007,14 +8070,13 @@ tsconfig-paths-webpack-plugin@^3.3.0: enhanced-resolve "^5.7.0" tsconfig-paths "^3.9.0" -tsconfig-paths@3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" - integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== +tsconfig-paths@4.1.0, tsconfig-paths@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.1.0.tgz#f8ef7d467f08ae3a695335bf1ece088c5538d2c1" + integrity sha512-AHx4Euop/dXFC+Vx589alFba8QItjF+8hf8LtmuiCwHyI4rHXQtOOENaM8kvYf5fR0dRChy3wzWIZ9WbB7FWow== dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.1" - minimist "^1.2.0" + json5 "^2.2.1" + minimist "^1.2.6" strip-bom "^3.0.0" tsconfig-paths@^3.11.0, tsconfig-paths@^3.5.0, tsconfig-paths@^3.9.0: @@ -8027,12 +8089,12 @@ tsconfig-paths@^3.11.0, tsconfig-paths@^3.5.0, tsconfig-paths@^3.9.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" - integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== +tslib@2.4.0, tslib@^2.3.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== -tslib@2.3.1, tslib@>=1.9.0, tslib@^2.1.0: +tslib@>=1.9.0, tslib@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== @@ -8056,11 +8118,6 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -8100,7 +8157,7 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: +type-is@^1.6.4, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -8130,32 +8187,33 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typeorm@^0.2.29: - version "0.2.41" - resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.41.tgz#88758101ac158dc0a0a903d70eaacea2974281cc" - integrity sha512-/d8CLJJxKPgsnrZWiMyPI0rz2MFZnBQrnQ5XP3Vu3mswv2WPexb58QM6BEtmRmlTMYN5KFWUz8SKluze+wS9xw== +typeorm@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.9.tgz#ad0f525d81c081fd11006f97030f47a55978ac81" + integrity sha512-xNcE44D4hn74n7pjuMog9hRgep+BiO3IBpjEaQZ8fb56zsDz7xHT1GAeWwmGuuU+4nDEELp2mIqgSCR+zxR7Jw== dependencies: "@sqltools/formatter" "^1.2.2" app-root-path "^3.0.0" buffer "^6.0.3" chalk "^4.1.0" cli-highlight "^2.1.11" - debug "^4.3.1" - dotenv "^8.2.0" - glob "^7.1.6" - js-yaml "^4.0.0" + date-fns "^2.28.0" + debug "^4.3.3" + dotenv "^16.0.0" + glob "^7.2.0" + js-yaml "^4.1.0" mkdirp "^1.0.4" reflect-metadata "^0.1.13" sha.js "^2.4.11" - tslib "^2.1.0" + tslib "^2.3.1" + uuid "^8.3.2" xml2js "^0.4.23" - yargs "^17.0.1" - zen-observable-ts "^1.0.0" + yargs "^17.3.1" -typescript@4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3" - integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw== +typescript@4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== typescript@^4.0.5: version "4.5.4" @@ -8182,6 +8240,20 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + universalify@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -8232,17 +8304,12 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@8.3.1: - version "8.3.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" - integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== - uuid@8.3.2, uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uuid@^3.2.1, uuid@^3.3.2, uuid@^3.3.3: +uuid@^3.2.1, uuid@^3.3.3: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -8279,15 +8346,6 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" @@ -8309,10 +8367,10 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.12" -watchpack@^2.0.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25" - integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA== +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== dependencies: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" @@ -8339,47 +8397,45 @@ webidl-conversions@^6.1.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== -webpack-node-externals@2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-2.5.2.tgz#178e017a24fec6015bc9e672c77958a6afac861d" - integrity sha512-aHdl/y2N7PW2Sx7K+r3AxpJO+aDMcYzMQd60Qxefq3+EwhewSbTBqNumOsCE1JsCUNoyfGj5465N0sSf6hc/5w== - -webpack-sources@^2.1.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.3.1.tgz#570de0af163949fe272233c2cefe1b56f74511fd" - integrity sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA== - dependencies: - source-list-map "^2.0.1" - source-map "^0.6.1" - -webpack@5.28.0: - version "5.28.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.28.0.tgz#0de8bcd706186b26da09d4d1e8cbd3e4025a7c2f" - integrity sha512-1xllYVmA4dIvRjHzwELgW4KjIU1fW4PEuEnjsylz7k7H5HgPOctIq7W1jrt3sKH9yG5d72//XWzsHhfoWvsQVg== - dependencies: - "@types/eslint-scope" "^3.7.0" - "@types/estree" "^0.0.46" - "@webassemblyjs/ast" "1.11.0" - "@webassemblyjs/wasm-edit" "1.11.0" - "@webassemblyjs/wasm-parser" "1.11.0" - acorn "^8.0.4" +webpack-node-externals@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz#1a3407c158d547a9feb4229a9e3385b7b60c9917" + integrity sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ== + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@5.74.0: + version "5.74.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980" + integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.7.1" + acorn-import-assertions "^1.7.6" browserslist "^4.14.5" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.7.0" - es-module-lexer "^0.4.0" - eslint-scope "^5.1.1" + enhanced-resolve "^5.10.0" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" events "^3.2.0" glob-to-regexp "^0.4.1" - graceful-fs "^4.2.4" - json-parse-better-errors "^1.0.2" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" loader-runner "^4.2.0" mime-types "^2.1.27" neo-async "^2.6.2" - schema-utils "^3.0.0" + schema-utils "^3.1.0" tapable "^2.1.1" - terser-webpack-plugin "^5.1.1" - watchpack "^2.0.0" - webpack-sources "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.4.0" + webpack-sources "^3.2.3" whatwg-encoding@^1.0.5: version "1.0.5" @@ -8426,13 +8482,6 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@1, which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - which@2.0.2, which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -8440,6 +8489,13 @@ which@2.0.2, which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + wide-align@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" @@ -8447,7 +8503,7 @@ wide-align@1.1.3: dependencies: string-width "^1.0.2 || 2" -wide-align@^1.1.0: +wide-align@^1.1.2, wide-align@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== @@ -8605,17 +8661,12 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yallist@^3.0.0, yallist@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^1.7.2: +yaml@^1.10.0: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== @@ -8630,6 +8681,11 @@ yargs-parser@20.x, yargs-parser@^20.2.2: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-parser@21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" @@ -8707,10 +8763,10 @@ yargs@^15.0.2, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^17.0.1: - version "17.3.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.0.tgz#295c4ffd0eef148ef3e48f7a2e0f58d0e4f26b1c" - integrity sha512-GQl1pWyDoGptFPJx9b9L6kmR33TGusZvXIZUT+BOz9f7X2L94oeAskFYLEg/FkhV06zZPBYLvLZRWeYId29lew== +yargs@^17.3.1: + version "17.5.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" + integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== dependencies: cliui "^7.0.2" escalade "^3.1.1" @@ -8739,16 +8795,3 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zen-observable-ts@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz#2d1aa9d79b87058e9b75698b92791c1838551f83" - integrity sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA== - dependencies: - "@types/zen-observable" "0.8.3" - zen-observable "0.8.15" - -zen-observable@0.8.15: - version "0.8.15" - resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" - integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== diff --git a/redisinsight/package.json b/redisinsight/package.json index 7b3867ea16..82dfb8fa53 100644 --- a/redisinsight/package.json +++ b/redisinsight/package.json @@ -2,7 +2,7 @@ "name": "redisinsight", "productName": "RedisInsight", "private": true, - "version": "2.8.0", + "version": "2.10.0", "description": "RedisInsight", "main": "./main.prod.js", "author": { @@ -12,7 +12,7 @@ }, "scripts": {}, "dependencies": { - "keytar": "^7.7.0", - "sqlite3": "^5.0.2" + "keytar": "^7.9.0", + "sqlite3": "^5.0.11" } } diff --git a/redisinsight/ui/.eslintrc.js b/redisinsight/ui/.eslintrc.js index 004cc971f6..f8f6e8953f 100644 --- a/redisinsight/ui/.eslintrc.js +++ b/redisinsight/ui/.eslintrc.js @@ -65,7 +65,7 @@ module.exports = { 'no-nested-ternary': 'off', 'no-param-reassign': ['error', { props: false }], 'sonarjs/no-duplicate-string': 'off', - 'sonarjs/cognitive-complexity': [1, 15], + 'sonarjs/cognitive-complexity': [1, 20], 'sonarjs/no-identical-functions': [0, 5], 'import/order': [ 1, diff --git a/redisinsight/ui/src/assets/assets.d.ts b/redisinsight/ui/src/assets/assets.d.ts index d13074bc79..c0fba07a95 100644 --- a/redisinsight/ui/src/assets/assets.d.ts +++ b/redisinsight/ui/src/assets/assets.d.ts @@ -1,5 +1,5 @@ declare module '*.svg' { - const ReactComponent: React.FC>; + const ReactComponent: React.FC> const content: string export { ReactComponent } diff --git a/redisinsight/ui/src/assets/img/icons/group_mode.svg b/redisinsight/ui/src/assets/img/icons/group_mode.svg new file mode 100644 index 0000000000..9b41ab8811 --- /dev/null +++ b/redisinsight/ui/src/assets/img/icons/group_mode.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/redisinsight/ui/src/assets/img/overview/input.svg b/redisinsight/ui/src/assets/img/overview/input.svg new file mode 100644 index 0000000000..d6669fc2c0 --- /dev/null +++ b/redisinsight/ui/src/assets/img/overview/input.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/redisinsight/ui/src/assets/img/overview/key.svg b/redisinsight/ui/src/assets/img/overview/key.svg new file mode 100644 index 0000000000..e844cbdbdf --- /dev/null +++ b/redisinsight/ui/src/assets/img/overview/key.svg @@ -0,0 +1,17 @@ + + + + + diff --git a/redisinsight/ui/src/assets/img/overview/measure.svg b/redisinsight/ui/src/assets/img/overview/measure.svg new file mode 100644 index 0000000000..bb364a8e26 --- /dev/null +++ b/redisinsight/ui/src/assets/img/overview/measure.svg @@ -0,0 +1,17 @@ + + + + + diff --git a/redisinsight/ui/src/assets/img/overview/memory.svg b/redisinsight/ui/src/assets/img/overview/memory.svg new file mode 100644 index 0000000000..eef854c42d --- /dev/null +++ b/redisinsight/ui/src/assets/img/overview/memory.svg @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/redisinsight/ui/src/assets/img/overview/output.svg b/redisinsight/ui/src/assets/img/overview/output.svg new file mode 100644 index 0000000000..6af5a48cc1 --- /dev/null +++ b/redisinsight/ui/src/assets/img/overview/output.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/redisinsight/ui/src/assets/img/overview/time.svg b/redisinsight/ui/src/assets/img/overview/time.svg new file mode 100644 index 0000000000..13cf5d166b --- /dev/null +++ b/redisinsight/ui/src/assets/img/overview/time.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/redisinsight/ui/src/assets/img/overview/user.svg b/redisinsight/ui/src/assets/img/overview/user.svg new file mode 100644 index 0000000000..e91ef77a16 --- /dev/null +++ b/redisinsight/ui/src/assets/img/overview/user.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/redisinsight/ui/src/assets/img/survey_icon.svg b/redisinsight/ui/src/assets/img/survey_icon.svg new file mode 100644 index 0000000000..81ba29f001 --- /dev/null +++ b/redisinsight/ui/src/assets/img/survey_icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/redisinsight/ui/src/components/action-bar/styles.module.scss b/redisinsight/ui/src/components/action-bar/styles.module.scss index f4875b7271..c85a41dd84 100644 --- a/redisinsight/ui/src/components/action-bar/styles.module.scss +++ b/redisinsight/ui/src/components/action-bar/styles.module.scss @@ -2,14 +2,6 @@ .euiPopoverTitle { text-transform: none !important; } - - .euiButton { - min-width: 93px !important; - - &:focus { - text-decoration: none !important; - } - } } .container { diff --git a/redisinsight/ui/src/components/advanced-settings/AdvancedSettings.tsx b/redisinsight/ui/src/components/advanced-settings/AdvancedSettings.tsx deleted file mode 100644 index 045d77ad0f..0000000000 --- a/redisinsight/ui/src/components/advanced-settings/AdvancedSettings.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { EuiLink, EuiSpacer } from '@elastic/eui' -import { toNumber } from 'lodash' - -import { validateCountNumber, validateNumber } from 'uiSrc/utils' -import { SCAN_COUNT_DEFAULT, PIPELINE_COUNT_DEFAULT } from 'uiSrc/constants/api' -import { updateUserConfigSettingsAction, userSettingsConfigSelector } from 'uiSrc/slices/user/user-settings' - -import AdvancedSettingsItem from './AdvancedSettingsItem' - -const AdvancedSettings = () => { - const { scanThreshold = '', batchSize = PIPELINE_COUNT_DEFAULT } = useSelector(userSettingsConfigSelector) ?? {} - - const dispatch = useDispatch() - - const handleApplyKeysToScanChanges = (value: string) => { - // eslint-disable-next-line no-nested-ternary - const data = value ? (+value < SCAN_COUNT_DEFAULT ? SCAN_COUNT_DEFAULT : +value) : null - - dispatch( - updateUserConfigSettingsAction( - { scanThreshold: data }, - ) - ) - } - - const handleApplyPipelineCountChanges = (value: string) => { - dispatch( - updateUserConfigSettingsAction( - { batchSize: toNumber(value) }, - ) - ) - } - - return ( - <> - - - validateNumber(value)} - title="Pipeline mode" - testid="pipeline-bunch" - placeholder={`${PIPELINE_COUNT_DEFAULT}`} - label="Commands in pipeline:" - summary={( - <> - {'Sets the size of a command batch for the '} - - pipeline - - {' mode in Workbench. 0 or 1 pipelines every command.'} - - )} - /> - - ) -} - -export default AdvancedSettings diff --git a/redisinsight/ui/src/components/analytics-tabs/AnalyticsTabs.spec.tsx b/redisinsight/ui/src/components/analytics-tabs/AnalyticsTabs.spec.tsx new file mode 100644 index 0000000000..1ed5e9379d --- /dev/null +++ b/redisinsight/ui/src/components/analytics-tabs/AnalyticsTabs.spec.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import reactRouterDom from 'react-router-dom' +import { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics' +import { act, fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import AnalyticsTabs from './AnalyticsTabs' + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: jest.fn, + }), +})) + +describe('StreamTabs', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('click on clusterDetails tab should call History push with /cluster-details path ', async () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + + await act(() => { + fireEvent.click(screen.getByTestId(`analytics-tab-${AnalyticsViewTab.ClusterDetails}`)) + }) + + expect(pushMock).toHaveBeenCalledTimes(1) + expect(pushMock).toHaveBeenCalledWith('/instanceId/analytics/cluster-details') + }) + it('click on SlowLog tab should call History push with /slowlog path ', async () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + + await act(() => { + fireEvent.click(screen.getByTestId(`analytics-tab-${AnalyticsViewTab.SlowLog}`)) + }) + + expect(pushMock).toHaveBeenCalledTimes(1) + expect(pushMock).toHaveBeenCalledWith('/instanceId/analytics/slowlog') + }) +}) diff --git a/redisinsight/ui/src/components/analytics-tabs/AnalyticsTabs.tsx b/redisinsight/ui/src/components/analytics-tabs/AnalyticsTabs.tsx new file mode 100644 index 0000000000..3e87a6de1f --- /dev/null +++ b/redisinsight/ui/src/components/analytics-tabs/AnalyticsTabs.tsx @@ -0,0 +1,49 @@ +import React, { useCallback } from 'react' +import { EuiTab, EuiTabs } from '@elastic/eui' +import { useDispatch, useSelector } from 'react-redux' +import { useParams, useHistory } from 'react-router-dom' + +import { Pages } from 'uiSrc/constants' +import { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics' +import { analyticsSettingsSelector, setAnalyticsViewTab } from 'uiSrc/slices/analytics/settings' + +import { analyticsViewTabs } from './constants' + +const AnalyticsTabs = () => { + const { viewTab } = useSelector(analyticsSettingsSelector) + const history = useHistory() + + const { instanceId } = useParams<{ instanceId: string }>() + + const dispatch = useDispatch() + + const onSelectedTabChanged = (id: AnalyticsViewTab) => { + if (id === AnalyticsViewTab.ClusterDetails) { + history.push(Pages.clusterDetails(instanceId)) + } + if (id === AnalyticsViewTab.SlowLog) { + history.push(Pages.slowLog(instanceId)) + } + dispatch(setAnalyticsViewTab(id)) + } + + const renderTabs = useCallback(() => + [...analyticsViewTabs].map(({ id, label }) => ( + onSelectedTabChanged(id)} + key={id} + data-testid={`analytics-tab-${id}`} + > + {label} + + )), [viewTab]) + + return ( + <> + {renderTabs()} + + ) +} + +export default AnalyticsTabs diff --git a/redisinsight/ui/src/components/analytics-tabs/constants.ts b/redisinsight/ui/src/components/analytics-tabs/constants.ts new file mode 100644 index 0000000000..bb03fbb79a --- /dev/null +++ b/redisinsight/ui/src/components/analytics-tabs/constants.ts @@ -0,0 +1,17 @@ +import { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics' + +interface AnalyticsTabs { + id: AnalyticsViewTab, + label: string, +} + +export const analyticsViewTabs: AnalyticsTabs[] = [ + { + id: AnalyticsViewTab.ClusterDetails, + label: 'Overview', + }, + { + id: AnalyticsViewTab.SlowLog, + label: 'Slow Log', + }, +] diff --git a/redisinsight/ui/src/components/analytics-tabs/index.ts b/redisinsight/ui/src/components/analytics-tabs/index.ts new file mode 100644 index 0000000000..8f9bcc33c3 --- /dev/null +++ b/redisinsight/ui/src/components/analytics-tabs/index.ts @@ -0,0 +1,3 @@ +import AnalyticsTabs from './AnalyticsTabs' + +export default AnalyticsTabs diff --git a/redisinsight/ui/src/components/bottom-group-components/components/bottom-group-minimized/BottomGroupMinimized.tsx b/redisinsight/ui/src/components/bottom-group-components/components/bottom-group-minimized/BottomGroupMinimized.tsx index 512eb68b3b..2c6bdcbd85 100644 --- a/redisinsight/ui/src/components/bottom-group-components/components/bottom-group-minimized/BottomGroupMinimized.tsx +++ b/redisinsight/ui/src/components/bottom-group-components/components/bottom-group-minimized/BottomGroupMinimized.tsx @@ -1,8 +1,9 @@ import React, { useEffect } from 'react' import cx from 'classnames' -import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui' +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiHideFor, EuiShowFor } from '@elastic/eui' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' +import { EXTERNAL_LINKS } from 'uiSrc/constants/links' import { clearSearchingCommand, @@ -14,6 +15,7 @@ import { } from 'uiSrc/slices/cli/cli-settings' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { monitorSelector, toggleHideMonitor, toggleMonitor } from 'uiSrc/slices/cli/monitor' +import SurveyIcon from 'uiSrc/assets/img/survey_icon.svg' import styles from '../../styles.module.scss' @@ -67,6 +69,12 @@ const BottomGroupMinimized = () => { dispatch(toggleMonitor()) } + const onClickSurvey = () => { + sendEventTelemetry({ + event: TelemetryEvent.USER_SURVEY_LINK_CLICKED + }) + } + return ( ) } diff --git a/redisinsight/ui/src/components/bottom-group-components/styles.module.scss b/redisinsight/ui/src/components/bottom-group-components/styles.module.scss index e675ec53e8..4f1bd33903 100644 --- a/redisinsight/ui/src/components/bottom-group-components/styles.module.scss +++ b/redisinsight/ui/src/components/bottom-group-components/styles.module.scss @@ -11,9 +11,31 @@ } .containerMinimized { + display: flex; + align-items: center; height: 26px; line-height: 26px; border: 1px solid var(--euiColorLightShade); + + .surveyLink { + display: flex; + align-items: center; + height: 100%; + padding: 0 12px; + color: var(--htmlColor) !important; + font: normal normal normal 12px/18px Graphik, sans-serif; + + &:hover { + background-color: var(--euiColorSecondary); + color: var(--euiColorPrimaryText) !important; + } + } + + .surveyIcon { + margin-right: 8px; + width: 18px; + height: 18px; + } } .componentBadgeItem { diff --git a/redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.tsx b/redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.tsx index 30eca9e222..8c59533f99 100644 --- a/redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.tsx +++ b/redisinsight/ui/src/components/bulk-actions-config/BulkActionsConfig.tsx @@ -16,7 +16,7 @@ import { keysSelector } from 'uiSrc/slices/browser/keys' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { isProcessingBulkAction } from 'uiSrc/pages/browser/components/bulk-actions/utils' import { BrowserStorageItem, BulkActionsServerEvent, BulkActionsStatus, BulkActionsType, SocketEvent } from 'uiSrc/constants' -import { addErrorNotification, addMessageNotification } from 'uiSrc/slices/app/notifications' +import { addErrorNotification } from 'uiSrc/slices/app/notifications' interface IProps { retryDelay?: number; @@ -51,7 +51,7 @@ const BulkActionsConfig = ({ retryDelay = 5000 } : IProps) => { }) // Catch connect error - socketRef.current?.on(SocketEvent.ConnectionError, (error) => {}) + socketRef.current?.on(SocketEvent.ConnectionError, () => {}) // Catch disconnect socketRef.current?.on(SocketEvent.Disconnect, () => { diff --git a/redisinsight/ui/src/components/charts/donut-chart/DonutChart.spec.tsx b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.spec.tsx new file mode 100644 index 0000000000..a574e157ef --- /dev/null +++ b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.spec.tsx @@ -0,0 +1,90 @@ +import React from 'react' +import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' + +import DonutChart, { ChartData } from './DonutChart' + +const mockData: ChartData[] = [ + { value: 1, name: 'A', color: [0, 0, 0] }, + { value: 5, name: 'B', color: [10, 10, 10] }, + { value: 10, name: 'C', color: [20, 20, 20] }, + { value: 2, name: 'D', color: [30, 30, 30] }, + { value: 30, name: 'E', color: [40, 40, 40] }, + { value: 15, name: 'F', color: [50, 50, 50] }, +] + +describe('DonutChart', () => { + it('should render with empty data', () => { + expect(render()).toBeTruthy() + }) + + it('should render with data', () => { + expect(render()).toBeTruthy() + }) + + it('should not render donut with empty data', () => { + const { container } = render() + expect(container).toBeEmptyDOMElement() + }) + + it('should not render donut with 0 values', () => { + const mockData: ChartData[] = [ + { value: 0, name: 'A', color: [0, 0, 0] }, + { value: 0, name: 'B', color: [10, 10, 10] }, + ] + const { container } = render() + expect(container).toBeEmptyDOMElement() + }) + + it('should render svg', () => { + render() + expect(screen.getByTestId('donut-test')).toBeInTheDocument() + }) + + it('should render arcs and labels', () => { + render() + mockData.forEach(({ value, name }) => { + expect(screen.getByTestId(`arc-${name}-${value}`)).toBeInTheDocument() + expect(screen.getByTestId(`label-${name}-${value}`)).toBeInTheDocument() + }) + }) + + it('should not render arcs and labels with 0 value', () => { + const mockData: ChartData[] = [ + { value: 0, name: 'A', color: [0, 0, 0] }, + { value: 10, name: 'B', color: [10, 10, 10] }, + ] + render() + expect(screen.queryByTestId('arc-A-0')).not.toBeInTheDocument() + expect(screen.queryByTestId('label-A-0')).not.toBeInTheDocument() + }) + + it('should do not render label value if value less than 5%', () => { + render() + expect(screen.getByTestId('label-A-1')).toHaveTextContent('') + }) + + it('should render label value if value more than 5%', () => { + render() + expect(screen.getByTestId('label-E-30')).toHaveTextContent('E: 30') + }) + + it('should call render tooltip and label methods', () => { + const renderLabel = jest.fn() + const renderTooltip = jest.fn() + render() + expect(renderLabel).toBeCalled() + + fireEvent.mouseEnter(screen.getByTestId('arc-A-1')) + expect(renderTooltip).toBeCalled() + }) + + it('should set tooltip as visible on hover and hidden on leave', () => { + render() + + fireEvent.mouseEnter(screen.getByTestId('arc-A-1')) + expect(screen.getByTestId('chart-value-tooltip')).toBeVisible() + + fireEvent.mouseLeave(screen.getByTestId('arc-A-1')) + expect(screen.getByTestId('chart-value-tooltip')).not.toBeVisible() + }) +}) diff --git a/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx new file mode 100644 index 0000000000..aed642d73a --- /dev/null +++ b/redisinsight/ui/src/components/charts/donut-chart/DonutChart.tsx @@ -0,0 +1,211 @@ +import cx from 'classnames' +import * as d3 from 'd3' +import { sumBy } from 'lodash' +import React, { useEffect, useRef, useState } from 'react' +import { flushSync } from 'react-dom' +import { Nullable, truncateNumberToRange } from 'uiSrc/utils' +import { rgb, RGBColor } from 'uiSrc/utils/colors' +import { getPercentage } from 'uiSrc/utils/numbers' + +import styles from './styles.module.scss' + +export interface ChartData { + value: number + name: string + color: RGBColor + meta?: { + [key: string]: any + } +} + +interface IProps { + name?: string + data: ChartData[] + width?: number + height?: number + title?: React.ReactElement | string + config?: { + percentToShowLabel?: number + arcWidth?: number + margin?: number + radius?: number + } + classNames?: { + chart?: string + arc?: string + arcLabel?: string + arcLabelValue?: string + tooltip?: string + } + renderLabel?: (data: ChartData) => string + renderTooltip?: (data: ChartData) => React.ReactElement | string + labelAs?: 'value' | 'percentage' +} + +const ANIMATION_DURATION_MS = 100 + +const DonutChart = (props: IProps) => { + const { + name = '', + data, + width = 380, + height = 300, + title, + config, + classNames, + labelAs = 'value', + renderLabel, + renderTooltip, + } = props + + const margin = config?.margin || 98 + const radius = config?.radius || (width / 2 - margin) + const arcWidth = config?.arcWidth || 8 + const percentToShowLabel = config?.percentToShowLabel || 5 + + const [hoveredData, setHoveredData] = useState>(null) + const svgRef = useRef(null) + const tooltipRef = useRef(null) + const sum = sumBy(data, 'value') + + const arc = d3.arc>() + .outerRadius(radius) + .innerRadius(radius - arcWidth) + + const arcHover = d3.arc>() + .outerRadius(radius + 4) + .innerRadius(radius - arcWidth) + + const onMouseEnterSlice = (e: MouseEvent, d: d3.PieArcDatum) => { + d3 + .select>(e.target as SVGPathElement) + .transition() + .duration(ANIMATION_DURATION_MS) + .attr('d', arcHover) + + if (!tooltipRef.current) { + return + } + + // calculate position after tooltip rendering (do update as synchronous operation) + if (e.type === 'mouseenter') { + flushSync(() => { setHoveredData(d.data) }) + } + + tooltipRef.current.style.top = `${e.pageY + 15}px` + tooltipRef.current.style.left = (window.innerWidth < (tooltipRef.current.scrollWidth + e.pageX + 20)) + ? `${e.pageX - tooltipRef.current.scrollWidth - 15}px` + : `${e.pageX + 15}px` + tooltipRef.current.style.visibility = 'visible' + } + + const onMouseLeaveSlice = (e: MouseEvent) => { + d3 + .select>(e.target as SVGPathElement) + .transition() + .duration(ANIMATION_DURATION_MS) + .attr('d', arc) + + if (tooltipRef.current) { + tooltipRef.current.style.visibility = 'hidden' + setHoveredData(null) + } + } + + const isShowLabel = (d: d3.PieArcDatum) => + d.endAngle - d.startAngle > (Math.PI * 2) / (100 / percentToShowLabel) + + const getLabelPosition = (d: d3.PieArcDatum) => { + const [x, y] = arc.centroid(d) + const h = Math.sqrt(x * x + y * y) + return `translate(${(x / h) * (radius + 16)}, ${((y + 4) / h) * (radius + 16)})` + } + + useEffect(() => { + const pie = d3.pie().value((d: ChartData) => d.value).sort(null) + const dataReady = pie(data.filter((d) => d.value !== 0)) + + d3 + .select(svgRef.current) + .select('g') + .remove() + + const svg = d3 + .select(svgRef.current) + .attr('width', width) + .attr('height', height) + .attr('data-testid', `donut-${name}`) + .attr('class', cx(classNames?.chart)) + .append('g') + .attr('transform', `translate(${width / 2},${height / 2})`) + + // add arcs + svg + .selectAll() + .data(dataReady) + .enter() + .append('path') + .attr('data-testid', (d) => `arc-${d.data.name}-${d.data.value}`) + .attr('d', arc) + .attr('fill', (d) => rgb(d.data.color)) + .attr('class', cx(styles.arc, classNames?.arc)) + .on('mouseenter mousemove', onMouseEnterSlice) + .on('mouseleave', onMouseLeaveSlice) + + // add labels + svg + .selectAll() + .data(dataReady) + .enter() + .append('text') + .attr('class', cx(styles.chartLabel, classNames?.arcLabel)) + .attr('transform', getLabelPosition) + .text((d) => (isShowLabel(d) ? d.data.name : '')) + .attr('data-testid', (d) => `label-${d.data.name}-${d.data.value}`) + .style('text-anchor', (d) => ((d.endAngle + d.startAngle) / 2 > Math.PI ? 'end' : 'start')) + .on('mouseenter mousemove', onMouseEnterSlice) + .on('mouseleave', onMouseLeaveSlice) + .append('tspan') + .text((d) => { + if (!isShowLabel(d)) { + return '' + } + + if (renderLabel) { + return renderLabel(d.data) + } + + const separator = ': ' + if (labelAs === 'percentage') { + return `${separator}${getPercentage(d.value, sum)}%` + } + + return `${separator}${truncateNumberToRange(d.value)}` + }) + .attr('class', cx(styles.chartLabelValue, classNames?.arcLabelValue)) + }, [data]) + + if (!data.length || sum === 0) { + return null + } + + return ( +
+ +
+ {(renderTooltip && hoveredData) ? renderTooltip(hoveredData) : (hoveredData?.value || '')} +
+ {title && ( +
+ {title} +
+ )} +
+ ) +} + +export default DonutChart diff --git a/redisinsight/ui/src/components/charts/donut-chart/index.ts b/redisinsight/ui/src/components/charts/donut-chart/index.ts new file mode 100644 index 0000000000..bdfb67bbc5 --- /dev/null +++ b/redisinsight/ui/src/components/charts/donut-chart/index.ts @@ -0,0 +1,3 @@ +import DonutChart from './DonutChart' + +export default DonutChart diff --git a/redisinsight/ui/src/components/charts/donut-chart/styles.module.scss b/redisinsight/ui/src/components/charts/donut-chart/styles.module.scss new file mode 100644 index 0000000000..ed34a35b87 --- /dev/null +++ b/redisinsight/ui/src/components/charts/donut-chart/styles.module.scss @@ -0,0 +1,36 @@ +.wrapper { + position: relative; +} + +.innerTextContainer { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.tooltip { + position: fixed; + background: var(--euiTooltipBackgroundColor); + color: var(--htmlColor); + padding: 10px; + visibility: hidden; + border-radius: 4px; + z-index: 100; +} + +.chartLabel { + fill: var(--euiTextSubduedColor); + font-size: 12px; + font-weight: bold; + + .chartLabelValue { + font-weight: normal; + } +} + +.arc { + stroke: var(--euiColorLightestShade); + stroke-width: 2px; + cursor: pointer; +} diff --git a/redisinsight/ui/src/components/charts/index.ts b/redisinsight/ui/src/components/charts/index.ts new file mode 100644 index 0000000000..0dc80344b0 --- /dev/null +++ b/redisinsight/ui/src/components/charts/index.ts @@ -0,0 +1,5 @@ +import DonutChart from './donut-chart' + +export { + DonutChart +} diff --git a/redisinsight/ui/src/components/cli/CliWrapper.spec.tsx b/redisinsight/ui/src/components/cli/CliWrapper.spec.tsx index e11a34f5d6..e10ce243f7 100644 --- a/redisinsight/ui/src/components/cli/CliWrapper.spec.tsx +++ b/redisinsight/ui/src/components/cli/CliWrapper.spec.tsx @@ -1,16 +1,15 @@ import { cloneDeep } from 'lodash' import React from 'react' -import { processCliClient, setCliEnteringCommand } from 'uiSrc/slices/cli/cli-settings' -import { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils' +import { InitOutputText } from 'uiSrc/constants/cliOutput' +import { concatToOutput } from 'uiSrc/slices/cli/cli-output' +import { setCliEnteringCommand } from 'uiSrc/slices/cli/cli-settings' +import { cleanup, clearStoreActions, mockedStore, render } from 'uiSrc/utils/test-utils' import CliWrapper from './CliWrapper' -jest.mock('uiSrc/slices/cli/cli-output', () => ({ - ...jest.requireActual('uiSrc/slices/cli/cli-output'), - concatToOutput: () => jest.fn(), -})) - const redisCommandsPath = 'uiSrc/slices/app/redis-commands' +let mathRandom: jest.SpyInstance +const random = 0.91911 let store: typeof mockedStore beforeEach(() => { cleanup() @@ -32,6 +31,14 @@ jest.mock(redisCommandsPath, () => { }) describe('CliWrapper', () => { + beforeAll(() => { + mathRandom = jest.spyOn(Math, 'random').mockImplementation(() => random) + }) + + afterAll(() => { + mathRandom.mockRestore() + }) + it('should render', () => { expect(render()).toBeTruthy() }) @@ -40,7 +47,12 @@ describe('CliWrapper', () => { unmount() - const expectedActions = [processCliClient(), setCliEnteringCommand()] - expect(store.getActions().slice(-2)).toEqual(expectedActions) + const handleWorkbenchClick = () => {} + + const expectedActions = [ + concatToOutput(InitOutputText('', 0, 0, true, handleWorkbenchClick)), + setCliEnteringCommand(), + ] + expect(clearStoreActions(store.getActions().slice(-2))).toEqual(clearStoreActions(expectedActions)) }) }) diff --git a/redisinsight/ui/src/components/cli/components/cli-body/CliBodyWrapper.tsx b/redisinsight/ui/src/components/cli/components/cli-body/CliBodyWrapper.tsx index 2273f0e8cc..fa6691f45a 100644 --- a/redisinsight/ui/src/components/cli/components/cli-body/CliBodyWrapper.tsx +++ b/redisinsight/ui/src/components/cli/components/cli-body/CliBodyWrapper.tsx @@ -53,7 +53,7 @@ const CliBodyWrapper = () => { const { db: currentDbIndex } = useSelector(outputSelector) useEffect(() => { - !cliClientUuid && dispatch(createCliClientAction(handleWorkbenchClick)) + !cliClientUuid && dispatch(createCliClientAction(instanceId, handleWorkbenchClick)) }, []) useEffect(() => { diff --git a/redisinsight/ui/src/components/database-list-modules/DatabaseListModules.tsx b/redisinsight/ui/src/components/database-list-modules/DatabaseListModules.tsx index 0c7a7029d7..1a5495fbc6 100644 --- a/redisinsight/ui/src/components/database-list-modules/DatabaseListModules.tsx +++ b/redisinsight/ui/src/components/database-list-modules/DatabaseListModules.tsx @@ -158,9 +158,8 @@ const DatabaseListModules = React.memo((props: Props) => { )) - const Module = (moduleName: string = '', abbreviation: string = '', icon: string, content: string = '') => { - return ( - + const Module = (moduleName: string = '', abbreviation: string = '', icon: string, content: string = '') => ( + {icon ? ( { )} - ) - } + ) const Modules = () => ( newModules.map(({ icon, content, abbreviation, moduleName }, i) => ( diff --git a/redisinsight/ui/src/components/database-overview/components/icons.ts b/redisinsight/ui/src/components/database-overview/components/icons.ts index 2f9ca14513..f37fd56eb0 100644 --- a/redisinsight/ui/src/components/database-overview/components/icons.ts +++ b/redisinsight/ui/src/components/database-overview/components/icons.ts @@ -1,24 +1,37 @@ import KeyDarkIcon from 'uiSrc/assets/img/overview/key_dark.svg' import KeyTipIcon from 'uiSrc/assets/img/overview/key_tip.svg' import KeyLightIcon from 'uiSrc/assets/img/overview/key_light.svg' +import { ReactComponent as KeyIconSvg } from 'uiSrc/assets/img/overview/key.svg' + import MemoryDarkIcon from 'uiSrc/assets/img/overview/memory_dark.svg' import MemoryLightIcon from 'uiSrc/assets/img/overview/memory_light.svg' import MemoryTipIcon from 'uiSrc/assets/img/overview/memory_tip.svg' +import { ReactComponent as MemoryIconSvg } from 'uiSrc/assets/img/overview/memory.svg' + import MeasureLightIcon from 'uiSrc/assets/img/overview/measure_light.svg' import MeasureDarkIcon from 'uiSrc/assets/img/overview/measure_dark.svg' import MeasureTipIcon from 'uiSrc/assets/img/overview/measure_tip.svg' +import { ReactComponent as MeasureIconSvg } from 'uiSrc/assets/img/overview/measure.svg' + import TimeLightIcon from 'uiSrc/assets/img/overview/time_light.svg' import TimeDarkIcon from 'uiSrc/assets/img/overview/time_dark.svg' import TimeTipIcon from 'uiSrc/assets/img/overview/time_tip.svg' +import { ReactComponent as TimeIconSvg } from 'uiSrc/assets/img/overview/time.svg' + import UserDarkIcon from 'uiSrc/assets/img/overview/user_dark.svg' import UserLightIcon from 'uiSrc/assets/img/overview/user_light.svg' import UserTipIcon from 'uiSrc/assets/img/overview/user_tip.svg' +import { ReactComponent as UserIconSvg } from 'uiSrc/assets/img/overview/user.svg' + import InputTipIcon from 'uiSrc/assets/img/overview/input_tip.svg' import InputLightIcon from 'uiSrc/assets/img/overview/input_light.svg' import InputDarkIcon from 'uiSrc/assets/img/overview/input_dark.svg' +import { ReactComponent as InputIconSvg } from 'uiSrc/assets/img/overview/input.svg' + import OutputTipIcon from 'uiSrc/assets/img/overview/output_tip.svg' import OutputLightIcon from 'uiSrc/assets/img/overview/output_light.svg' import OutputDarkIcon from 'uiSrc/assets/img/overview/output_dark.svg' +import { ReactComponent as OutputIconSvg } from 'uiSrc/assets/img/overview/output.svg' export { KeyDarkIcon, @@ -42,4 +55,11 @@ export { OutputTipIcon, OutputLightIcon, OutputDarkIcon, + KeyIconSvg, + MemoryIconSvg, + MeasureIconSvg, + TimeIconSvg, + UserIconSvg, + InputIconSvg, + OutputIconSvg, } diff --git a/redisinsight/ui/src/components/index.ts b/redisinsight/ui/src/components/index.ts index e5771c5a31..3cfce1e0af 100644 --- a/redisinsight/ui/src/components/index.ts +++ b/redisinsight/ui/src/components/index.ts @@ -10,7 +10,7 @@ import InputFieldSentinel from './input-field-sentinel/InputFieldSentinel' import PageBreadcrumbs from './page-breadcrumbs/PageBreadcrumbs' import ContentEditable from './ContentEditable' import Config from './config' -import AdvancedSettings from './advanced-settings/AdvancedSettings' +import SettingItem from './settings-item/SettingItem' import { ConsentsSettings, ConsentsSettingsPopup, ConsentsPrivacy, ConsentsNotifications } from './consents-settings' import KeyboardShortcut from './keyboard-shortcut/KeyboardShortcut' import ShortcutsFlyout from './shortcuts-flyout/ShortcutsFlyout' @@ -38,7 +38,7 @@ export { ConsentsSettingsPopup, ConsentsPrivacy, ConsentsNotifications, - AdvancedSettings, + SettingItem, KeyboardShortcut, MonitorConfig, PubSubConfig, diff --git a/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.spec.tsx b/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.spec.tsx index a1e30bc1ac..3a07826178 100644 --- a/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.spec.tsx +++ b/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.spec.tsx @@ -39,4 +39,66 @@ describe('InlineItemEditor', () => { render() expect(screen.getByTestId(INLINE_ITEM_EDITOR)).toHaveFocus() }) + + describe('approveByValidation', () => { + it('should not render popover after click on Apply btn if approveByValidation return "true" in the props and onApply should be called', () => { + const approveByValidationMock = jest.fn().mockReturnValue(true) + const onApplyMock = jest.fn().mockReturnValue(false) + const { queryByTestId } = render( + + ) + + fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value: 'val123' } }) + + fireEvent.click(screen.getByTestId(/apply-btn/)) + expect(queryByTestId('approve-popover')).not.toBeInTheDocument() + expect(onApplyMock).toBeCalled() + }) + + it('should render popover after click on Apply btn if approveByValidation return "false" in the props and onApply should not be called ', () => { + const approveByValidationMock = jest.fn().mockReturnValue(false) + const onApplyMock = jest.fn().mockReturnValue(false) + const { queryByTestId } = render( + + ) + + fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value: 'val123' } }) + + fireEvent.click(screen.getByTestId(/apply-btn/)) + expect(queryByTestId('approve-popover')).toBeInTheDocument() + expect(onApplyMock).not.toBeCalled() + }) + + it('should render popover after click on Apply btn if approveByValidation return "false" in the props and onApply should be called after click on Save btn', () => { + const approveByValidationMock = jest.fn().mockReturnValue(false) + const onApplyMock = jest.fn().mockReturnValue(false) + const { queryByTestId } = render( + + ) + + fireEvent.change(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value: 'val123' } }) + + fireEvent.click(screen.getByTestId(/apply-btn/)) + expect(queryByTestId('approve-popover')).toBeInTheDocument() + expect(onApplyMock).not.toBeCalled() + + fireEvent.click(screen.getByTestId(/save-btn/)) + expect(onApplyMock).toBeCalled() + }) + }) }) diff --git a/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.tsx b/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.tsx index 11fffa33b0..34bdae8b4b 100644 --- a/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.tsx +++ b/redisinsight/ui/src/components/inline-item-editor/InlineItemEditor.tsx @@ -16,6 +16,9 @@ import { EuiFocusTrap, EuiWindowEvent, EuiToolTip, + EuiPopover, + EuiButton, + EuiText, } from '@elastic/eui' import { IconSize } from '@elastic/eui/src/components/icon/icon' import styles from './styles.module.scss' @@ -25,7 +28,7 @@ type Design = 'default' | 'separate' export interface Props { onDecline: (event?: React.MouseEvent) => void - onApply: (value: string, event: React.MouseEvent) => void + onApply: (value: string, event: React.MouseEvent) => void onChange?: (value: string) => void fieldName?: string initialValue?: string @@ -47,6 +50,10 @@ export interface Props { autoComplete?: string controlsClassName?: string disabledTooltipText?: { title: string, text: string } + preventOutsideClick?: boolean + disableFocusTrap?: boolean + approveByValidation?: (value: string) => boolean + approveText?: { title: string, text: string } } const InlineItemEditor = (props: Props) => { @@ -74,10 +81,15 @@ const InlineItemEditor = (props: Props) => { autoComplete = 'off', controlsClassName, disabledTooltipText, + preventOutsideClick = false, + disableFocusTrap = false, + approveByValidation, + approveText, } = props const containerEl: Ref = useRef(null) const [value, setValue] = useState(initialValue) const [isError, setIsError] = useState(false) + const [isShowApprovePopover, setIsShowApprovePopover] = useState(false) const inputRef: Ref = useRef(null) @@ -110,6 +122,7 @@ const InlineItemEditor = (props: Props) => { } const handleClickOutside = (event: any) => { + if (preventOutsideClick) return if (!containerEl?.current?.contains(event.target)) { if (!isLoading) { onDecline(event) @@ -127,15 +140,24 @@ const InlineItemEditor = (props: Props) => { } } - const handleFormSubmit = (event: React.MouseEvent): void => { + const handleApplyClick = (event: React.MouseEvent) => { + if (approveByValidation && !approveByValidation?.(value)) { + setIsShowApprovePopover(true) + } else { + handleFormSubmit(event) + } + } + + const handleFormSubmit = (event: React.MouseEvent): void => { event.preventDefault() + event.stopPropagation() onApply(value, event) } const isDisabledApply = (): boolean => !!(isLoading || isError || isDisabled || (disableEmpty && !value.length)) - const ApplyBtn = () => ( + const ApplyBtn = ( { iconSize={iconSize ?? 'l'} iconType="check" color="primary" - type="submit" aria-label="Apply" className={cx(styles.btn, styles.applyBtn)} isDisabled={isDisabledApply()} + onClick={handleApplyClick} data-testid="apply-btn" /> @@ -163,7 +185,7 @@ const InlineItemEditor = (props: Props) => {
- + { isDisabled={isLoading} data-testid="cancel-btn" /> - + {!approveByValidation && ApplyBtn} + {approveByValidation && ( + setIsShowApprovePopover(false)} + anchorClassName={styles.popoverAnchor} + panelClassName={cx(styles.popoverPanel)} + className={styles.popoverWrapper} + button={ApplyBtn} + > +
+ + {!!approveText?.title && ( +

+ {approveText?.title} +

+ )} + + {approveText?.text} + +
+
+ + Save + +
+
+ +
+ )}
diff --git a/redisinsight/ui/src/components/inline-item-editor/styles.module.scss b/redisinsight/ui/src/components/inline-item-editor/styles.module.scss index 3563a8cf81..0b5c389f59 100644 --- a/redisinsight/ui/src/components/inline-item-editor/styles.module.scss +++ b/redisinsight/ui/src/components/inline-item-editor/styles.module.scss @@ -23,13 +23,15 @@ z-index: 1; .tooltip, - :global(.euiButtonIcon) { + .declineBtn, + .popoverWrapper { width: 50% !important; height: 100% !important; } } -.tooltip :global(.euiButtonIcon) { +.applyBtn { + height: 100% !important; width: 100% !important; } @@ -76,14 +78,19 @@ width: 60px; z-index: 4; - .btn { + .btn, + .popoverWrapper { margin: 6px 3px; height: 24px !important; width: 24px !important; + } + + .btn:hover { + background-color: var(--hoverInListColorDarken) !important; + } - &:hover { - background-color: var(--hoverInListColorDarken) !important; - } + .applyBtn { + margin-top: 0; } svg { @@ -109,3 +116,47 @@ margin-right: 80px; word-break: break-all; } + +.popoverAnchor, +.popoverWrapper .tooltip { + width: 100% !important; + height: 100% !important; +} + +.popoverPanel:global(.euiPanel--paddingMedium) { + width: 296px !important; + padding: 24px 30px !important; + border: 1px solid var(--euiColorPrimary) !important; + background-color: var(--browserTableRowEven) !important; + + :global(.euiPopover__panelArrow:after) { + border-left-color: var(--browserTableRowEven) !important; + } + :global(.euiPopover__panelArrow:before) { + border-left-color: var(--euiColorPrimary) !important; + } +} + +.popover { + word-wrap: break-word; + + h4 b { + font-size: 14px !important; + color: var(--htmlColor) !important; + } +} +.approveText { + font-size: 13px !important; + letter-spacing: -0.13px; +} + +.popoverFooter { + display: flex; + justify-content: flex-end; + margin-top: 12px; + + .saveBtn { + height: 36px !important; + width: 86px !important; + } +} diff --git a/redisinsight/ui/src/components/json-viewer/styles.module.scss b/redisinsight/ui/src/components/json-viewer/styles.module.scss index 4aab799892..26d78f2ddc 100644 --- a/redisinsight/ui/src/components/json-viewer/styles.module.scss +++ b/redisinsight/ui/src/components/json-viewer/styles.module.scss @@ -2,7 +2,7 @@ :global { .__json { &-pretty__, &-pretty-error__ { - font: normal normal normal 13px/18px Graphik; + font: normal normal normal 13px/18px Graphik, sans-serif; letter-spacing: -0.13px; padding: 0; background: transparent; diff --git a/redisinsight/ui/src/components/keyboard-shortcut/KeyboardShortcut.tsx b/redisinsight/ui/src/components/keyboard-shortcut/KeyboardShortcut.tsx index f776cd722c..29ac9936d5 100644 --- a/redisinsight/ui/src/components/keyboard-shortcut/KeyboardShortcut.tsx +++ b/redisinsight/ui/src/components/keyboard-shortcut/KeyboardShortcut.tsx @@ -6,23 +6,27 @@ import { EuiBadge, EuiText } from '@elastic/eui' import styles from './styles.module.scss' export interface Props { - items: (string | JSX.Element)[]; - separator?: string; - transparent?: boolean; + items: (string | JSX.Element)[] + separator?: string + transparent?: boolean + badgeTextClassName?: string } -const KeyboardShortcut = ({ items = [], separator = '', transparent = false }: Props) => ( -
- { +const KeyboardShortcut = (props: Props) => { + const { items = [], separator = '', transparent = false, badgeTextClassName = '' } = props + return ( +
+ { items.map((item: string | JSX.Element, index: number) => (
- { (index !== 0) &&
{separator}
} + {(index !== 0) &&
{separator}
} - {item} + {item}
)) } -
-) +
+ ) +} export default KeyboardShortcut diff --git a/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts b/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts index 2bb821c78e..dd4420bff9 100644 --- a/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts @@ -10,8 +10,9 @@ import { RedisClusterDatabasesPage, } from 'uiSrc/pages' import WorkbenchPage from 'uiSrc/pages/workbench' -import SlowLogPage from 'uiSrc/pages/slowLog' import PubSubPage from 'uiSrc/pages/pubSub' +import AnalyticsPage from 'uiSrc/pages/analytics' +import { ANALYTICS_ROUTES } from './sub-routes' import COMMON_ROUTES from './commonRoutes' @@ -26,16 +27,16 @@ const INSTANCE_ROUTES: IRoute[] = [ path: Pages.workbench(':instanceId'), component: WorkbenchPage, }, - { - pageName: PageNames.slowLog, - path: Pages.slowLog(':instanceId'), - component: SlowLogPage, - }, { pageName: PageNames.pubSub, path: Pages.pubSub(':instanceId'), component: PubSubPage, }, + { + path: Pages.analytics(':instanceId'), + component: AnalyticsPage, + routes: ANALYTICS_ROUTES, + }, ] const ROUTES: IRoute[] = [ diff --git a/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts b/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts index d876d5529a..6897dd80b2 100644 --- a/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts @@ -6,8 +6,25 @@ import WorkbenchPage from 'uiSrc/pages/workbench' import SlowLogPage from 'uiSrc/pages/slowLog' import PubSubPage from 'uiSrc/pages/pubSub' import EditConnection from 'uiSrc/pages/redisStack/components/edit-connection' +import ClusterDetailsPage from 'uiSrc/pages/clusterDetails' +import AnalyticsPage from 'uiSrc/pages/analytics' import COMMON_ROUTES from './commonRoutes' +const ANALYTICS_ROUTES: IRoute[] = [ + { + pageName: PageNames.slowLog, + protected: true, + path: Pages.slowLog(':instanceId'), + component: SlowLogPage, + }, + { + pageName: PageNames.clusterDetails, + protected: true, + path: Pages.clusterDetails(':instanceId'), + component: ClusterDetailsPage, + }, +] + const INSTANCE_ROUTES: IRoute[] = [ { pageName: PageNames.browser, @@ -21,18 +38,18 @@ const INSTANCE_ROUTES: IRoute[] = [ path: Pages.workbench(':instanceId'), component: WorkbenchPage, }, - { - pageName: PageNames.slowLog, - protected: true, - path: Pages.slowLog(':instanceId'), - component: SlowLogPage, - }, { pageName: PageNames.pubSub, protected: true, path: Pages.pubSub(':instanceId'), component: PubSubPage, }, + { + path: Pages.analytics(':instanceId'), + protected: true, + component: AnalyticsPage, + routes: ANALYTICS_ROUTES, + }, ] const ROUTES: IRoute[] = [ diff --git a/redisinsight/ui/src/components/main-router/constants/sub-routes/analyticsRoutes.ts b/redisinsight/ui/src/components/main-router/constants/sub-routes/analyticsRoutes.ts new file mode 100644 index 0000000000..6f214d6fa6 --- /dev/null +++ b/redisinsight/ui/src/components/main-router/constants/sub-routes/analyticsRoutes.ts @@ -0,0 +1,16 @@ +import { IRoute, PageNames, Pages } from 'uiSrc/constants' +import ClusterDetailsPage from 'uiSrc/pages/clusterDetails' +import SlowLogPage from 'uiSrc/pages/slowLog' + +export const ANALYTICS_ROUTES: IRoute[] = [ + { + pageName: PageNames.slowLog, + path: Pages.slowLog(':instanceId'), + component: SlowLogPage, + }, + { + pageName: PageNames.clusterDetails, + path: Pages.clusterDetails(':instanceId'), + component: ClusterDetailsPage, + }, +] diff --git a/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts b/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts new file mode 100644 index 0000000000..7cfe9e8d2b --- /dev/null +++ b/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts @@ -0,0 +1,5 @@ +import { ANALYTICS_ROUTES } from './analyticsRoutes' + +export { + ANALYTICS_ROUTES +} diff --git a/redisinsight/ui/src/components/monitor/Monitor/styles.module.scss b/redisinsight/ui/src/components/monitor/Monitor/styles.module.scss index 12e0b6b958..421d07b624 100644 --- a/redisinsight/ui/src/components/monitor/Monitor/styles.module.scss +++ b/redisinsight/ui/src/components/monitor/Monitor/styles.module.scss @@ -88,7 +88,7 @@ } .saveLogContainer { - font: normal normal normal 13px/18px Graphik; + font: normal normal normal 13px/18px Graphik, sans-serif; letter-spacing: -0.13px; margin-bottom: 18px; :global(.euiSwitch__label) { diff --git a/redisinsight/ui/src/components/monitor/MonitorLog/MonitorLog.tsx b/redisinsight/ui/src/components/monitor/MonitorLog/MonitorLog.tsx index 66a10d3f8e..7c571242bc 100644 --- a/redisinsight/ui/src/components/monitor/MonitorLog/MonitorLog.tsx +++ b/redisinsight/ui/src/components/monitor/MonitorLog/MonitorLog.tsx @@ -1,4 +1,4 @@ -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui' +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui' import { format, formatDuration, intervalToDuration } from 'date-fns' import React from 'react' import { useDispatch, useSelector } from 'react-redux' diff --git a/redisinsight/ui/src/components/monitor/MonitorOutputList/MonitorOutputList.tsx b/redisinsight/ui/src/components/monitor/MonitorOutputList/MonitorOutputList.tsx index 39d33687ad..7f7bbe8d04 100644 --- a/redisinsight/ui/src/components/monitor/MonitorOutputList/MonitorOutputList.tsx +++ b/redisinsight/ui/src/components/monitor/MonitorOutputList/MonitorOutputList.tsx @@ -3,8 +3,7 @@ import cx from 'classnames' import { EuiTextColor } from '@elastic/eui' import { CellMeasurer, List, CellMeasurerCache, ListRowProps } from 'react-virtualized' -import { getFormatTime } from 'uiSrc/utils' -import { DEFAULT_ERROR_TEXT } from 'uiSrc/components/notifications' +import { DEFAULT_ERROR_MESSAGE, getFormatTime } from 'uiSrc/utils' import styles from 'uiSrc/components/monitor/Monitor/styles.module.scss' import 'react-virtualized/styles.css' @@ -80,7 +79,7 @@ const MonitorOutputList = (props: Props) => { )} {isError && ( - {message ?? DEFAULT_ERROR_TEXT} + {message ?? DEFAULT_ERROR_MESSAGE} )} )} diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx index 734f6674d2..9a05e6824f 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx @@ -17,6 +17,7 @@ import { EuiTitle, EuiToolTip } from '@elastic/eui' +import { ANALYTICS_ROUTES } from 'uiSrc/components/main-router/constants/sub-routes' import { PageNames, Pages } from 'uiSrc/constants' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' @@ -41,15 +42,15 @@ import PubSubSVG from 'uiSrc/assets/img/sidebar/pubsub.svg' import PubSubActiveSVG from 'uiSrc/assets/img/sidebar/pubsub_active.svg' import GithubSVG from 'uiSrc/assets/img/sidebar/github.svg' import Divider from 'uiSrc/components/divider/Divider' - import { BuildType } from 'uiSrc/constants/env' +import { ConnectionType } from 'uiSrc/slices/interfaces' + import NotificationMenu from './components/notifications-center' import styles from './styles.module.scss' const workbenchPath = `/${PageNames.workbench}` const browserPath = `/${PageNames.browser}` -const slowLogPath = `/${PageNames.slowLog}` const pubSubPath = `/${PageNames.pubSub}` interface INavigations { @@ -71,7 +72,7 @@ const NavigationMenu = () => { const [activePage, setActivePage] = useState(Pages.home) const [isHelpMenuActive, setIsHelpMenuActive] = useState(false) - const { id: connectedInstanceId = '' } = useSelector(connectedInstanceSelector) + const { id: connectedInstanceId = '', connectionType } = useSelector(connectedInstanceSelector) const { isReleaseNotesViewed } = useSelector(appElectronInfoSelector) const { server } = useSelector(appInfoSelector) @@ -86,6 +87,10 @@ const NavigationMenu = () => { dispatch(setShortcutsFlyoutState(true)) } + const isAnalyticsPath = (activePage: string) => !!ANALYTICS_ROUTES.find( + ({ path }) => (`/${last(path.split('/'))}` === activePage) + ) + const privateRoutes: INavigations[] = [ { tooltipText: 'Browser', @@ -116,12 +121,14 @@ const NavigationMenu = () => { }, }, { - tooltipText: 'Slow Log', - ariaLabel: 'SlowLog page button', - onClick: () => handleGoPage(Pages.slowLog(connectedInstanceId)), - dataTestId: 'slowlog-page-btn', + tooltipText: 'Analysis Tools', + ariaLabel: 'Analysis Tools', + onClick: () => handleGoPage(connectionType === ConnectionType.Cluster + ? Pages.clusterDetails(connectedInstanceId) + : Pages.slowLog(connectedInstanceId)), + dataTestId: 'analytics-page-btn', connectedInstanceId, - isActivePage: activePage === slowLogPath, + isActivePage: isAnalyticsPath(activePage), getClassName() { return cx(styles.navigationButton, { [styles.active]: this.isActivePage }) }, diff --git a/redisinsight/ui/src/components/navigation-menu/components/notifications-center/Notification/Notification.tsx b/redisinsight/ui/src/components/navigation-menu/components/notifications-center/Notification/Notification.tsx index 83effa50f1..ef97c0fd99 100644 --- a/redisinsight/ui/src/components/navigation-menu/components/notifications-center/Notification/Notification.tsx +++ b/redisinsight/ui/src/components/navigation-menu/components/notifications-center/Notification/Notification.tsx @@ -44,6 +44,7 @@ const Notification = (props: Props) => { {truncateText(notification.category, 32)} diff --git a/redisinsight/ui/src/components/navigation-menu/components/notifications-center/styles.module.scss b/redisinsight/ui/src/components/navigation-menu/components/notifications-center/styles.module.scss index 4c6cb2275e..e95ce49e6d 100644 --- a/redisinsight/ui/src/components/navigation-menu/components/notifications-center/styles.module.scss +++ b/redisinsight/ui/src/components/navigation-menu/components/notifications-center/styles.module.scss @@ -167,7 +167,7 @@ display: block; margin-right: 30px; margin-bottom: 12px; - font: normal normal 500 18px/24px Graphik; + font: normal normal 500 18px/24px Graphik, sans-serif; } .notificationDate { diff --git a/redisinsight/ui/src/components/notifications/Notifications.tsx b/redisinsight/ui/src/components/notifications/Notifications.tsx index c854afbf89..1aa5a9f776 100644 --- a/redisinsight/ui/src/components/notifications/Notifications.tsx +++ b/redisinsight/ui/src/components/notifications/Notifications.tsx @@ -17,13 +17,12 @@ import { import { setReleaseNotesViewed } from 'uiSrc/slices/app/info' import { IError, IMessage } from 'uiSrc/slices/interfaces' import { ApiEncryptionErrors } from 'uiSrc/constants/apiErrors' +import { DEFAULT_ERROR_MESSAGE } from 'uiSrc/utils' import errorMessages from './error-messages' import styles from './styles.module.scss' -export const DEFAULT_ERROR_TEXT = 'Something went wrong.' - const Notifications = () => { const messagesData = useSelector(messagesSelector) const errorsData = useSelector(errorsSelector) @@ -82,7 +81,7 @@ const Notifications = () => { }) const getErrorsToasts = (errors: IError[]) => - errors.map(({ id = '', message = DEFAULT_ERROR_TEXT, instanceId = '', name }) => { + errors.map(({ id = '', message = DEFAULT_ERROR_MESSAGE, instanceId = '', name }) => { if (ApiEncryptionErrors.includes(name)) { return errorMessages.ENCRYPTION(id, () => removeToast({ id }), instanceId) } diff --git a/redisinsight/ui/src/components/notifications/success-messages.tsx b/redisinsight/ui/src/components/notifications/success-messages.tsx index 60df2028e5..5d79a27013 100644 --- a/redisinsight/ui/src/components/notifications/success-messages.tsx +++ b/redisinsight/ui/src/components/notifications/success-messages.tsx @@ -1,6 +1,6 @@ import React from 'react' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' -import { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { bufferToString, formatNameShort, Maybe } from 'uiSrc/utils' import styles from './styles.module.scss' @@ -71,7 +71,7 @@ export default { ), }), - REMOVED_KEY_VALUE: (keyName: RedisResponseBuffer, keyValue: RedisString, valueType: string) => ({ + REMOVED_KEY_VALUE: (keyName: RedisResponseBuffer, keyValue: RedisResponseBuffer, valueType: string) => ({ title: ( <> {valueType} diff --git a/redisinsight/ui/src/components/popover-item-editor/PopoverItemEditor.spec.tsx b/redisinsight/ui/src/components/popover-item-editor/PopoverItemEditor.spec.tsx index 6d3cb2cdef..671f7a5514 100644 --- a/redisinsight/ui/src/components/popover-item-editor/PopoverItemEditor.spec.tsx +++ b/redisinsight/ui/src/components/popover-item-editor/PopoverItemEditor.spec.tsx @@ -1,6 +1,6 @@ import React from 'react' import { instance, mock } from 'ts-mockito' -import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' +import { render } from 'uiSrc/utils/test-utils' import PopoverItemEditor, { Props } from './PopoverItemEditor' const mockedProps = mock() diff --git a/redisinsight/ui/src/components/pub-sub-config/PubSubConfig.tsx b/redisinsight/ui/src/components/pub-sub-config/PubSubConfig.tsx index d15bc0f8d1..cf007f73aa 100644 --- a/redisinsight/ui/src/components/pub-sub-config/PubSubConfig.tsx +++ b/redisinsight/ui/src/components/pub-sub-config/PubSubConfig.tsx @@ -47,10 +47,10 @@ const PubSubConfig = ({ retryDelay = 5000 } : IProps) => { }) // Catch connect error - socketRef.current?.on(SocketEvent.ConnectionError, (error) => {}) + socketRef.current?.on(SocketEvent.ConnectionError, () => {}) // Catch exceptions - socketRef.current?.on(PubSubEvent.Exception, (error) => { + socketRef.current?.on(PubSubEvent.Exception, () => { handleDisconnect() }) diff --git a/redisinsight/ui/src/components/query-card/QueryCard.spec.tsx b/redisinsight/ui/src/components/query-card/QueryCard.spec.tsx index cdb2c2832f..06c547a6fa 100644 --- a/redisinsight/ui/src/components/query-card/QueryCard.spec.tsx +++ b/redisinsight/ui/src/components/query-card/QueryCard.spec.tsx @@ -2,11 +2,17 @@ import { cloneDeep } from 'lodash' import React from 'react' import { instance, mock } from 'ts-mockito' import { toggleOpenWBResult } from 'uiSrc/slices/workbench/wb-results' +import { ResultsMode } from 'uiSrc/slices/interfaces/workbench' import { cleanup, clearStoreActions, fireEvent, mockedStore, render } from 'uiSrc/utils/test-utils' -import QueryCard, { Props } from './QueryCard' +import QueryCard, { Props, getSummaryText } from './QueryCard' const mockedProps = mock() +const mockResult = [{ + response: 'response', + status: 'success' +}] + let store: typeof mockedStore beforeEach(() => { cleanup() @@ -46,11 +52,6 @@ describe('QueryCard', () => { it('Cli result should in the document when "isOpen = true"', () => { const cliResultTestId = 'query-cli-result' - const mockResult = [{ - response: 'response', - status: 'success' - }] - const { queryByTestId } = render( { expect(cliResultEl).toBeInTheDocument() }) - it('Cli result should not in the document when "isOpen = true"', () => { + it('Cli result should not in the document when "isOpen = false"', () => { const cliResultTestId = 'query-cli-result' - const mockResult = [{ - response: 'response', - status: 'success' - }] + const { queryByTestId } = render() + + const cliResultEl = queryByTestId(cliResultTestId) + + expect(cliResultEl).not.toBeInTheDocument() + }) + + it('Should be in the document when resultsMode === ResultsMode.GroupMode', () => { + const cliResultTestId = 'query-cli-result' const { queryByTestId } = render() const cliResultEl = queryByTestId(cliResultTestId) @@ -85,11 +96,6 @@ describe('QueryCard', () => { const cardHeaderTestId = 'query-card-open' const mockId = '123' - const mockResult = [{ - response: 'response', - status: 'success' - }] - const { queryByTestId } = render( { clearStoreActions(expectedActions) ) }) + + it('Should return correct summary string', () => { + const summary = { total: 2, success: 1, fail: 1 } + const summaryText = '2 Command(s) - 1 success, 1 error(s)' + + const summaryString = getSummaryText(summary) + + expect(summaryString).toEqual(summaryText) + }) }) diff --git a/redisinsight/ui/src/components/query-card/QueryCard.tsx b/redisinsight/ui/src/components/query-card/QueryCard.tsx index df1f815f87..aa320689c5 100644 --- a/redisinsight/ui/src/components/query-card/QueryCard.tsx +++ b/redisinsight/ui/src/components/query-card/QueryCard.tsx @@ -5,7 +5,7 @@ import { EuiLoadingContent, keys } from '@elastic/eui' import { useParams } from 'react-router-dom' import { WBQueryType } from 'uiSrc/pages/workbench/constants' -import { RunQueryMode } from 'uiSrc/slices/interfaces/workbench' +import { RunQueryMode, ResultsMode, ResultsSummary } from 'uiSrc/slices/interfaces/workbench' import { getWBQueryType, getVisualizationsByCommand, @@ -17,7 +17,7 @@ import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { toggleOpenWBResult } from 'uiSrc/slices/workbench/wb-results' import QueryCardHeader from './QueryCardHeader' -import QueryCardCliResult from './QueryCardCliResult' +import QueryCardCliResultWrapper from './QueryCardCliResultWrapper' import QueryCardCliPlugin from './QueryCardCliPlugin' import QueryCardCommonResult, { CommonErrorResponse } from './QueryCardCommonResult' @@ -29,9 +29,14 @@ export interface Props { isOpen: boolean result: Maybe activeMode: RunQueryMode - mode: RunQueryMode + mode?: RunQueryMode + activeResultsMode?: ResultsMode + resultsMode?: ResultsMode + emptyCommand: boolean + summary?: ResultsSummary createdAt?: Date loading?: boolean + isNotStored?: boolean onQueryDelete: () => void onQueryReRun: () => void onQueryOpen: () => void @@ -40,6 +45,14 @@ export interface Props { const getDefaultPlugin = (views: IPluginVisualization[], query: string) => getVisualizationsByCommand(query, views).find((view) => view.default)?.uniqId || '' +export const getSummaryText = (summary?: ResultsSummary) => { + if (summary) { + const { total, success, fail } = summary + return `${total} Command(s) - ${success} success, ${fail} error(s)` + } + return summary +} + const QueryCard = (props: Props) => { const { id, @@ -47,12 +60,17 @@ const QueryCard = (props: Props) => { result, activeMode, mode, + activeResultsMode, + resultsMode, + summary, isOpen, createdAt, onQueryOpen, onQueryDelete, onQueryReRun, - loading + loading, + emptyCommand, + isNotStored, } = props const { visualizations = [] } = useSelector(appPluginsSelector) @@ -63,7 +81,7 @@ const QueryCard = (props: Props) => { const [viewTypeSelected, setViewTypeSelected] = useState(queryType) const [summaryText, setSummaryText] = useState('') const [selectedViewValue, setSelectedViewValue] = useState( - getDefaultPlugin(visualizations, command) || queryType + getDefaultPlugin(visualizations, command || '') || queryType ) const dispatch = useDispatch() @@ -146,6 +164,9 @@ const QueryCard = (props: Props) => { selectedValue={selectedViewValue} activeMode={activeMode} mode={mode} + activeResultsMode={activeResultsMode} + emptyCommand={emptyCommand} + summary={getSummaryText(summary)} toggleOpen={toggleOpen} toggleFullScreen={toggleFullScreen} setSelectedValue={changeViewTypeSelected} @@ -158,30 +179,46 @@ const QueryCard = (props: Props) => { ? : ( <> - {viewTypeSelected === WBQueryType.Plugin && ( + {resultsMode === ResultsMode.GroupMode && ( + + )} + {(resultsMode === ResultsMode.Default || !resultsMode) && ( <> - {!loading && result !== undefined ? ( - + {!loading && result !== undefined ? ( + + ) : ( +
+ +
+ )} + + )} + {(viewTypeSelected === WBQueryType.Text) && ( + - ) : ( -
- -
)} )} - {viewTypeSelected === WBQueryType.Text && ( - - )} )} diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliDefaultResult/QueryCardCliDefaultResult.spec.tsx b/redisinsight/ui/src/components/query-card/QueryCardCliDefaultResult/QueryCardCliDefaultResult.spec.tsx new file mode 100644 index 0000000000..2885d03d5c --- /dev/null +++ b/redisinsight/ui/src/components/query-card/QueryCardCliDefaultResult/QueryCardCliDefaultResult.spec.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import QueryCardCliDefaultResult, { Props } from './QueryCardCliDefaultResult' + +const mockedProps = mock() + +describe('QueryCardCliDefaultResult', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('Result element should render (nil) result', () => { + const mockResult = [{ + response: '', + status: 'success' + }] + + const { queryByTestId } = render( + + ) + + const resultEl = queryByTestId('query-cli-group-result') + + expect(resultEl).toHaveTextContent('(nil)') + }) +}) diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliDefaultResult/QueryCardCliDefaultResult.tsx b/redisinsight/ui/src/components/query-card/QueryCardCliDefaultResult/QueryCardCliDefaultResult.tsx new file mode 100644 index 0000000000..879a9ea15b --- /dev/null +++ b/redisinsight/ui/src/components/query-card/QueryCardCliDefaultResult/QueryCardCliDefaultResult.tsx @@ -0,0 +1,22 @@ +import React from 'react' + +import { CommandExecutionResult } from 'uiSrc/slices/interfaces' +import { cliParseTextResponse, CliPrefix, Maybe } from 'uiSrc/utils' + +export interface Props { + query: string + result: Maybe +} + +const QueryCardCliGroupResult = (props: Props) => { + const { result = [], query } = props + + return ( +
+ {result?.map(({ response, status }) => + cliParseTextResponse(response || '(nil)', query, status, CliPrefix.QueryCard))} +
+ ) +} + +export default QueryCardCliGroupResult diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliDefaultResult/index.ts b/redisinsight/ui/src/components/query-card/QueryCardCliDefaultResult/index.ts new file mode 100644 index 0000000000..bbd2e4a2a6 --- /dev/null +++ b/redisinsight/ui/src/components/query-card/QueryCardCliDefaultResult/index.ts @@ -0,0 +1,3 @@ +import QueryCardCliDefaultResult from './QueryCardCliDefaultResult' + +export default QueryCardCliDefaultResult diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliGroupResult/QueryCardCliGroupResult.spec.tsx b/redisinsight/ui/src/components/query-card/QueryCardCliGroupResult/QueryCardCliGroupResult.spec.tsx new file mode 100644 index 0000000000..3b949dd17d --- /dev/null +++ b/redisinsight/ui/src/components/query-card/QueryCardCliGroupResult/QueryCardCliGroupResult.spec.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { render } from 'uiSrc/utils/test-utils' +import QueryCardCliGroupResult from './QueryCardCliGroupResult' + +describe('QueryCardCliGroupResult', () => { + it('should render', () => { + const mockResult = [{ + response: [{ + response: 'response', + status: 'success' + }], + status: 'success' + }] + expect(render()).toBeTruthy() + }) + + it('Should render result when result is undefined', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliGroupResult/QueryCardCliGroupResult.tsx b/redisinsight/ui/src/components/query-card/QueryCardCliGroupResult/QueryCardCliGroupResult.tsx new file mode 100644 index 0000000000..97db801cc5 --- /dev/null +++ b/redisinsight/ui/src/components/query-card/QueryCardCliGroupResult/QueryCardCliGroupResult.tsx @@ -0,0 +1,20 @@ +import React from 'react' + +import { CommandExecutionResult } from 'uiSrc/slices/interfaces' +import { cliParseCommandsGroupResult, Maybe } from 'uiSrc/utils' + +export interface Props { + result?: Maybe +} + +const QueryCardCliGroupResult = (props: Props) => { + const { result = [] } = props + return ( +
+ {result[0]?.response.map((item: any, index: number) => + cliParseCommandsGroupResult(item, index))} +
+ ) +} + +export default QueryCardCliGroupResult diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliGroupResult/index.ts b/redisinsight/ui/src/components/query-card/QueryCardCliGroupResult/index.ts new file mode 100644 index 0000000000..19367bde42 --- /dev/null +++ b/redisinsight/ui/src/components/query-card/QueryCardCliGroupResult/index.ts @@ -0,0 +1,3 @@ +import QueryCardCliGroupResult from './QueryCardCliGroupResult' + +export default QueryCardCliGroupResult diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliResult/QueryCardCliResult.spec.tsx b/redisinsight/ui/src/components/query-card/QueryCardCliResult/QueryCardCliResult.spec.tsx deleted file mode 100644 index 1ed05e2ddf..0000000000 --- a/redisinsight/ui/src/components/query-card/QueryCardCliResult/QueryCardCliResult.spec.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { cloneDeep } from 'lodash' -import React from 'react' -import { instance, mock } from 'ts-mockito' -import { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils' -import QueryCardCliResult, { Props } from './QueryCardCliResult' - -const mockedProps = mock() - -let store: typeof mockedStore -beforeEach(() => { - cleanup() - store = cloneDeep(mockedStore) - store.clearActions() -}) - -const resultTestId = 'query-cli-result' - -jest.mock('uiSrc/services', () => ({ - ...jest.requireActual('uiSrc/services'), - sessionStorageService: { - set: jest.fn(), - get: jest.fn(), - }, -})) - -describe('QueryCardCliResult', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) - - it('Result element should render with result prop', () => { - const mockResult = [{ - response: 'response', - status: 'success' - }] - - const { queryByTestId } = render( - - ) - - const resultEl = queryByTestId(resultTestId) - - expect(resultEl).toBeInTheDocument() - expect(resultEl).toHaveTextContent(mockResult?.[0]?.response) - }) - - it('Result element should render (nil) result', () => { - const mockResult = [{ - response: '', - status: 'success' - }] - - const { queryByTestId } = render( - - ) - - const resultEl = queryByTestId(resultTestId) - - expect(resultEl).toHaveTextContent('(nil)') - }) -}) diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliResult/QueryCardCliResult.tsx b/redisinsight/ui/src/components/query-card/QueryCardCliResult/QueryCardCliResult.tsx deleted file mode 100644 index 903781b615..0000000000 --- a/redisinsight/ui/src/components/query-card/QueryCardCliResult/QueryCardCliResult.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react' -import cx from 'classnames' -import { EuiLoadingContent } from '@elastic/eui' - -import { CommandExecutionResult } from 'uiSrc/slices/interfaces' -import { cliParseTextResponse, CliPrefix, Maybe, } from 'uiSrc/utils' - -import styles from './styles.module.scss' - -export interface Props { - query: string; - result: Maybe - loading?: boolean; -} - -const QueryCardCliResult = (props: Props) => { - const { result = [], query, loading } = props - - return ( -
- {!loading && ( -
- {result?.map(({ response, status }) => - cliParseTextResponse(response || '(nil)', query, status, CliPrefix.QueryCard))} -
- )} - {loading && ( -
- -
- )} -
- ) -} - -export default QueryCardCliResult diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliResult/index.ts b/redisinsight/ui/src/components/query-card/QueryCardCliResult/index.ts deleted file mode 100644 index ba35164fe6..0000000000 --- a/redisinsight/ui/src/components/query-card/QueryCardCliResult/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import QueryCardCliResult from './QueryCardCliResult' - -export default QueryCardCliResult diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.spec.tsx b/redisinsight/ui/src/components/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.spec.tsx new file mode 100644 index 0000000000..acfb56a571 --- /dev/null +++ b/redisinsight/ui/src/components/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.spec.tsx @@ -0,0 +1,106 @@ +import { cloneDeep } from 'lodash' +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils' +import { ResultsMode } from 'uiSrc/slices/interfaces/workbench' +import QueryCardCliResultWrapper, { Props } from './QueryCardCliResultWrapper' +import QueryCardCliDefaultResult, { Props as QueryCardCliDefaultResultProps } from '../QueryCardCliDefaultResult' +import QueryCardCliGroupResult, { Props as QueryCardCliGroupResultProps } from '../QueryCardCliGroupResult' + +const mockedProps = mock() +const mockedQueryCardCliDefaultResultProps = mock() +const mockedQueryCardCliGroupResultProps = mock() + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), + sessionStorageService: { + set: jest.fn(), + get: jest.fn(), + }, +})) + +describe('QueryCardCliResultWrapper', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('Result element should render with result prop', () => { + const mockResult = [{ + response: 'response', + status: 'success' + }] + + render( + + ) + + expect(screen.queryByTestId('query-cli-result')).toBeInTheDocument() + expect(screen.queryByTestId('query-cli-result')).toHaveTextContent(mockResult?.[0]?.response) + }) + + it('Result element should render (nil) result', () => { + const mockResult = [{ + response: '', + status: 'success' + }] + + render( + + ) + + expect(screen.queryByTestId('query-cli-result')).toHaveTextContent('(nil)') + }) + + it('should render QueryCardCliDefaultResult', () => { + expect(render()).toBeTruthy() + }) + + it('should render QueryCardCliGroupResult', () => { + const mockResult = [{ + response: ['response'], + status: 'success' + }] + + render( + + ) + + expect(render()).toBeTruthy() + }) + + it('should render QueryCardCliDefaultResult when result.response is not array', () => { + const mockResult = [{ + response: 'response', + status: 'success' + }] + + render( + + ) + + expect(render()).toBeTruthy() + }) + + it('Should render loader', () => { + render( + + ) + + expect(screen.queryByTestId('query-cli-loader')).toBeInTheDocument() + }) + + it('should render warning', () => { + render( + + ) + + expect(screen.queryByTestId('query-cli-warning')).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.tsx b/redisinsight/ui/src/components/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.tsx new file mode 100644 index 0000000000..2f62af87f5 --- /dev/null +++ b/redisinsight/ui/src/components/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import cx from 'classnames' +import { EuiLoadingContent, EuiIcon, EuiText } from '@elastic/eui' +import { isArray } from 'lodash' + +import { CommandExecutionResult } from 'uiSrc/slices/interfaces' +import { ResultsMode } from 'uiSrc/slices/interfaces/workbench' +import { Maybe } from 'uiSrc/utils' + +import QueryCardCliDefaultResult from '../QueryCardCliDefaultResult' +import QueryCardCliGroupResult from '../QueryCardCliGroupResult' +import styles from './styles.module.scss' + +export interface Props { + query: string + result: Maybe + loading?: boolean + status?: string + resultsMode?: ResultsMode + isNotStored?: boolean +} + +const QueryCardCliResultWrapper = (props: Props) => { + const { result = [], query, loading, resultsMode, isNotStored } = props + + return ( +
+ {!loading && ( +
+ {isNotStored && ( + + + The result is too big to be saved. It will be deleted after the application is closed. + + )} + {resultsMode === ResultsMode.GroupMode && isArray(result[0]?.response) + ? + : } +
+ )} + {loading && ( +
+ +
+ )} +
+ ) +} + +export default React.memo(QueryCardCliResultWrapper) diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliResultWrapper/index.ts b/redisinsight/ui/src/components/query-card/QueryCardCliResultWrapper/index.ts new file mode 100644 index 0000000000..c49e174b7e --- /dev/null +++ b/redisinsight/ui/src/components/query-card/QueryCardCliResultWrapper/index.ts @@ -0,0 +1,3 @@ +import QueryCardCliResultWrapper from './QueryCardCliResultWrapper' + +export default QueryCardCliResultWrapper diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliResult/styles.module.scss b/redisinsight/ui/src/components/query-card/QueryCardCliResultWrapper/styles.module.scss similarity index 73% rename from redisinsight/ui/src/components/query-card/QueryCardCliResult/styles.module.scss rename to redisinsight/ui/src/components/query-card/QueryCardCliResultWrapper/styles.module.scss index 717405011d..e42a5ab4fb 100644 --- a/redisinsight/ui/src/components/query-card/QueryCardCliResult/styles.module.scss +++ b/redisinsight/ui/src/components/query-card/QueryCardCliResultWrapper/styles.module.scss @@ -25,7 +25,6 @@ } .loading { - // margin-top: 8px; height: 17px; max-width: 600px; @@ -33,3 +32,16 @@ margin-bottom: 0 !important; } } + +.container .alert { + font-size: 14px !important; + line-height: 17px !important; + letter-spacing: -0.13px !important; + color: var(--euiColorWarningLight) !important; + margin-bottom: 4px; +} + +.container .alertIcon { + margin-right: 6px; + margin-top: -3px; +} diff --git a/redisinsight/ui/src/components/query-card/QueryCardHeader/QueryCardHeader.tsx b/redisinsight/ui/src/components/query-card/QueryCardHeader/QueryCardHeader.tsx index 34523d0584..a2b2f1df06 100644 --- a/redisinsight/ui/src/components/query-card/QueryCardHeader/QueryCardHeader.tsx +++ b/redisinsight/ui/src/components/query-card/QueryCardHeader/QueryCardHeader.tsx @@ -22,7 +22,7 @@ import { appPluginsSelector } from 'uiSrc/slices/app/plugins' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { getViewTypeOptions, WBQueryType } from 'uiSrc/pages/workbench/constants' import { IPluginVisualization } from 'uiSrc/slices/interfaces' -import { RunQueryMode } from 'uiSrc/slices/interfaces/workbench' +import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import DefaultPluginIconDark from 'uiSrc/assets/img/workbench/default_view_dark.svg' @@ -39,10 +39,13 @@ export interface Props { createdAt?: Date summaryText?: string activeMode: RunQueryMode - mode: RunQueryMode + mode?: RunQueryMode + activeResultsMode?: ResultsMode + summary?: string queryType: WBQueryType selectedValue: string loading?: boolean + emptyCommand: boolean toggleOpen: () => void toggleFullScreen: () => void setSelectedValue: (type: WBQueryType, value: string) => void @@ -61,8 +64,11 @@ const QueryCardHeader = (props: Props) => { summaryText, createdAt, mode, + activeResultsMode, + summary, activeMode, selectedValue, + emptyCommand, setSelectedValue, onQueryDelete, onQueryReRun, @@ -86,6 +92,7 @@ const QueryCardHeader = (props: Props) => { databaseId: instanceId, command: getCommandNameFromQuery(query, COMMANDS_SPEC), rawMode: activeMode === RunQueryMode.Raw, + group: activeResultsMode === ResultsMode.GroupMode, ...additionalData } }) @@ -218,13 +225,14 @@ const QueryCardHeader = (props: Props) => { >
- + handleCopy(event, query)} + onClick={(event: React.MouseEvent) => handleCopy(event, query || '')} + data-testid="copy-command" />
@@ -235,7 +243,7 @@ const QueryCardHeader = (props: Props) => { {mode === RunQueryMode.Raw && ( @@ -256,7 +264,7 @@ const QueryCardHeader = (props: Props) => { className={cx(styles.buttonIcon, styles.viewTypeIcon)} onClick={onDropDownViewClick} > - {isOpen && options.length > 1 && ( + {isOpen && options.length > 1 && !summary && (
{ content="Run again" position="left" > - + )} diff --git a/redisinsight/ui/src/components/query-card/QueryCardHeader/styles.module.scss b/redisinsight/ui/src/components/query-card/QueryCardHeader/styles.module.scss index 13e5f7f87b..e7dd9af359 100644 --- a/redisinsight/ui/src/components/query-card/QueryCardHeader/styles.module.scss +++ b/redisinsight/ui/src/components/query-card/QueryCardHeader/styles.module.scss @@ -40,10 +40,10 @@ $marginIcon: 12px; } .titleWrapper { - width: calc(100% - 350px); + width: calc(100% - 380px); @media (min-width: $breakpoint-m) { - width: calc(100% - 420px); + width: calc(100% - 450px); } } diff --git a/redisinsight/ui/src/components/query-card/QueryCardTooltip/QueryCardTooltip.spec.tsx b/redisinsight/ui/src/components/query-card/QueryCardTooltip/QueryCardTooltip.spec.tsx index a36a484c87..33a8833808 100644 --- a/redisinsight/ui/src/components/query-card/QueryCardTooltip/QueryCardTooltip.spec.tsx +++ b/redisinsight/ui/src/components/query-card/QueryCardTooltip/QueryCardTooltip.spec.tsx @@ -1,5 +1,6 @@ import React from 'react' import { instance, mock } from 'ts-mockito' +import { EMPTY_COMMAND } from 'uiSrc/constants' import { render } from 'uiSrc/utils/test-utils' import QueryCardTooltip, { Props } from './QueryCardTooltip' @@ -9,4 +10,12 @@ describe('QueryCardTooltip', () => { it('should render', () => { expect(render()).toBeTruthy() }) + + it(`should show ${EMPTY_COMMAND} if command=null and summary=`, () => { + const { queryByTestId } = render( + + ) + + expect(queryByTestId('query-card-tooltip-anchor')).toHaveTextContent(EMPTY_COMMAND) + }) }) diff --git a/redisinsight/ui/src/components/query-card/QueryCardTooltip/QueryCardTooltip.tsx b/redisinsight/ui/src/components/query-card/QueryCardTooltip/QueryCardTooltip.tsx index 4d0cb75754..fa532584fa 100644 --- a/redisinsight/ui/src/components/query-card/QueryCardTooltip/QueryCardTooltip.tsx +++ b/redisinsight/ui/src/components/query-card/QueryCardTooltip/QueryCardTooltip.tsx @@ -3,24 +3,26 @@ import { EuiToolTip } from '@elastic/eui' import { take } from 'lodash' import cx from 'classnames' -import { truncateText } from 'uiSrc/utils' +import { Nullable, truncateText } from 'uiSrc/utils' +import { EMPTY_COMMAND } from 'uiSrc/constants' import styles from './styles.module.scss' export interface Props { - query: string; - maxLinesNumber?: number; + query: Nullable + summary?: Nullable + maxLinesNumber?: number } interface IQueryLine { - index: number; - value: string; - isFolding?: boolean; + index: number + value: string + isFolding?: boolean } const QueryCardTooltip = (props: Props) => { - const { query = '', maxLinesNumber = 20 } = props - let queryLines: IQueryLine[] = query - .split('\n') + const { query = '', maxLinesNumber = 20, summary = '' } = props + + let queryLines: IQueryLine[] = (query || EMPTY_COMMAND).split('\n') .map((query: string, i) => ({ value: truncateText(query, 497, '...'), index: i @@ -54,7 +56,7 @@ const QueryCardTooltip = (props: Props) => { content={<>{contentItems}} position="bottom" > - {query} + {summary || query || EMPTY_COMMAND} ) } diff --git a/redisinsight/ui/src/components/query-card/styles.module.scss b/redisinsight/ui/src/components/query-card/styles.module.scss index d420b72d5c..f7dd14a19b 100644 --- a/redisinsight/ui/src/components/query-card/styles.module.scss +++ b/redisinsight/ui/src/components/query-card/styles.module.scss @@ -3,6 +3,7 @@ @import '@elastic/eui/src/global_styling/index'; .containerWrapper { + min-width: 420px; &:nth-of-type(even) { background-color: var(--euiColorEmptyShade) !important; } diff --git a/redisinsight/ui/src/components/query/Query/Query.tsx b/redisinsight/ui/src/components/query/Query/Query.tsx index 16ca685738..c2a406b2bb 100644 --- a/redisinsight/ui/src/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/components/query/Query/Query.tsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useRef, useState } from 'react' import AutoSizer from 'react-virtualized-auto-sizer' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { compact, findIndex } from 'lodash' import cx from 'classnames' import { EuiButtonIcon, EuiButton, EuiIcon, EuiLoadingSpinner, EuiText, EuiToolTip } from '@elastic/eui' @@ -35,23 +35,26 @@ import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { IEditorMount, ISnippetController } from 'uiSrc/pages/workbench/interfaces' import { CommandExecutionUI } from 'uiSrc/slices/interfaces' import { darkTheme, lightTheme } from 'uiSrc/constants/monaco/cypher' -import { RunQueryMode } from 'uiSrc/slices/interfaces/workbench' +import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { workbenchResultsSelector } from 'uiSrc/slices/workbench/wb-results' +import { stopProcessing, workbenchResultsSelector } from 'uiSrc/slices/workbench/wb-results' import DedicatedEditor from 'uiSrc/components/query/DedicatedEditor/DedicatedEditor' import { ReactComponent as RawModeIcon } from 'uiSrc/assets/img/icons/raw_mode.svg' +import { ReactComponent as GroupModeIcon } from 'uiSrc/assets/img/icons/group_mode.svg' import styles from './styles.module.scss' export interface Props { query: string activeMode: RunQueryMode + resultsMode?: ResultsMode setQueryEl: Function setQuery: (script: string) => void setIsCodeBtnDisabled: (value: boolean) => void onSubmit: (query?: string) => void onKeyDown?: (e: React.KeyboardEvent, script: string) => void onQueryChangeMode: () => void + onChangeGroupMode: () => void } const SYNTAX_CONTEXT_ID = 'syntaxWidgetContext' @@ -68,12 +71,14 @@ const Query = (props: Props) => { const { query = '', activeMode, + resultsMode, setQuery, onKeyDown, onSubmit, setQueryEl, setIsCodeBtnDisabled = () => { }, - onQueryChangeMode + onQueryChangeMode, + onChangeGroupMode } = props let contribution: Nullable = null const [isDedicatedEditorOpen, setIsDedicatedEditorOpen] = useState(false) @@ -86,18 +91,21 @@ const Query = (props: Props) => { let syntaxWidgetContext: Nullable> = null const { commandsArray: REDIS_COMMANDS_ARRAY, spec: REDIS_COMMANDS_SPEC } = useSelector(appRedisCommandsSelector) - const { items: execHistoryItems, loading } = useSelector(workbenchResultsSelector) + const { items: execHistoryItems, loading, processing } = useSelector(workbenchResultsSelector) const { theme } = useContext(ThemeContext) const monacoObjects = useRef>(null) const { instanceId = '' } = useParams<{ instanceId: string }>() + const dispatch = useDispatch() + let disposeCompletionItemProvider = () => {} let disposeSignatureHelpProvider = () => {} useEffect(() => // componentWillUnmount () => { + dispatch(stopProcessing()) contribution?.dispose?.() disposeCompletionItemProvider() disposeSignatureHelpProvider() @@ -451,6 +459,8 @@ const Query = (props: Props) => { monaco.editor.defineTheme('light', lightTheme) } + const isLoading = loading || processing + return (
{
{ size="s" color="secondary" onClick={() => onQueryChangeMode()} - disabled={loading} + disabled={isLoading} className={cx(styles.textBtn, { [styles.activeBtn]: activeMode === RunQueryMode.Raw })} data-testid="btn-change-mode" > @@ -492,34 +502,54 @@ const Query = (props: Props) => { - {`${KEYBOARD_SHORTCUTS.workbench.runQuery?.label}:\u00A0\u00A0`} - -
- ) + isLoading + ? 'Please wait while the commands are being executed…' + : KEYBOARD_SHORTCUTS?.workbench?.runQuery && ( +
+ {`${KEYBOARD_SHORTCUTS.workbench.runQuery?.label}:\u00A0\u00A0`} + +
+ ) } data-testid="run-query-tooltip" > <> - {loading && ( + {isLoading && ( )} handleSubmit()} - disabled={loading} + disabled={isLoading} iconType="playFilled" - className={cx(styles.submitButton, { [styles.submitButtonLoading]: loading })} + className={cx(styles.submitButton, { [styles.submitButtonLoading]: isLoading })} aria-label="submit" data-testid="btn-submit" /> - {/* block for third action icon */} -
+ + <> + onChangeGroupMode()} + disabled={isLoading} + className={cx(styles.textBtn, { [styles.activeBtn]: resultsMode === ResultsMode.GroupMode })} + data-testid="btn-change-group-mode" + > + + + +
{isDedicatedEditorOpen && ( diff --git a/redisinsight/ui/src/components/query/Query/styles.module.scss b/redisinsight/ui/src/components/query/Query/styles.module.scss index 2538f48358..9df4b430b5 100644 --- a/redisinsight/ui/src/components/query/Query/styles.module.scss +++ b/redisinsight/ui/src/components/query/Query/styles.module.scss @@ -137,3 +137,7 @@ height: 24px; } } + +.tooltipText { + font-size: 12px !important; +} diff --git a/redisinsight/ui/src/components/query/QueryWrapper.tsx b/redisinsight/ui/src/components/query/QueryWrapper.tsx index 0758540282..496520286c 100644 --- a/redisinsight/ui/src/components/query/QueryWrapper.tsx +++ b/redisinsight/ui/src/components/query/QueryWrapper.tsx @@ -8,7 +8,7 @@ import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { getMultiCommands, removeMonacoComments, splitMonacoValuePerLines } from 'uiSrc/utils' import { userSettingsConfigSelector } from 'uiSrc/slices/user/user-settings' -import { RunQueryMode } from 'uiSrc/slices/interfaces/workbench' +import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' import { PIPELINE_COUNT_DEFAULT } from 'uiSrc/constants/api' import Query from './Query' import styles from './Query/styles.module.scss' @@ -16,32 +16,38 @@ import styles from './Query/styles.module.scss' export interface Props { query: string activeMode: RunQueryMode + resultsMode?: ResultsMode setQuery: (script: string) => void setQueryEl: Function setIsCodeBtnDisabled: (value: boolean) => void onKeyDown?: (e: React.KeyboardEvent, script: string) => void onSubmit: (value?: string) => void onQueryChangeMode: () => void + onChangeGroupMode: () => void } interface IState { activeMode: RunQueryMode + resultsMode?: ResultsMode } let state: IState = { activeMode: RunQueryMode.ASCII, + resultsMode: ResultsMode.Default } const QueryWrapper = (props: Props) => { const { query = '', activeMode, + resultsMode, setQuery, setQueryEl, setIsCodeBtnDisabled, onKeyDown, onSubmit, - onQueryChangeMode + onQueryChangeMode, + onChangeGroupMode } = props const { instanceId = '' } = useParams<{ instanceId: string }>() const { @@ -52,6 +58,7 @@ const QueryWrapper = (props: Props) => { state = { activeMode, + resultsMode } const sendEventSubmitTelemetry = (commandInit = query) => { @@ -73,7 +80,8 @@ const QueryWrapper = (props: Props) => { databaseId: instanceId, multiple: multiCommands ? 'Multiple' : 'Single', pipeline: batchSize > 1, - rawMode: state.activeMode === RunQueryMode.Raw + rawMode: state.activeMode === RunQueryMode.Raw, + group: state.resultsMode === ResultsMode.GroupMode } })() @@ -101,12 +109,14 @@ const QueryWrapper = (props: Props) => { ) } diff --git a/redisinsight/ui/src/components/range-filter/styles.module.scss b/redisinsight/ui/src/components/range-filter/styles.module.scss index 67262bbdab..9c1b2ae5bd 100644 --- a/redisinsight/ui/src/components/range-filter/styles.module.scss +++ b/redisinsight/ui/src/components/range-filter/styles.module.scss @@ -83,7 +83,7 @@ .sliderRightValue { width: max-content; color: var(--euiColorMediumShade); - font: normal normal normal 12px/18px Graphik; + font: normal normal normal 12px/18px Graphik, sans-serif; margin-top: 8px; } diff --git a/redisinsight/ui/src/components/scan-more/ScanMore.tsx b/redisinsight/ui/src/components/scan-more/ScanMore.tsx index 8cff7d16bb..629e965ccb 100644 --- a/redisinsight/ui/src/components/scan-more/ScanMore.tsx +++ b/redisinsight/ui/src/components/scan-more/ScanMore.tsx @@ -34,32 +34,32 @@ const ScanMore = ({ {((scanned < totalItemsCount || isNull(totalItemsCount))) && nextCursor !== '0' && ( - - loadMoreItems?.({ - stopIndex: SCAN_COUNT_DEFAULT - 1, - startIndex: 0, - })} - data-testid="scan-more" - > - {withAlert && ( - - - - )} - Scan more - - )} + + loadMoreItems?.({ + stopIndex: SCAN_COUNT_DEFAULT - 1, + startIndex: 0, + })} + data-testid="scan-more" + > + {withAlert && ( + + + + )} + Scan more + + )} ) diff --git a/redisinsight/ui/src/components/advanced-settings/AdvancedSettingsItem.spec.tsx b/redisinsight/ui/src/components/settings-item/SettingItem.spec.tsx similarity index 83% rename from redisinsight/ui/src/components/advanced-settings/AdvancedSettingsItem.spec.tsx rename to redisinsight/ui/src/components/settings-item/SettingItem.spec.tsx index 8f7fa86a4d..c99288ad59 100644 --- a/redisinsight/ui/src/components/advanced-settings/AdvancedSettingsItem.spec.tsx +++ b/redisinsight/ui/src/components/settings-item/SettingItem.spec.tsx @@ -4,7 +4,7 @@ import { screen, fireEvent, } from 'uiSrc/utils/test-utils' -import AdvancedSettingsItem from './AdvancedSettingsItem' +import SettingItem from './SettingItem' jest.mock('uiSrc/slices/user/user-settings', () => ({ ...jest.requireActual('uiSrc/slices/user/user-settings'), @@ -28,24 +28,24 @@ const mockedProps = { label: 'Keys to Scan:', } -describe('AdvancedSettingsItem', () => { +describe('SettingItem', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) it('should render keys to scan value', () => { - render() + render() expect(screen.getByTestId(/keys-to-scan-value/)).toHaveTextContent('10000') }) it('should render keys to scan input after click value', () => { - render() + render() fireEvent.click(screen.getByTestId(/keys-to-scan-value/)) expect(screen.getByTestId(/keys-to-scan-input/)).toBeInTheDocument() }) it('should change keys to scan input properly', () => { - render() + render() fireEvent.click(screen.getByTestId(/keys-to-scan-value/)) fireEvent.change( screen.getByTestId(/keys-to-scan-input/), @@ -57,7 +57,7 @@ describe('AdvancedSettingsItem', () => { }) it('should properly apply changes', () => { - render() + render() fireEvent.click(screen.getByTestId(/keys-to-scan-value/)) fireEvent.change( @@ -71,7 +71,7 @@ describe('AdvancedSettingsItem', () => { }) it('should properly decline changes', async () => { - render() + render() fireEvent.click(screen.getByTestId(/keys-to-scan-value/)) fireEvent.change( diff --git a/redisinsight/ui/src/components/advanced-settings/AdvancedSettingsItem.tsx b/redisinsight/ui/src/components/settings-item/SettingItem.tsx similarity index 97% rename from redisinsight/ui/src/components/advanced-settings/AdvancedSettingsItem.tsx rename to redisinsight/ui/src/components/settings-item/SettingItem.tsx index e943d024f9..1b74bca03d 100644 --- a/redisinsight/ui/src/components/advanced-settings/AdvancedSettingsItem.tsx +++ b/redisinsight/ui/src/components/settings-item/SettingItem.tsx @@ -25,7 +25,7 @@ export interface Props { validation: (value: string) => string, } -const AdvancedSettingsItem = (props: Props) => { +const SettingItem = (props: Props) => { const { initValue, title, summary, testid, placeholder, label, onApply, validation = (val: string) => val } = props const [value, setValue] = useState(initValue) @@ -117,4 +117,4 @@ const AdvancedSettingsItem = (props: Props) => { ) } -export default AdvancedSettingsItem +export default SettingItem diff --git a/redisinsight/ui/src/components/advanced-settings/styles.module.scss b/redisinsight/ui/src/components/settings-item/styles.module.scss similarity index 81% rename from redisinsight/ui/src/components/advanced-settings/styles.module.scss rename to redisinsight/ui/src/components/settings-item/styles.module.scss index 19b12ebb35..df304ccab8 100644 --- a/redisinsight/ui/src/components/advanced-settings/styles.module.scss +++ b/redisinsight/ui/src/components/settings-item/styles.module.scss @@ -17,7 +17,7 @@ } .inputLabel { - font: normal normal normal 13px/18px Graphik !important; + font: normal normal normal 13px/18px Graphik, sans-serif !important; font-weight: 500 !important; } @@ -35,6 +35,6 @@ } .smallText { - font: normal normal normal 14px/24px Graphik !important; + font: normal normal normal 14px/24px Graphik, sans-serif !important; letter-spacing: -0.14px; } diff --git a/redisinsight/ui/src/components/virtual-grid/utils.tsx b/redisinsight/ui/src/components/virtual-grid/utils.tsx index 50b53b4d86..756dea3fd5 100644 --- a/redisinsight/ui/src/components/virtual-grid/utils.tsx +++ b/redisinsight/ui/src/components/virtual-grid/utils.tsx @@ -43,10 +43,11 @@ export const useInnerElementType = ( React.forwardRef((props:ReactNode, ref) => { const sumRowsHeights = (index: number) => { let sum = 0 + let currentIndex = index - while (index > 1) { + while (currentIndex > 1) { sum += rowHeight(index - 1) - index -= 1 + currentIndex -= 1 } return sum @@ -54,10 +55,11 @@ export const useInnerElementType = ( const sumColumnWidths = (index: number) => { let sum = 0 + let currentIndex = index - while (index > 1) { + while (currentIndex > 1) { sum += columnWidth(index - 1, tableWidth, columns) - index -= 1 + currentIndex -= 1 } return sum @@ -65,7 +67,7 @@ export const useInnerElementType = ( const shownIndecies = getShownIndicies(props.children) - let children = React.Children.map(props.children, (child, index) => { + let children = React.Children.map(props.children, (child) => { const { column, row } = getCellIndicies(child) // do not show non-sticky cell diff --git a/redisinsight/ui/src/components/virtual-table/utils.tsx b/redisinsight/ui/src/components/virtual-table/utils.tsx index 6c82e18bfb..84954f7fa0 100644 --- a/redisinsight/ui/src/components/virtual-table/utils.tsx +++ b/redisinsight/ui/src/components/virtual-table/utils.tsx @@ -1,6 +1,6 @@ import React from 'react' -export const StopPropagation = ({ children }) => ( +export const StopPropagation = ({ children }: { children: JSX.Element }) => (
e.stopPropagation()} diff --git a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx b/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx index 6ec385929b..ccb5722676 100644 --- a/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx +++ b/redisinsight/ui/src/components/virtual-tree/components/Node/Node.tsx @@ -16,7 +16,6 @@ const Node = ({ setOpen }: NodePublicState) => { const { - id, isLeaf, leafIcon, keys, diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index b28463858a..932fe610dd 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -72,6 +72,7 @@ enum ApiEndpoints { PUB_SUB = 'pub-sub', PUB_SUB_MESSAGES = 'pub-sub/messages', + CLUSTER_DETAILS = 'cluster-details', NOTIFICATIONS = 'notifications', NOTIFICATIONS_READ = 'notifications/read', diff --git a/redisinsight/ui/src/constants/browser.ts b/redisinsight/ui/src/constants/browser.ts index e6bf53ee4a..e746b0797a 100644 --- a/redisinsight/ui/src/constants/browser.ts +++ b/redisinsight/ui/src/constants/browser.ts @@ -4,3 +4,9 @@ export const TEXT_UNPRINTABLE_CHARACTERS = { title: 'Non-printable characters have been detected', text: 'Use Workbench or CLI to edit without data loss.', } +export const TEXT_DISABLED_FORMATTER_EDITING = 'Cannot edit the value in this format' + +export const TEXT_INVALID_VALUE = { + title: 'Value will be saved as Unicode', + text: 'as it is not valid in the selected format.', +} diff --git a/redisinsight/ui/src/constants/keys.ts b/redisinsight/ui/src/constants/keys.ts index 59fe9ddb29..12aaaa8d66 100644 --- a/redisinsight/ui/src/constants/keys.ts +++ b/redisinsight/ui/src/constants/keys.ts @@ -170,4 +170,8 @@ export enum KeyValueFormat { HEX = 'HEX', Binary = 'Binary', Msgpack = 'Msgpack', + PHP = 'PHP serialized', + JAVA = 'Java serialized', + Protobuf = 'Protobuf', + Pickle = 'Pickle', } diff --git a/redisinsight/ui/src/constants/links.ts b/redisinsight/ui/src/constants/links.ts index 0c29f63633..506be9f976 100644 --- a/redisinsight/ui/src/constants/links.ts +++ b/redisinsight/ui/src/constants/links.ts @@ -2,4 +2,5 @@ export const EXTERNAL_LINKS = { githubRepo: 'https://github.com/RedisInsight/RedisInsight', githubIssues: 'https://github.com/RedisInsight/RedisInsight/issues', releaseNotes: 'https://github.com/RedisInsight/RedisInsight/releases', + userSurvey: 'https://www.surveymonkey.com/r/redisinsight', } diff --git a/redisinsight/ui/src/constants/pages.ts b/redisinsight/ui/src/constants/pages.ts index 2302822b24..7a67015265 100644 --- a/redisinsight/ui/src/constants/pages.ts +++ b/redisinsight/ui/src/constants/pages.ts @@ -15,6 +15,8 @@ export enum PageNames { browser = 'browser', slowLog = 'slowlog', pubSub = 'pub-sub', + analytics = 'analytics', + clusterDetails = 'cluster-details', } const redisCloud = '/redis-cloud' @@ -34,6 +36,8 @@ export const Pages = { sentinelDatabasesResult: `${sentinel}/databases-result`, browser: (instanceId: string) => `/${instanceId}/${PageNames.browser}`, workbench: (instanceId: string) => `/${instanceId}/${PageNames.workbench}`, - slowLog: (instanceId: string) => `/${instanceId}/${PageNames.slowLog}`, pubSub: (instanceId: string) => `/${instanceId}/${PageNames.pubSub}`, + analytics: (instanceId: string) => `/${instanceId}/${PageNames.analytics}`, + slowLog: (instanceId: string) => `/${instanceId}/${PageNames.analytics}/${PageNames.slowLog}`, + clusterDetails: (instanceId: string) => `/${instanceId}/${PageNames.analytics}/${PageNames.clusterDetails}`, } diff --git a/redisinsight/ui/src/constants/storage.ts b/redisinsight/ui/src/constants/storage.ts index 72fda64ab9..5d79cd8afc 100644 --- a/redisinsight/ui/src/constants/storage.ts +++ b/redisinsight/ui/src/constants/storage.ts @@ -15,7 +15,10 @@ enum BrowserStorageItem { autoRefreshRate = 'autoRefreshRate', bulkActionId = 'bulkActionId', dbConfig = 'dbConfig_', - RunQueryMode = 'RunQueryMode' + RunQueryMode = 'RunQueryMode', + wbCleanUp = 'wbCleanUp', + viewFormat = 'viewFormat', + wbGroupMode = 'wbGroupMode' } export default BrowserStorageItem diff --git a/redisinsight/ui/src/constants/texts.tsx b/redisinsight/ui/src/constants/texts.tsx index afc5413a39..bb36075032 100644 --- a/redisinsight/ui/src/constants/texts.tsx +++ b/redisinsight/ui/src/constants/texts.tsx @@ -1,8 +1,24 @@ import React from 'react' -import { EuiText, EuiSpacer } from '@elastic/eui' +import { EuiText, EuiSpacer, EuiLink } from '@elastic/eui' + +import { getRouterLinkProps } from 'uiSrc/services' export const NoResultsFoundText = (No results found.) -export const NoKeysToDisplayText = (No keys to display.) +export const NoKeysToDisplayText = (path: string, onClick: ()=> void) => ( + + No keys to display. +
+ + Use Workbench Guides and Tutorials + + {' to quickly load the data.'} +
+) + export const FullScanNoResultsFoundText = ( <> No results found. diff --git a/redisinsight/ui/src/constants/workbenchResults.ts b/redisinsight/ui/src/constants/workbenchResults.ts index 5dd9d3bcd0..affce8b9e2 100644 --- a/redisinsight/ui/src/constants/workbenchResults.ts +++ b/redisinsight/ui/src/constants/workbenchResults.ts @@ -1 +1,3 @@ export const bulkReplyCommands = ['LOLWUT', 'INFO', 'CLIENT', 'CLUSTER', 'MEMORY', 'MONITOR', 'PSUBSCRIBE'] + +export const EMPTY_COMMAND = 'Encrypted data' diff --git a/redisinsight/ui/src/mocks/handlers/analytics/clusterDetailsHandlers.ts b/redisinsight/ui/src/mocks/handlers/analytics/clusterDetailsHandlers.ts new file mode 100644 index 0000000000..b328a8e685 --- /dev/null +++ b/redisinsight/ui/src/mocks/handlers/analytics/clusterDetailsHandlers.ts @@ -0,0 +1,111 @@ +import { DatabaseInstanceResponse } from 'apiSrc/modules/instances/dto/database-instance.dto' +import { rest, RestHandler } from 'msw' +import { ClusterDetails, HealthStatus, NodeRole } from 'apiSrc/modules/cluster-monitor/models' +import { ApiEndpoints } from 'uiSrc/constants' +import { getUrl } from 'uiSrc/utils' +import { getMswURL } from 'uiSrc/utils/test-utils' + +export const INSTANCE_ID_MOCK = 'instanceId' + +const handlers: RestHandler[] = [ + // useGetClusterDetailsQuery + rest.get(getMswURL(getUrl(INSTANCE_ID_MOCK, ApiEndpoints.CLUSTER_DETAILS)), + async (_req, res, ctx) => res( + ctx.status(200), + ctx.json(CLUSTER_DETAILS_DATA_MOCK), + )) +] + +export const CLUSTER_DETAILS_DATA_MOCK: ClusterDetails = { + state: 'ok', + slotsAssigned: 16384, + slotsOk: 16384, + slotsPFail: 0, + slotsFail: 0, + slotsUnassigned: 0, + statsMessagesSent: 0, + statsMessagesReceived: 0, + currentEpoch: 0, + myEpoch: 0, + size: 3, + knownNodes: 3, + uptimeSec: 1661931600, + nodes: [ + { + id: '3', + host: '3.93.234.244', + port: 12511, + role: 'primary' as NodeRole, + slots: [ + '10923-16383' + ], + health: 'online' as HealthStatus, + totalKeys: 0, + usedMemory: 38448896, + opsPerSecond: 0, + connectionsReceived: 15, + connectedClients: 6, + commandsProcessed: 114, + networkInKbps: 0.35, + networkOutKbps: 3.62, + cacheHitRatio: 0, + replicationOffset: 0, + uptimeSec: 1661931600, + version: '6.2.6', + mode: 'standalone', + replicas: [] + }, + { + id: '4', + host: '44.202.117.57', + port: 12511, + role: 'primary' as NodeRole, + slots: [ + '0-5460' + ], + health: 'online' as HealthStatus, + totalKeys: 0, + usedMemory: 38448896, + opsPerSecond: 0, + connectionsReceived: 15, + connectedClients: 6, + commandsProcessed: 114, + networkInKbps: 0.35, + networkOutKbps: 3.62, + cacheHitRatio: 0, + replicationOffset: 0, + uptimeSec: 1661931600, + version: '6.2.6', + mode: 'standalone', + replicas: [] + }, + { + id: '5', + host: '44.210.115.34', + port: 12511, + role: 'primary' as NodeRole, + slots: [ + '5461-10922' + ], + health: 'online' as HealthStatus, + totalKeys: 0, + usedMemory: 38448896, + opsPerSecond: 0, + connectionsReceived: 15, + connectedClients: 6, + commandsProcessed: 114, + networkInKbps: 0.35, + networkOutKbps: 3.62, + cacheHitRatio: 0, + replicationOffset: 0, + uptimeSec: 1661931600, + version: '6.2.6', + mode: 'standalone', + replicas: [] + } + ], + version: '6.2.6', + mode: 'standalone' +} + +export default handlers diff --git a/redisinsight/ui/src/mocks/handlers/analytics/index.ts b/redisinsight/ui/src/mocks/handlers/analytics/index.ts new file mode 100644 index 0000000000..f548fd7655 --- /dev/null +++ b/redisinsight/ui/src/mocks/handlers/analytics/index.ts @@ -0,0 +1,6 @@ +import { DefaultBodyType, MockedRequest, RestHandler } from 'msw' + +import clusterDetails from './clusterDetailsHandlers' + +const handlers: RestHandler>[] = [].concat(clusterDetails) +export default handlers diff --git a/redisinsight/ui/src/mocks/handlers/app/index.ts b/redisinsight/ui/src/mocks/handlers/app/index.ts new file mode 100644 index 0000000000..8d37d358d5 --- /dev/null +++ b/redisinsight/ui/src/mocks/handlers/app/index.ts @@ -0,0 +1,6 @@ +import { DefaultBodyType, MockedRequest, RestHandler } from 'msw' + +import info from './infoHandlers' + +const handlers: RestHandler>[] = [].concat(info) +export default handlers diff --git a/redisinsight/ui/src/mocks/handlers/app/infoHandlers.ts b/redisinsight/ui/src/mocks/handlers/app/infoHandlers.ts new file mode 100644 index 0000000000..9a0af1ac9f --- /dev/null +++ b/redisinsight/ui/src/mocks/handlers/app/infoHandlers.ts @@ -0,0 +1,22 @@ +import { DatabaseInstanceResponse } from 'apiSrc/modules/instances/dto/database-instance.dto' +import { rest, RestHandler } from 'msw' +import { ApiEndpoints } from 'uiSrc/constants' +import { getMswURL } from 'uiSrc/utils/test-utils' + +export const APP_INFO_DATA_MOCK = { + id: 'id1', + createDateTime: '2000-01-01T00:00:00.000Z', + appVersion: '2.0.0', + osPlatform: 'win32', + buildType: 'ELECTRON' +} + +const handlers: RestHandler[] = [ + // fetchServerInfo + rest.get(getMswURL(ApiEndpoints.INFO), async (req, res, ctx) => res( + ctx.status(200), + ctx.json(APP_INFO_DATA_MOCK), + )) +] + +export default handlers diff --git a/redisinsight/ui/src/mocks/handlers/content/createRedisButtonsHandlers.ts b/redisinsight/ui/src/mocks/handlers/content/createRedisButtonsHandlers.ts new file mode 100644 index 0000000000..5c79f99723 --- /dev/null +++ b/redisinsight/ui/src/mocks/handlers/content/createRedisButtonsHandlers.ts @@ -0,0 +1,69 @@ +import { rest, RestHandler } from 'msw' +import { ApiEndpoints } from 'uiSrc/constants' +import { ConnectionType } from 'uiSrc/slices/interfaces' +import { getMswResourceURL } from 'uiSrc/utils/test-utils' + +const handlers: RestHandler[] = [ + + // fetchContentAction + rest.get(getMswResourceURL(ApiEndpoints.CONTENT_CREATE_DATABASE), async (req, res, ctx) => res( + ctx.status(200), + ctx.json([ + { + id: 'e37cc441-a4f2-402c-8bdb-fc2413cbbaff', + host: 'localhost', + port: 6379, + name: 'localhost', + username: null, + password: null, + connectionType: ConnectionType.Standalone, + nameFromProvider: null, + modules: [], + lastConnection: new Date('2021-04-22T09:03:56.917Z'), + }, + { + id: 'a0db1bc8-a353-4c43-a856-b72f4811d2d4', + host: 'localhost', + port: 12000, + name: 'oea123123', + username: null, + password: null, + connectionType: ConnectionType.Standalone, + nameFromProvider: null, + modules: [], + tls: { + verifyServerCert: true, + caCertId: '70b95d32-c19d-4311-bb24-e684af12cf15', + clientCertPairId: '70b95d32-c19d-4311-b23b24-e684af12cf15', + }, + }, + { + id: 'b83a3932-e95f-4f09-9d8a-55079f400186', + host: 'localhost', + port: 5005, + name: 'sentinel', + username: null, + password: null, + connectionType: ConnectionType.Sentinel, + nameFromProvider: null, + lastConnection: new Date('2021-04-22T18:40:44.031Z'), + modules: [], + endpoints: [ + { + host: 'localhost', + port: 5005, + }, + { + host: '127.0.0.1', + port: 5006, + }, + ], + sentinelMaster: { + name: 'mymaster', + }, + }, + ]), + )) +] + +export default handlers diff --git a/redisinsight/ui/src/mocks/handlers/content/index.ts b/redisinsight/ui/src/mocks/handlers/content/index.ts new file mode 100644 index 0000000000..b7bfd09f30 --- /dev/null +++ b/redisinsight/ui/src/mocks/handlers/content/index.ts @@ -0,0 +1,6 @@ +import { DefaultBodyType, MockedRequest, RestHandler } from 'msw' + +import crb from './createRedisButtonsHandlers' + +const handlers: RestHandler[] = [].concat(crb) +export default handlers diff --git a/redisinsight/ui/src/mocks/handlers/index.ts b/redisinsight/ui/src/mocks/handlers/index.ts new file mode 100644 index 0000000000..519ee1acf3 --- /dev/null +++ b/redisinsight/ui/src/mocks/handlers/index.ts @@ -0,0 +1,7 @@ +import { MockedRequest, RestHandler } from 'msw' +import instances from './instances' +import content from './content' +import app from './app' +import analytics from './analytics' + +export const handlers: RestHandler[] = [].concat(instances, content, app, analytics) diff --git a/redisinsight/ui/src/mocks/handlers/instances/caCertsHandlers.ts b/redisinsight/ui/src/mocks/handlers/instances/caCertsHandlers.ts new file mode 100644 index 0000000000..da4632ff4c --- /dev/null +++ b/redisinsight/ui/src/mocks/handlers/instances/caCertsHandlers.ts @@ -0,0 +1,20 @@ +import { DefaultBodyType, MockedRequest, rest, RestHandler } from 'msw' +import { ApiEndpoints } from 'uiSrc/constants' +import { getMswURL } from 'uiSrc/utils/test-utils' + +const handlers: RestHandler>[] = [ + rest.post(getMswURL(ApiEndpoints.CA_CERTIFICATES), (req, res, ctx) => { + const { username } = req.body + + return res( + ctx.json({ + id: 'f79e82e8-c34a-4dc7-a49e-9fadc0979fda', + username, + firstName: 'John', + lastName: 'Maverick', + }), + ) + }), +] + +export default handlers diff --git a/redisinsight/ui/src/mocks/handlers/instances/index.ts b/redisinsight/ui/src/mocks/handlers/instances/index.ts new file mode 100644 index 0000000000..10fc9527e8 --- /dev/null +++ b/redisinsight/ui/src/mocks/handlers/instances/index.ts @@ -0,0 +1,7 @@ +import { DefaultBodyType, MockedRequest, RestHandler } from 'msw' + +import instances from './instancesHandlers' +import caCerts from './caCertsHandlers' + +const handlers: RestHandler>[] = [].concat(instances, caCerts) +export default handlers diff --git a/redisinsight/ui/src/mocks/handlers/instances/instancesHandlers.ts b/redisinsight/ui/src/mocks/handlers/instances/instancesHandlers.ts new file mode 100644 index 0000000000..820d934fa6 --- /dev/null +++ b/redisinsight/ui/src/mocks/handlers/instances/instancesHandlers.ts @@ -0,0 +1,85 @@ +import { DatabaseInstanceResponse } from 'apiSrc/modules/instances/dto/database-instance.dto' +import { rest, RestHandler } from 'msw' +import { ApiEndpoints } from 'uiSrc/constants' +import { ConnectionType } from 'uiSrc/slices/interfaces' +import { getMswURL } from 'uiSrc/utils/test-utils' + +export const INSTANCE_ID_MOCK = 'instanceId' + +const handlers: RestHandler[] = [ + // fetchInstancesAction + rest.get(getMswURL(ApiEndpoints.INSTANCE), async (req, res, ctx) => res( + ctx.status(200), + ctx.json([ + { + id: 'e37cc441-a4f2-402c-8bdb-fc2413cbbaff', + host: 'localhost', + port: 6379, + name: 'localhost', + username: null, + password: null, + connectionType: ConnectionType.Standalone, + nameFromProvider: null, + modules: [], + uoeu: 123, + lastConnection: new Date('2021-04-22T09:03:56.917Z'), + }, + { + id: 'a0db1bc8-a353-4c43-a856-b72f4811d2d4', + host: 'localhost', + port: 12000, + name: 'oea123123', + username: null, + password: null, + connectionType: ConnectionType.Standalone, + nameFromProvider: null, + modules: [], + tls: { + verifyServerCert: true, + caCertId: '70b95d32-c19d-4311-bb24-e684af12cf15', + clientCertPairId: '70b95d32-c19d-4311-b23b24-e684af12cf15', + }, + }, + { + id: 'b83a3932-e95f-4f09-9d8a-55079f400186', + host: 'localhost', + port: 5005, + name: 'sentinel', + username: null, + password: null, + connectionType: ConnectionType.Sentinel, + nameFromProvider: null, + lastConnection: new Date('2021-04-22T18:40:44.031Z'), + modules: [], + endpoints: [ + { + host: 'localhost', + port: 5005, + }, + { + host: '127.0.0.1', + port: 5006, + }, + ], + sentinelMaster: { + name: 'mymaster', + }, + }, + ]), + )) +] + +// rest.post(`${ApiEndpoints.INSTANCE}`, (req, res, ctx) => { +// const { username } = req.body + +// return res( +// ctx.json({ +// id: 'f79e82e8-c34a-4dc7-a49e-9fadc0979fda', +// username, +// firstName: 'John', +// lastName: 'Maverick', +// }), +// ) +// }), + +export default handlers diff --git a/redisinsight/ui/src/mocks/res/responseComposition.ts b/redisinsight/ui/src/mocks/res/responseComposition.ts new file mode 100644 index 0000000000..19d5b4b972 --- /dev/null +++ b/redisinsight/ui/src/mocks/res/responseComposition.ts @@ -0,0 +1,9 @@ +import { createResponseComposition, context, rest } from 'msw' +import { DEFAULT_ERROR_MESSAGE } from 'uiSrc/utils' + +export const errorHandlers = [ + rest.all('*', (_req, res, ctx) => res( + ctx.status(500), + ctx.json({ message: DEFAULT_ERROR_MESSAGE }) + )), +] diff --git a/redisinsight/ui/src/mocks/server.ts b/redisinsight/ui/src/mocks/server.ts new file mode 100644 index 0000000000..ada76a1ced --- /dev/null +++ b/redisinsight/ui/src/mocks/server.ts @@ -0,0 +1,5 @@ +import { setupServer } from 'msw/node' +import { handlers } from './handlers' + +// Setup requests interception using the given handlers. +export const mswServer = setupServer(...handlers) diff --git a/redisinsight/ui/src/packages/redistimeseries-app/src/response.ts b/redisinsight/ui/src/packages/redistimeseries-app/src/response.ts index f1ca6732b9..bb0cc4edad 100644 --- a/redisinsight/ui/src/packages/redistimeseries-app/src/response.ts +++ b/redisinsight/ui/src/packages/redistimeseries-app/src/response.ts @@ -1,3 +1,4 @@ +/* eslint-disable */ export const response1 = { "query": "TS.MRANGE - + COUNT 50 FILTER metric=cpu", "result": [ diff --git a/redisinsight/ui/src/pages/analytics/AnalyticsPage.spec.tsx b/redisinsight/ui/src/pages/analytics/AnalyticsPage.spec.tsx new file mode 100644 index 0000000000..88a3081a03 --- /dev/null +++ b/redisinsight/ui/src/pages/analytics/AnalyticsPage.spec.tsx @@ -0,0 +1,28 @@ +import { cloneDeep } from 'lodash' +import React from 'react' +import { BrowserRouter } from 'react-router-dom' +import { instance, mock } from 'ts-mockito' + +import { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils' +import AnalyticsPage, { Props } from './AnalyticsPage' + +const mockedProps = mock() + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('AnalyticsPage', () => { + it('should render', () => { + expect( + render( + + + + ) + ).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/analytics/AnalyticsPage.tsx b/redisinsight/ui/src/pages/analytics/AnalyticsPage.tsx new file mode 100644 index 0000000000..ddb91161fd --- /dev/null +++ b/redisinsight/ui/src/pages/analytics/AnalyticsPage.tsx @@ -0,0 +1,33 @@ +import React, { useEffect } from 'react' +import { useSelector } from 'react-redux' +import { useHistory, useParams, useLocation } from 'react-router-dom' +import { Pages } from 'uiSrc/constants' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { ConnectionType } from 'uiSrc/slices/interfaces' + +import AnalyticsPageRouter from './AnalyticsPageRouter' + +export interface Props { + routes: any[]; +} + +const AnalyticsPage = ({ routes = [] }: Props) => { + const history = useHistory() + const { instanceId } = useParams<{ instanceId: string }>() + const { pathname } = useLocation() + const { connectionType } = useSelector(connectedInstanceSelector) + + useEffect(() => { + if (pathname === Pages.analytics(instanceId)) { + history.push(connectionType === ConnectionType.Cluster + ? Pages.clusterDetails(instanceId) + : Pages.slowLog(instanceId)) + } + }, [connectionType, instanceId, pathname]) + + return ( + + ) +} + +export default AnalyticsPage diff --git a/redisinsight/ui/src/pages/analytics/AnalyticsPageRouter.spec.tsx b/redisinsight/ui/src/pages/analytics/AnalyticsPageRouter.spec.tsx new file mode 100644 index 0000000000..c9c68c1084 --- /dev/null +++ b/redisinsight/ui/src/pages/analytics/AnalyticsPageRouter.spec.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { render } from 'uiSrc/utils/test-utils' +import AnalyticsPageRouter from './AnalyticsPageRouter' + +const mockedRoutes = [ + { + path: '/slowlog', + }, +] + +describe('AnalyticsPageRouter', () => { + it('should render', () => { + expect( + render(, { withRouter: true }) + ).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/analytics/AnalyticsPageRouter.tsx b/redisinsight/ui/src/pages/analytics/AnalyticsPageRouter.tsx new file mode 100644 index 0000000000..ce61918b20 --- /dev/null +++ b/redisinsight/ui/src/pages/analytics/AnalyticsPageRouter.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { Switch } from 'react-router-dom' +import RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes' + +export interface Props { + routes: any[]; +} +const InstancePageRouter = ({ routes }: Props) => ( + + {routes.map((route, i) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + +) + +export default React.memo(InstancePageRouter) diff --git a/redisinsight/ui/src/pages/analytics/index.ts b/redisinsight/ui/src/pages/analytics/index.ts new file mode 100644 index 0000000000..6bc97d44b0 --- /dev/null +++ b/redisinsight/ui/src/pages/analytics/index.ts @@ -0,0 +1,6 @@ +import AnalyticsPage from './AnalyticsPage' +import AnalyticsPageRouter from './AnalyticsPageRouter' + +export { AnalyticsPage, AnalyticsPageRouter } + +export default AnalyticsPage diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.spec.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.spec.tsx index 21cda37a44..8dcc1bb49b 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.spec.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import { render } from 'uiSrc/utils/test-utils' import { instance, mock } from 'ts-mockito' import AddKeyStream, { Props } from './AddKeyStream' diff --git a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.tsx b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.tsx index 83a619dab3..c538289436 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.tsx +++ b/redisinsight/ui/src/pages/browser/components/add-key/AddKeyStream/AddKeyStream.tsx @@ -1,4 +1,3 @@ -import { map } from 'lodash' import React, { FormEvent, useEffect, useState } from 'react' import { useDispatch } from 'react-redux' import { diff --git a/redisinsight/ui/src/pages/browser/components/add-key/styles.module.scss b/redisinsight/ui/src/pages/browser/components/add-key/styles.module.scss index bd619e9e4a..9202946597 100644 --- a/redisinsight/ui/src/pages/browser/components/add-key/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/add-key/styles.module.scss @@ -42,7 +42,7 @@ color: var(--euiTextSubduedColor); display: block; margin-bottom: 12px; - font: normal normal normal 14px/24px Graphik; + font: normal normal normal 14px/24px Graphik, sans-serif; } .closeKeyTooltip { diff --git a/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx b/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx index 5da6be54a5..6248e142a9 100644 --- a/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx +++ b/redisinsight/ui/src/pages/browser/components/auto-refresh/AutoRefresh.tsx @@ -244,4 +244,4 @@ const AutoRefresh = ({ ) } -export default AutoRefresh +export default React.memo(AutoRefresh) diff --git a/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx b/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx index d139d21fc2..cdc265c294 100644 --- a/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx +++ b/redisinsight/ui/src/pages/browser/components/hash-details/HashDetails.tsx @@ -3,6 +3,7 @@ import cx from 'classnames' import React, { ChangeEvent, Ref, useCallback, useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { CellMeasurerCache } from 'react-virtualized' +import AutoSizer from 'react-virtualized-auto-sizer' import { hashSelector, @@ -13,6 +14,7 @@ import { updateHashValueStateSelector, updateHashFieldsAction, } from 'uiSrc/slices/browser/hash' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { formatLongName, createDeleteFieldHeader, @@ -20,11 +22,11 @@ import { Nullable, formattingBuffer, bufferToString, - isEqualBuffers, - isTextViewFormatter, bufferToSerializedFormat, stringToSerializedBufferFormat, - isNonUnicodeFormatter + isNonUnicodeFormatter, + isFormatEditable, + isEqualBuffers } from 'uiSrc/utils' import { sendEventTelemetry, TelemetryEvent, getBasedOnViewTypeEvent, getMatchType } from 'uiSrc/telemetry' import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' @@ -38,7 +40,14 @@ import { selectedKeyDataSelector, keysSelector, selectedKeySelector } from 'uiSr import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' import HelpTexts from 'uiSrc/constants/help-texts' -import { KeyTypes, OVER_RENDER_BUFFER_COUNT, TableCellAlignment, TEXT_UNPRINTABLE_CHARACTERS } from 'uiSrc/constants' +import { + KeyTypes, + OVER_RENDER_BUFFER_COUNT, + TableCellAlignment, + TEXT_INVALID_VALUE, + TEXT_DISABLED_FORMATTER_EDITING, + TEXT_UNPRINTABLE_CHARACTERS +} from 'uiSrc/constants' import { getColumnWidth } from 'uiSrc/components/virtual-grid' import { StopPropagation } from 'uiSrc/components/virtual-table' import { stringToBuffer } from 'uiSrc/utils/formatters/bufferFormatters' @@ -55,16 +64,13 @@ const suffix = '_hash' const matchAllValue = '*' const headerHeight = 60 const rowHeight = 43 -const APPROXIMATE_WIDTH_OF_SIGN = 8.3 const cellCache = new CellMeasurerCache({ fixedWidth: true, minHeight: rowHeight, }) -interface IHashField extends HashFieldDto { - editing: boolean; -} +interface IHashField extends HashFieldDto {} export interface Props { isFooterOpen: boolean @@ -88,6 +94,7 @@ const HashDetails = (props: Props) => { const [match, setMatch] = useState>(matchAllValue) const [deleting, setDeleting] = useState('') const [fields, setFields] = useState([]) + const [editingIndex, setEditingIndex] = useState>(null) const [width, setWidth] = useState(100) const [expandedRows, setExpandedRows] = useState([]) const [viewFormat, setViewFormat] = useState(viewFormatProp) @@ -100,9 +107,7 @@ const HashDetails = (props: Props) => { const dispatch = useDispatch() useEffect(() => { - const hashFields = loadedFields.map(formatItem) - - setFields(hashFields) + setFields(loadedFields) if (loadedFields.length < fields.length) { formattedLastIndexRef.current = 0 @@ -111,15 +116,17 @@ const HashDetails = (props: Props) => { if (viewFormat !== viewFormatProp) { setExpandedRows([]) setViewFormat(viewFormatProp) + setEditingIndex(null) - cellCache.clearAll() - setTimeout(() => { - cellCache.clearAll() - forceUpdate({}) - }, 0) + clearCache() } }, [loadedFields, viewFormatProp]) + const clearCache = () => setTimeout(() => { + cellCache.clearAll() + forceUpdate({}) + }, 0) + const closePopover = useCallback(() => { setDeleting('') }, []) @@ -148,21 +155,28 @@ const HashDetails = (props: Props) => { closePopover() } - const handleEditField = useCallback((field = '', editing: boolean) => { - setFields((prevFields) => prevFields.map((item) => { - if (isEqualBuffers(item.field, field)) { - const value = bufferToSerializedFormat(viewFormat, item.value, 4) - setAreaValue(value) - return { ...item, editing } - } - return item - })) + const handleEditField = useCallback(( + rowIndex: Nullable = null, + editing: boolean, + valueItem?: RedisResponseBuffer + ) => { + setEditingIndex(editing ? rowIndex : null) + + if (editing) { + const value = bufferToSerializedFormat(viewFormat, valueItem, 4) + setAreaValue(value) + + setTimeout(() => { + textAreaRef?.current?.focus() + }, 0) + } + // hack to update scrollbar padding + clearCache() setTimeout(() => { - cellCache.clearAll() - forceUpdate({}) + clearCache() }, 0) - }, [cellCache, viewFormat]) + }, [viewFormat]) const handleApplyEditField = (field = '') => { const data: AddFieldsToHashDto = { @@ -239,7 +253,7 @@ const HashDetails = (props: Props) => { if (nextCursor !== 0) { dispatch( fetchMoreHashFields( - key, + key as RedisResponseBuffer, nextCursor, SCAN_COUNT_DEFAULT, match || matchAllValue @@ -248,11 +262,12 @@ const HashDetails = (props: Props) => { } } - const formatItem = useCallback(({ field, value }: HashFieldDto): IHashField => ({ - field, - value, - editing: false - }), [viewFormatProp]) + const updateTextAreaHeight = () => { + if (textAreaRef.current) { + textAreaRef.current.style.height = '0px' + textAreaRef.current.style.height = `${textAreaRef.current?.scrollHeight || 0}px` + } + } const columns: ITableColumn[] = [ { @@ -282,7 +297,7 @@ const HashDetails = (props: Props) => { position="bottom" content={tooltipContent} > - <>{value.substring?.(0, 200) ?? value} + <>{value?.substring?.(0, 200) ?? value} )} {expanded && value} @@ -298,8 +313,9 @@ const HashDetails = (props: Props) => { alignment: TableCellAlignment.Left, render: function Value( _name: string, - { field: fieldItem, value: valueItem, editing }: IHashField, + { field: fieldItem, value: valueItem }: IHashField, expanded?: boolean, + rowIndex = 0 ) { // Better to cut the long string, because it could affect virtual scroll performance const value = bufferToString(valueItem) @@ -307,51 +323,65 @@ const HashDetails = (props: Props) => { const tooltipContent = formatLongName(value) const { value: formattedValue, isValid } = formattingBuffer(valueItem, viewFormatProp, { expanded }) - if (editing) { - const text = areaValue - const calculatedBreaks = text?.split('\n').length - const textAreaWidth = textAreaRef.current?.clientWidth ?? 0 - const OneRowLength = textAreaWidth / APPROXIMATE_WIDTH_OF_SIGN - const approximateLinesByLength = isTextViewFormatter(viewFormat) ? text?.length / OneRowLength : 0 - const calculatedRows = Math.round(approximateLinesByLength + calculatedBreaks) - const disabled = !isEqualBuffers(valueItem, stringToBuffer(value)) - && !isNonUnicodeFormatter(viewFormat) + if (rowIndex === editingIndex) { + const disabled = !isNonUnicodeFormatter(viewFormat, isValid) + && !isEqualBuffers(valueItem, stringToBuffer(value)) + + setTimeout(() => cellCache.clear(rowIndex, 1), 0) + updateTextAreaHeight() return ( - - handleEditField(fieldItem, false)} - onApply={() => handleApplyEditField(fieldItem)} - > - ) => { - cellCache.clearAll() - setAreaValue(e.target.value) - }} - disabled={updateLoading} - inputRef={textAreaRef} - className={cx(styles.textArea, { [styles.areaWarning]: disabled })} - data-testid="hash-value-editor" - /> - - + setTimeout(updateTextAreaHeight, 0)}> + {({ width }) => ( +
+ + handleEditField(rowIndex, false)} + onApply={() => handleApplyEditField(fieldItem)} + approveText={TEXT_INVALID_VALUE} + approveByValidation={() => + formattingBuffer( + stringToSerializedBufferFormat(viewFormat, areaValue), + viewFormat + )?.isValid} + > + ) => { + cellCache.clearAll() + setAreaValue(e.target.value) + updateTextAreaHeight() + }} + disabled={updateLoading} + inputRef={textAreaRef} + className={cx(styles.textArea, { [styles.areaWarning]: disabled })} + spellCheck={false} + data-testid="hash-value-editor" + style={{ height: textAreaRef.current?.scrollHeight || 0 }} + /> + + +
+ )} +
) } return ( @@ -368,7 +398,7 @@ const HashDetails = (props: Props) => { content={tooltipContent} anchorClassName="truncateText" > - <>{formattedValue.substring?.(0, 200) ?? formattedValue} + <>{formattedValue?.substring?.(0, 200) ?? formattedValue} )} {expanded && formattedValue} @@ -385,20 +415,23 @@ const HashDetails = (props: Props) => { absoluteWidth: 95, minWidth: 95, maxWidth: 95, - render: function Actions(_act: any, { field: fieldItem }: HashFieldDto) { + render: function Actions(_act: any, { field: fieldItem, value: valueItem }: HashFieldDto, _, rowIndex?: number) { const field = bufferToString(fieldItem, viewFormat) + const isEditable = isFormatEditable(viewFormat) return (
- handleEditField(fieldItem, true)} - data-testid={`edit-hash-button-${field}`} - /> + + handleEditField(rowIndex, true, valueItem)} + data-testid={`edit-hash-button-${field}`} + /> + ) - const Actions = (width: number) => ( - <> - {KEY_TYPES_ACTIONS[keyType] && 'addItems' in KEY_TYPES_ACTIONS[keyType] && ( - MIDDLE_SCREEN_RESOLUTION ? '' : KEY_TYPES_ACTIONS[keyType].addItems?.name} - position="left" - anchorClassName={cx(styles.actionBtn, { [styles.withText]: width > MIDDLE_SCREEN_RESOLUTION })} - > - <> - {width > MIDDLE_SCREEN_RESOLUTION ? ( - - {KEY_TYPES_ACTIONS[keyType].addItems?.name} - - ) : ( - - )} - - - )} - {keyType === KeyTypes.Stream && ( - MIDDLE_SCREEN_RESOLUTION ? '' : STREAM_ADD_ACTION[streamViewType].name} - position="left" - anchorClassName={cx(styles.actionBtn, { [styles.withText]: width > MIDDLE_SCREEN_RESOLUTION })} - > - <> - {width > MIDDLE_SCREEN_RESOLUTION ? ( - - {STREAM_ADD_ACTION[streamViewType].name} - - ) : ( + const Actions = (width: number) => { + const isEditable = isFormatEditable(viewFormatProp) + return ( + <> + {KEY_TYPES_ACTIONS[keyType] && 'addItems' in KEY_TYPES_ACTIONS[keyType] && ( + MIDDLE_SCREEN_RESOLUTION ? '' : KEY_TYPES_ACTIONS[keyType].addItems?.name} + position="left" + anchorClassName={cx(styles.actionBtn, { [styles.withText]: width > MIDDLE_SCREEN_RESOLUTION })} + > + <> + {width > MIDDLE_SCREEN_RESOLUTION ? ( + + {KEY_TYPES_ACTIONS[keyType].addItems?.name} + + ) : ( + + )} + + + )} + {keyType === KeyTypes.Stream && ( + MIDDLE_SCREEN_RESOLUTION ? '' : STREAM_ADD_ACTION[streamViewType].name} + position="left" + anchorClassName={cx(styles.actionBtn, { [styles.withText]: width > MIDDLE_SCREEN_RESOLUTION })} + > + <> + {width > MIDDLE_SCREEN_RESOLUTION ? ( + + {STREAM_ADD_ACTION[streamViewType].name} + + ) : ( + + )} + + + )} + {KEY_TYPES_ACTIONS[keyType] && 'removeItems' in KEY_TYPES_ACTIONS[keyType] && ( + + + + )} + {KEY_TYPES_ACTIONS[keyType] && 'editItem' in KEY_TYPES_ACTIONS[keyType] && ( +
+ - )} - - - )} - {KEY_TYPES_ACTIONS[keyType] && 'removeItems' in KEY_TYPES_ACTIONS[keyType] && ( - - - - )} - {KEY_TYPES_ACTIONS[keyType] && 'editItem' in KEY_TYPES_ACTIONS[keyType] && ( -
- -
- )} - - ) + +
+ )} + + ) + } return (
diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.spec.tsx index fca43ba269..982cbc33a8 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.spec.tsx @@ -1,7 +1,6 @@ import React from 'react' import { mock } from 'ts-mockito' -import { KeyTypes } from 'uiSrc/constants' -import { render, screen } from 'uiSrc/utils/test-utils' +import { render } from 'uiSrc/utils/test-utils' import KeyValueFormatter, { Props } from './KeyValueFormatter' diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.tsx b/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.tsx index a500277926..953410aaab 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/KeyValueFormatter.tsx @@ -1,3 +1,4 @@ +import cx from 'classnames' import React, { useContext, useEffect, useState } from 'react' import { EuiIcon, EuiSuperSelect, EuiSuperSelectOption, EuiText, EuiTextColor, EuiToolTip } from '@elastic/eui' import { useDispatch, useSelector } from 'react-redux' @@ -56,7 +57,7 @@ const KeyValueFormatter = (props: Props) => { ), - dropdownDisplay: {text}, + dropdownDisplay: {text}, 'data-test-subj': `format-option-${value}`, }) ) @@ -88,16 +89,19 @@ const KeyValueFormatter = (props: Props) => { } return ( -
- onChangeType(value)} - data-testid="select-format-key-value" - /> +
MIDDLE_SCREEN_RESOLUTION })}> +
+ onChangeType(value)} + data-testid="select-format-key-value" + /> +
) } diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/constants.ts b/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/constants.ts index 7183eee60a..86068f8894 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/constants.ts +++ b/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/constants.ts @@ -9,6 +9,14 @@ export const KEY_VALUE_FORMATTER_OPTIONS = [ text: 'ASCII', value: KeyValueFormat.ASCII, }, + { + text: 'Binary', + value: KeyValueFormat.Binary, + }, + { + text: 'HEX', + value: KeyValueFormat.HEX, + }, { text: 'JSON', value: KeyValueFormat.JSON, @@ -18,10 +26,20 @@ export const KEY_VALUE_FORMATTER_OPTIONS = [ value: KeyValueFormat.Msgpack, }, { - text: 'HEX', - iconDark: 'kqlSelector', - iconLight: 'kqlSelector', - value: KeyValueFormat.HEX, + text: 'Pickle', + value: KeyValueFormat.Pickle, + }, + { + text: 'Protobuf', + value: KeyValueFormat.Protobuf, + }, + { + text: 'PHP serialized', + value: KeyValueFormat.PHP, + }, + { + text: 'Java serialized', + value: KeyValueFormat.JAVA, }, ] @@ -31,5 +49,5 @@ export const getKeyValueFormatterOptions = (viewFormat?: KeyTypes | ModulesKeyTy viewFormat !== KeyTypes.ReJSON ? [...KEY_VALUE_FORMATTER_OPTIONS] : [...KEY_VALUE_FORMATTER_OPTIONS].filter((option) => - KEY_VALUE_JSON_FORMATTER_OPTIONS.indexOf(option.value) !== -1) + (KEY_VALUE_JSON_FORMATTER_OPTIONS as Array).indexOf(option.value) !== -1) ) diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/styles.module.scss index 1c960a5cc6..9b0ed537a7 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/key-details-header/components/Formatter/styles.module.scss @@ -1,8 +1,10 @@ .container { - margin-right: 6px; + margin-right: 12px; height: 30px; border-radius: 4px; transition: transform 0.3s ease; + width: 92px; + overflow: hidden; &:hover { transform: translateY(-1px); @@ -15,6 +17,28 @@ :global(.euiFormControlLayout) { height: 100%; } + + .selectWrapper { + width: 142px; + position: absolute; + + :global(.euiFormControlLayout__childrenWrapper) { + width: 92px; + } + } + + &:not(.fullWidth) { + width: 46px; + + :global(.euiFormControlLayout__childrenWrapper) { + width: 46px; + } + } + + .optionText { + overflow: hidden; + text-overflow: ellipsis; + } } .changeView:global(.euiSuperSelectControl) { @@ -36,6 +60,19 @@ } } +.formatType { + margin-top: 3px; + margin-bottom: 3px; + padding: 6px !important; + min-height: 36px !important; + + :global(.euiContextMenu__icon) { + height: 14px; + width: 14px; + margin-right: 6px; + } +} + .optionText { padding-left: 6px; padding-right: 4px; diff --git a/redisinsight/ui/src/pages/browser/components/key-details-header/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-details-header/styles.module.scss index 9673c3c775..2a72505c94 100644 --- a/redisinsight/ui/src/pages/browser/components/key-details-header/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/key-details-header/styles.module.scss @@ -179,7 +179,9 @@ } .actionBtn { - margin-right: 8px; + margin-right: 12px; + position: relative; + z-index: 2; &.withText { color: var(--euiTextSubduedColor) !important; @@ -199,7 +201,7 @@ .refreshSummary { color: var(--euiColorMediumShade) !important; - font: normal normal normal 12px/18px Graphik; + font: normal normal normal 12px/18px Graphik, sans-serif; padding-bottom: 2px; margin-right: 4px; } diff --git a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.spec.tsx index 64962e75e8..59b8ac45e4 100644 --- a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.spec.tsx @@ -1,6 +1,7 @@ import React from 'react' import { render } from 'uiSrc/utils/test-utils' -import { KeysStoreData } from 'uiSrc/slices/interfaces/keys' +import { KeysStoreData, KeyViewType } from 'uiSrc/slices/interfaces/keys' +import { keysSelector, setLastBatchKeys } from 'uiSrc/slices/browser/keys' import KeyList from './KeyList' const propsMock = { @@ -41,6 +42,20 @@ const propsMock = { handleAddKeyPanel: jest.fn(), } +jest.mock('uiSrc/slices/browser/keys', () => ({ + ...jest.requireActual('uiSrc/slices/browser/keys'), + setLastBatchKeys: jest.fn(), + keysSelector: jest.fn().mockResolvedValue({ + viewType: 'Browser', + isSearch: false, + isFiltered: false, + }), +})) + +afterEach(() => { + setLastBatchKeys.mockRestore() +}) + describe('KeyList', () => { it('should render', () => { expect(render()).toBeTruthy() @@ -53,4 +68,34 @@ describe('KeyList', () => { ) expect(rows).toHaveLength(3) }) + + it('should call "setLastBatchKeys" after unmount for Browser view', () => { + keysSelector.mockImplementation(() => ({ + viewType: KeyViewType.Browser, + isSearch: false, + isFiltered: false, + })) + + const { unmount } = render() + expect(setLastBatchKeys).not.toBeCalled() + + unmount() + + expect(setLastBatchKeys).toBeCalledTimes(1) + }) + + it('should not call "setLastBatchKeys" after unmount for Tree view', () => { + keysSelector.mockImplementation(() => ({ + viewType: KeyViewType.Tree, + isSearch: false, + isFiltered: false, + })) + + const { unmount } = render() + expect(setLastBatchKeys).not.toBeCalled() + + unmount() + + expect(setLastBatchKeys).not.toBeCalled() + }) }) diff --git a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx index b42d8b3354..6ed6924bc6 100644 --- a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx @@ -1,6 +1,7 @@ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import cx from 'classnames' +import { useParams } from 'react-router-dom' import { EuiText, @@ -9,7 +10,7 @@ import { } from '@elastic/eui' import { formatBytes, - truncateTTLToDuration, + truncateNumberToDuration, truncateNumberToFirstUnit, truncateTTLToSeconds, replaceSpaces, @@ -27,6 +28,7 @@ import { keysDataSelector, keysSelector, selectedKeySelector, + setLastBatchKeys, sourceKeysFetch, } from 'uiSrc/slices/browser/keys' import { @@ -35,11 +37,12 @@ import { } from 'uiSrc/slices/app/context' import { GroupBadge } from 'uiSrc/components' import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' -import { KeysStoreData } from 'uiSrc/slices/interfaces/keys' +import { KeysStoreData, KeyViewType } from 'uiSrc/slices/interfaces/keys' import VirtualTable from 'uiSrc/components/virtual-table/VirtualTable' import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' -import { OVER_RENDER_BUFFER_COUNT, TableCellAlignment, TableCellTextAlignment } from 'uiSrc/constants' +import { OVER_RENDER_BUFFER_COUNT, Pages, TableCellAlignment, TableCellTextAlignment } from 'uiSrc/constants' import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' +import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { GetKeyInfoResponse } from 'apiSrc/modules/browser/dto' import styles from './styles.module.scss' @@ -60,9 +63,11 @@ const KeyList = forwardRef((props: Props, ref) => { let wheelTimer = 0 const { selectKey, loadMoreItems, loading, keysState, hideFooter } = props + const { instanceId = '' } = useParams<{ instanceId: string }>() + const { data: selectedKey } = useSelector(selectedKeySelector) const { total, nextCursor, previousResultCount } = useSelector(keysDataSelector) - const { isSearched, isFiltered } = useSelector(keysSelector) + const { isSearched, isFiltered, viewType } = useSelector(keysSelector) const { keyList: { scrollTopPosition } } = useSelector(appContextBrowser) const [items, setItems] = useState(keysState.keys) @@ -77,6 +82,17 @@ const KeyList = forwardRef((props: Props, ref) => { } })) + useEffect(() => + () => { + if (viewType === KeyViewType.Tree) { + return + } + setItems((prevItems) => { + dispatch(setLastBatchKeys(prevItems.slice(-SCAN_COUNT_DEFAULT))) + return [] + }) + }, []) + useEffect(() => { const newKeys = bufferFormatRangeItems(keysState.keys, 0, OVER_RENDER_BUFFER_COUNT, formatItem) @@ -87,14 +103,30 @@ const KeyList = forwardRef((props: Props, ref) => { setItems(newKeys) }, [keysState.keys]) + const onNoKeysLinkClick = () => { + sendEventTelemetry({ + event: getBasedOnViewTypeEvent( + viewType, + TelemetryEvent.BROWSER_WORKBENCH_LINK_CLICKED, + TelemetryEvent.TREE_VIEW_WORKBENCH_LINK_CLICKED + ), + eventData: { + databaseId: instanceId, + } + }) + } + const getNoItemsMessage = () => { + if (total === 0) { + return NoKeysToDisplayText(Pages.workbench(instanceId), onNoKeysLinkClick) + } if (isSearched) { return keysState.scanned < total ? ScanNoResultsFoundText : FullScanNoResultsFoundText } if (isFiltered && keysState.scanned < total) { return ScanNoResultsFoundText } - return total ? NoResultsFoundText : NoKeysToDisplayText + return NoResultsFoundText } const onLoadMoreItems = (props: { startIndex: number, stopIndex: number }) => { @@ -177,8 +209,8 @@ const KeyList = forwardRef((props: Props, ref) => { { id: 'ttl', label: 'TTL', - absoluteWidth: 70, - minWidth: 70, + absoluteWidth: 86, + minWidth: 86, truncateText: true, alignment: TableCellAlignment.Right, render: (cellData: number, { nameString: name }: GetKeyInfoResponse) => { @@ -201,7 +233,7 @@ const KeyList = forwardRef((props: Props, ref) => { <> {`${truncateTTLToSeconds(cellData)} s`}
- {`(${truncateTTLToDuration(cellData)})`} + {`(${truncateNumberToDuration(cellData)})`} )} > @@ -215,8 +247,8 @@ const KeyList = forwardRef((props: Props, ref) => { { id: 'size', label: 'Size', - absoluteWidth: 100, - minWidth: 100, + absoluteWidth: 84, + minWidth: 84, alignment: TableCellAlignment.Right, textAlignment: TableCellTextAlignment.Right, render: (cellData: number, { nameString: name }: GetKeyInfoResponse) => { diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx index d7e2821911..79c5e42721 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react' +import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState, useTransition } from 'react' import cx from 'classnames' import { EuiResizableContainer } from '@elastic/eui' import { useDispatch, useSelector } from 'react-redux' @@ -41,6 +41,8 @@ const KeyTree = forwardRef((props: Props, ref) => { const { delimiter, panelSizes, openNodes, selectedLeaf } = useSelector(appContextBrowserTree) + const [,startTransition] = useTransition() + const [statusSelected, setStatusSelected] = useState(selectedLeaf) const [statusOpen, setStatusOpen] = useState(openNodes) const [sizes, setSizes] = useState(panelSizes) @@ -92,8 +94,10 @@ const KeyTree = forwardRef((props: Props, ref) => { const updateSelectedKeys = () => { setItems(parseKeyNames(keysState.keys)) setTimeout(() => { - setStatusSelected({}) - setSelectDefaultLeaf(true) + startTransition(() => { + setStatusSelected({}) + setSelectDefaultLeaf(true) + }) }, 0) } diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx index dc92318733..2e0d6edf95 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx @@ -93,4 +93,4 @@ const KeyTreeDelimiter = ({ loading }: Props) => { ) } -export default KeyTreeDelimiter +export default React.memo(KeyTreeDelimiter) diff --git a/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx b/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx index d3df71a276..fcdb91c533 100644 --- a/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx +++ b/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx @@ -294,4 +294,4 @@ const KeysHeader = (props: Props) => { ) } -export default KeysHeader +export default React.memo(KeysHeader) diff --git a/redisinsight/ui/src/pages/browser/components/list-details/ListDetails.tsx b/redisinsight/ui/src/pages/browser/components/list-details/ListDetails.tsx index 6f58f1111e..53c790b861 100644 --- a/redisinsight/ui/src/pages/browser/components/list-details/ListDetails.tsx +++ b/redisinsight/ui/src/pages/browser/components/list-details/ListDetails.tsx @@ -2,8 +2,9 @@ import { EuiButtonIcon, EuiProgress, EuiText, EuiTextArea, EuiToolTip } from '@e import React, { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import cx from 'classnames' -import { isEqual, isNull } from 'lodash' +import { isNull } from 'lodash' import { CellMeasurerCache } from 'react-virtualized' +import AutoSizer from 'react-virtualized-auto-sizer' import { listSelector, @@ -20,19 +21,28 @@ import { } from 'uiSrc/components/virtual-table/interfaces' import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { sendEventTelemetry, TelemetryEvent, getBasedOnViewTypeEvent } from 'uiSrc/telemetry' -import { KeyTypes, OVER_RENDER_BUFFER_COUNT, TableCellAlignment, TEXT_UNPRINTABLE_CHARACTERS } from 'uiSrc/constants' +import { + KeyTypes, + OVER_RENDER_BUFFER_COUNT, + TableCellAlignment, + TEXT_INVALID_VALUE, + TEXT_DISABLED_FORMATTER_EDITING, + TEXT_UNPRINTABLE_CHARACTERS +} from 'uiSrc/constants' import { bufferToSerializedFormat, bufferToString, formatLongName, formattingBuffer, + isFormatEditable, isNonUnicodeFormatter, isEqualBuffers, - isTextViewFormatter, stringToBuffer, stringToSerializedBufferFormat, - validateListIndex + validateListIndex, + Nullable } from 'uiSrc/utils' import { selectedKeyDataSelector, keysSelector, selectedKeySelector } from 'uiSrc/slices/browser/keys' import { NoResultsFoundText } from 'uiSrc/constants/texts' @@ -44,22 +54,20 @@ import { SetListElementDto, SetListElementResponse, } from 'apiSrc/modules/browser/dto' + import styles from './styles.module.scss' const headerHeight = 60 const rowHeight = 43 const footerHeight = 0 const initSearchingIndex = null -const APPROXIMATE_WIDTH_OF_SIGN = 8.3 const cellCache = new CellMeasurerCache({ fixedWidth: true, minHeight: rowHeight, }) -interface IListElement extends SetListElementResponse { - editing: boolean; -} +interface IListElement extends SetListElementResponse {} interface Props { isFooterOpen: boolean @@ -80,6 +88,7 @@ const ListDetails = (props: Props) => { const [elements, setElements] = useState([]) const [width, setWidth] = useState(100) const [expandedRows, setExpandedRows] = useState([]) + const [editingIndex, setEditingIndex] = useState>(null) const [viewFormat, setViewFormat] = useState(viewFormatProp) const [areaValue, setAreaValue] = useState('') const [, forceUpdate] = useState({}) @@ -90,9 +99,7 @@ const ListDetails = (props: Props) => { const dispatch = useDispatch() useEffect(() => { - const listElements = loadedElements.map(formatItem) - - setElements(listElements) + setElements(loadedElements) if (loadedElements.length < elements.length) { formattedLastIndexRef.current = 0 @@ -101,40 +108,39 @@ const ListDetails = (props: Props) => { if (viewFormat !== viewFormatProp) { setExpandedRows([]) setViewFormat(viewFormatProp) + setEditingIndex(null) - cellCache.clearAll() - setTimeout(() => { - cellCache.clearAll() - forceUpdate({}) - }, 0) + clearCache() } }, [loadedElements, viewFormatProp]) - const formatItem = useCallback(({ index, element }: IListElement): IListElement => ({ - index: searchedIndex ?? index, - editing: false, - element - }), [viewFormatProp]) - - const handleEditElement = (index = 0, editing: boolean) => { - const newElemsState = elements.map((item) => { - if (item.index === index) { - const value = bufferToSerializedFormat(viewFormat, item.element, 4) - setAreaValue(value) - return { ...item, editing } - } - return item - }) + const clearCache = () => setTimeout(() => { + cellCache.clearAll() + forceUpdate({}) + }, 0) + + const handleEditElement = useCallback(( + index: Nullable = null, + editing: boolean, + valueItem?: RedisResponseBuffer + ) => { + setEditingIndex(editing ? index : null) + + if (editing) { + const value = bufferToSerializedFormat(viewFormat, valueItem, 4) + setAreaValue(value) - if (!isEqual(elements, newElemsState)) { - setElements(newElemsState) + setTimeout(() => { + textAreaRef?.current?.focus() + }, 0) } + // hack to update scrollbar padding + clearCache() setTimeout(() => { - cellCache.clearAll() - forceUpdate({}) + clearCache() }, 0) - } + }, [cellCache, viewFormat]) const handleApplyEditElement = (index = 0) => { const data: SetListElementDto = { @@ -206,6 +212,13 @@ const ListDetails = (props: Props) => { cellCache.clearAll() } + const updateTextAreaHeight = () => { + if (textAreaRef.current) { + textAreaRef.current.style.height = '0px' + textAreaRef.current.style.height = `${textAreaRef.current?.scrollHeight || 0}px` + } + } + const columns: ITableColumn[] = [ { id: 'index', @@ -248,59 +261,74 @@ const ListDetails = (props: Props) => { alignment: TableCellAlignment.Left, render: function Element( _element: string, - { element: elementItem, index, editing }: IListElement, - expanded: boolean = false + { element: elementItem, index }: IListElement, + expanded: boolean = false, + rowIndex = 0 ) { const element = bufferToString(elementItem) const tooltipContent = formatLongName(element) const { value, isValid } = formattingBuffer(elementItem, viewFormatProp, { expanded }) - if (editing) { - const text = areaValue - const calculatedBreaks = text?.split('\n').length - const textAreaWidth = textAreaRef.current?.clientWidth ?? 0 - const OneRowLength = textAreaWidth / APPROXIMATE_WIDTH_OF_SIGN - const approximateLinesByLength = isTextViewFormatter(viewFormat) ? text?.length / OneRowLength : 0 - const calculatedRows = Math.round(approximateLinesByLength + calculatedBreaks) - const disabled = !isEqualBuffers(elementItem, stringToBuffer(element)) - && !isNonUnicodeFormatter(viewFormat) + if (index === editingIndex) { + const disabled = !isNonUnicodeFormatter(viewFormat, isValid) + && !isEqualBuffers(elementItem, stringToBuffer(element)) + + setTimeout(() => cellCache.clear(rowIndex, 1), 0) + return ( - -
- handleEditElement(index, false)} - onApply={() => handleApplyEditElement(index)} - > - ) => { - cellCache.clearAll() - setAreaValue(e.target.value) - }} - disabled={updateLoading} - inputRef={textAreaRef} - className={cx(styles.textArea, { [styles.areaWarning]: disabled })} - data-testid="element-value-editor" - /> - -
-
+ setTimeout(updateTextAreaHeight, 0)}> + {({ width }) => ( +
+ +
+ handleEditElement(index, false)} + onApply={() => handleApplyEditElement(index)} + approveText={TEXT_INVALID_VALUE} + approveByValidation={() => + formattingBuffer( + stringToSerializedBufferFormat(viewFormat, areaValue), + viewFormat + )?.isValid} + > + ) => { + cellCache.clearAll() + setAreaValue(e.target.value) + updateTextAreaHeight() + }} + disabled={updateLoading} + inputRef={textAreaRef} + className={cx(styles.textArea, { [styles.areaWarning]: disabled })} + spellCheck={false} + data-testid="element-value-editor" + style={{ height: textAreaRef.current?.scrollHeight || 0 }} + /> + +
+
+
+ )} +
) } return ( @@ -317,7 +345,7 @@ const ListDetails = (props: Props) => { content={tooltipContent} anchorClassName="truncateText" > - <>{value.substring?.(0, 200) ?? value} + <>{value?.substring?.(0, 200) ?? value} )} {expanded && value} @@ -334,18 +362,22 @@ const ListDetails = (props: Props) => { minWidth: 60, maxWidth: 60, absoluteWidth: 60, - render: function Actions(_element: any, { index }: IListElement) { + render: function Actions(_element: any, { index, element }: IListElement) { + const isEditable = isFormatEditable(viewFormat) return (
- handleEditElement(index, true)} - data-testid={`edit-list-button-${index}`} - /> + + handleEditElement(index, true, element)} + data-testid={`edit-list-button-${index}`} + /> +
) diff --git a/redisinsight/ui/src/pages/browser/components/list-details/styles.module.scss b/redisinsight/ui/src/pages/browser/components/list-details/styles.module.scss index 6a2fe5b802..635d5674ee 100644 --- a/redisinsight/ui/src/pages/browser/components/list-details/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/list-details/styles.module.scss @@ -7,6 +7,8 @@ .textAreaControls { right: -56px !important; + bottom: -4px !important; + top: auto !important; background-color: var(--euiPageBackgroundColor) !important; } @@ -15,9 +17,14 @@ border-color: var(--euiColorPrimary) !important; z-index: 3; padding-left: 20px; + padding-bottom: 36px !important; margin: -8px -6px -8px -20px !important; - height: calc(100% + 16px) !important; min-width: calc(100% + 106px) !important; + font: normal normal normal 13px/18px Graphik, sans-serif; + min-height: 43px; + overflow: hidden; + overflow-wrap: break-word; + resize: none; &:focus { background-image: none !important; diff --git a/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx b/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx index 5e43045a2c..853b7ec317 100644 --- a/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx @@ -33,7 +33,7 @@ const SearchKeyList = () => { setValue(initValue) } - const handleChangeOptions = (options: string[]) => { + const handleChangeOptions = () => { // now only one filter, so we delete option dispatch(setFilter(null)) handleApply() diff --git a/redisinsight/ui/src/pages/browser/components/set-details/SetDetails.tsx b/redisinsight/ui/src/pages/browser/components/set-details/SetDetails.tsx index 9d7994e2b0..feaa97b362 100644 --- a/redisinsight/ui/src/pages/browser/components/set-details/SetDetails.tsx +++ b/redisinsight/ui/src/pages/browser/components/set-details/SetDetails.tsx @@ -7,6 +7,7 @@ import { EuiToolTip, } from '@elastic/eui' import { CellMeasurerCache } from 'react-virtualized' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { bufferToString, @@ -186,12 +187,12 @@ const SetDetails = (props: Props) => { staySearchAlwaysOpen: true, initialSearchValue: '', truncateText: true, - render: function Name(_name: string, memberItem: string, expanded: boolean = false) { + render: function Name(_name: string, memberItem: RedisResponseBuffer, expanded: boolean = false) { // Better to cut the long string, because it could affect virtual scroll performance const member = bufferToString(memberItem) const tooltipContent = formatLongName(member) const { value, isValid } = formattingBuffer(memberItem, viewFormatProp, { expanded }) - const cellContent = value.substring?.(0, 200) ?? value + const cellContent = value?.substring?.(0, 200) ?? value return ( @@ -223,7 +224,7 @@ const SetDetails = (props: Props) => { minWidth: 60, maxWidth: 60, headerClassName: 'hidden', - render: function Actions(_act: any, memberItem: string) { + render: function Actions(_act: any, memberItem: RedisResponseBuffer) { const member = bufferToString(memberItem, viewFormat) return (
diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss index 9f50b488fd..54a0af89c9 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/stream-details/groups-view/GroupsView/styles.module.scss @@ -22,7 +22,7 @@ .idText, .error { display: inline-block; color: var(--euiColorMediumShade); - font: normal normal normal 12px/18px Graphik; + font: normal normal normal 12px/18px Graphik, sans-serif; margin-top: 6px; padding-right: 6px; } diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageAckPopover/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageAckPopover/styles.module.scss index bdb3aed509..0e1333d8e9 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageAckPopover/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageAckPopover/styles.module.scss @@ -32,5 +32,5 @@ } .ackBtn :global(.euiButtonContent .euiButton__text) { - font: normal normal normal 12px/18px Graphik !important; + font: normal normal normal 12px/18px Graphik, sans-serif !important; } diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageClaimPopover/MessageClaimPopover.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageClaimPopover/MessageClaimPopover.tsx index 9ec708beb6..4ea2f77681 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageClaimPopover/MessageClaimPopover.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageClaimPopover/MessageClaimPopover.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, ChangeEvent, useCallback } from 'react' +import React, { useState, useEffect, ChangeEvent } from 'react' import { useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import { diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageClaimPopover/styles.module.scss b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageClaimPopover/styles.module.scss index 790a1196ed..ce47121b5f 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageClaimPopover/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageClaimPopover/styles.module.scss @@ -28,7 +28,7 @@ &:global(.euiFlexGroup--gutterLarge) > :global(.euiFlexItem) { margin-left: 0; } - + :global(.euiSwitch) :global(.euiSwitch__button) { width: 30px; } @@ -49,7 +49,7 @@ } .claimBtn :global(.euiButtonContent .euiButton__text) { - font: normal normal normal 12px/18px Graphik !important; + font: normal normal normal 12px/18px Graphik, sans-serif !important; } .option { @@ -61,7 +61,7 @@ } .option .pendingCount { - font: normal normal normal 13px/24px Graphik; + font: normal normal normal 13px/24px Graphik, sans-serif; letter-spacing: -0.13px; color: var(--inputPlaceholderColor) !important; white-space: nowrap; diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx index a75a47c47c..6cde093977 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-data-view/StreamDataViewWrapper.tsx @@ -5,6 +5,7 @@ import { last, mergeWith, toNumber } from 'lodash' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { + bufferToString, createDeleteFieldHeader, createDeleteFieldMessage, formatLongName, @@ -19,7 +20,6 @@ import { KeyTypes, TableCellTextAlignment } from 'uiSrc/constants' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { keysSelector, selectedKeySelector, updateSelectedKeyRefreshTime } from 'uiSrc/slices/browser/keys' -import bufferToString from 'uiSrc/utils/formatters/bufferFormatters' import { StreamEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' import StreamDataView from './StreamDataView' @@ -95,8 +95,30 @@ const StreamDataViewWrapper = (props: Props) => { id: field, label: field, render: () => { - const { value: formattedValue } = formattingBuffer(name || stringToBuffer(''), viewFormatProp) - return formattedValue || (
 
) + const value = name ? bufferToString(name) : '' + const { value: formattedValue, isValid } = formattingBuffer(name || stringToBuffer(''), viewFormatProp) + const tooltipContent = formatLongName(value) + return ( + <> + {formattedValue ? ( +
+ + <>{formattedValue} + +
+ ) : ( +
 
+ )} + + ) } } } @@ -181,7 +203,6 @@ const StreamDataViewWrapper = (props: Props) => { isSortable: false, className: styles.cell, headerClassName: 'streamItemHeader', - headerCellClassName: 'truncateText', render: function Id({ id, fields }: StreamEntryDto, expanded: boolean) { const index = toNumber(last(label.split('-'))) const values = fields.filter(({ name: fieldName }) => bufferToString(fieldName, viewFormat) === name) @@ -189,7 +210,7 @@ const StreamDataViewWrapper = (props: Props) => { const bufferValue = values[index]?.value || stringToBuffer('') const { value: formattedValue, isValid } = formattingBuffer(bufferValue, viewFormatProp, { expanded }) - const cellContent = formattedValue.substring?.(0, 650) ?? formattedValue + const cellContent = formattedValue?.substring?.(0, 650) ?? formattedValue const tooltipContent = formatLongName(value) return ( diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx index ecbc3a5e61..f9f808d870 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/stream-tabs/StreamTabs.tsx @@ -9,10 +9,14 @@ import { fetchConsumerGroups, selectedGroupSelector, selectedConsumerSelector, + fetchStreamEntries, } from 'uiSrc/slices/browser/stream' import { StreamViewType } from 'uiSrc/slices/interfaces/stream' import { ConsumerGroupDto } from 'apiSrc/modules/browser/dto/stream.dto' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' +import { SortOrder } from 'uiSrc/constants' +import { selectedKeyDataSelector } from 'uiSrc/slices/browser/keys' import { streamViewTypeTabs } from '../constants' @@ -20,6 +24,7 @@ import styles from './styles.module.scss' const StreamTabs = () => { const { viewType } = useSelector(streamSelector) + const { name: key } = useSelector(selectedKeyDataSelector) ?? { name: '' } const { nameString: selectedGroupName = '' } = useSelector(selectedGroupSelector) ?? {} const { nameString: selectedConsumerName = '' } = useSelector(selectedConsumerSelector) ?? {} @@ -38,6 +43,14 @@ const StreamTabs = () => { } const onSelectedTabChanged = (id: StreamViewType) => { + if (id === StreamViewType.Data) { + dispatch(fetchStreamEntries( + key, + SCAN_COUNT_DEFAULT, + SortOrder.DESC, + true + )) + } if (id === StreamViewType.Groups) { dispatch(fetchConsumerGroups( true, diff --git a/redisinsight/ui/src/pages/browser/components/string-details/StringDetails.tsx b/redisinsight/ui/src/pages/browser/components/string-details/StringDetails.tsx index d70fe91367..9abd861a9b 100644 --- a/redisinsight/ui/src/pages/browser/components/string-details/StringDetails.tsx +++ b/redisinsight/ui/src/pages/browser/components/string-details/StringDetails.tsx @@ -8,6 +8,7 @@ import React, { useState, } from 'react' import { useDispatch, useSelector } from 'react-redux' +import cx from 'classnames' import { EuiProgress, EuiText, EuiTextArea, EuiToolTip } from '@elastic/eui' import { @@ -16,7 +17,7 @@ import { formattingBuffer, isNonUnicodeFormatter, isEqualBuffers, - isTextViewFormatter, + isFormatEditable, stringToBuffer, stringToSerializedBufferFormat } from 'uiSrc/utils' @@ -29,13 +30,14 @@ import { import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor' import { AddStringFormConfig as config } from 'uiSrc/pages/browser/components/add-key/constants/fields-config' import { selectedKeyDataSelector, selectedKeySelector } from 'uiSrc/slices/browser/keys' +import { TEXT_INVALID_VALUE, TEXT_UNPRINTABLE_CHARACTERS } from 'uiSrc/constants' +import { calculateTextareaLines } from 'uiSrc/utils/calculateTextareaLines' -import { TEXT_UNPRINTABLE_CHARACTERS } from 'uiSrc/constants' import styles from './styles.module.scss' const MAX_ROWS = 25 const MIN_ROWS = 4 -const APPROXIMATE_WIDTH_OF_SIGN = 8.3 +const APPROXIMATE_WIDTH_OF_SIGN = 8.6 export interface Props { isEditItem: boolean; @@ -56,6 +58,7 @@ const StringDetails = (props: Props) => { const [viewFormat, setViewFormat] = useState(viewFormatProp) const [isValid, setIsValid] = useState(true) const [isDisabled, setIsDisabled] = useState(false) + const [isEditable, setIsEditable] = useState(true) const textAreaRef: Ref = useRef(null) const viewValueRef: Ref = useRef(null) @@ -76,9 +79,10 @@ const StringDetails = (props: Props) => { setValue(formattedValue) setIsValid(isValid) setIsDisabled( - !isEqualBuffers(initialValue, stringToBuffer(initialValueString)) - && !isNonUnicodeFormatter(viewFormatProp) + !isNonUnicodeFormatter(viewFormatProp, isValid) + && !isEqualBuffers(initialValue, stringToBuffer(initialValueString)) ) + setIsEditable(isFormatEditable(viewFormatProp)) if (viewFormat !== viewFormatProp) { setViewFormat(viewFormatProp) @@ -90,12 +94,7 @@ const StringDetails = (props: Props) => { if (!isEditItem || !textAreaRef.current || value === null) { return } - const text = areaValue - const calculatedBreaks = text?.split('\n').length - const textAreaWidth = textAreaRef.current.clientWidth - const OneRowLength = textAreaWidth / APPROXIMATE_WIDTH_OF_SIGN - const approximateLinesByLength = isTextViewFormatter(viewFormat) ? text?.length / OneRowLength : 0 - const calculatedRows = Math.round(approximateLinesByLength + calculatedBreaks) + const calculatedRows = calculateTextareaLines(areaValue, textAreaRef.current.clientWidth, APPROXIMATE_WIDTH_OF_SIGN) if (calculatedRows > MAX_ROWS) { setRows(MAX_ROWS) @@ -145,7 +144,7 @@ const StringDetails = (props: Props) => { )} {!isEditItem && ( setIsEdit(true)} + onClick={() => isEditable && setIsEdit(true)} style={{ whiteSpace: 'break-spaces' }} data-testid="string-value" > @@ -177,6 +176,12 @@ const StringDetails = (props: Props) => { onDecline={onDeclineChanges} onApply={onApplyChanges} declineOnUnmount={false} + approveText={TEXT_INVALID_VALUE} + approveByValidation={() => + formattingBuffer( + stringToSerializedBufferFormat(viewFormat, areaValue), + viewFormat + )?.isValid} > { }} disabled={loading} inputRef={textAreaRef} - className={styles.stringTextArea} + className={cx(styles.stringTextArea, { [styles.areaWarning]: isDisabled })} data-testid="string-value" /> diff --git a/redisinsight/ui/src/pages/browser/components/string-details/styles.module.scss b/redisinsight/ui/src/pages/browser/components/string-details/styles.module.scss index 69cffb71d9..df481309a5 100644 --- a/redisinsight/ui/src/pages/browser/components/string-details/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/string-details/styles.module.scss @@ -45,4 +45,9 @@ $outer-height-mobile: 340px; @media only screen and (max-width: 767px) { max-height: calc(100vh - #{$outer-height-mobile} - 55px); } + + &.areaWarning { + border-color: var(--euiColorWarningLight) !important; + background-image: none !important; + } } diff --git a/redisinsight/ui/src/pages/browser/components/zset-details/ZSetDetails.tsx b/redisinsight/ui/src/pages/browser/components/zset-details/ZSetDetails.tsx index 5fc64ea68f..be5df9d6c2 100644 --- a/redisinsight/ui/src/pages/browser/components/zset-details/ZSetDetails.tsx +++ b/redisinsight/ui/src/pages/browser/components/zset-details/ZSetDetails.tsx @@ -4,7 +4,6 @@ import { toNumber } from 'lodash' import cx from 'classnames' import { EuiButtonIcon, EuiProgress, EuiText, EuiToolTip } from '@elastic/eui' import { CellMeasurerCache } from 'react-virtualized' -import { RedisString } from 'src/common/constants' import { zsetSelector, @@ -22,6 +21,8 @@ import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' import HelpTexts from 'uiSrc/constants/help-texts' import { NoResultsFoundText } from 'uiSrc/constants/texts' import { selectedKeyDataSelector, keysSelector, selectedKeySelector } from 'uiSrc/slices/browser/keys' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { ZsetMember } from 'uiSrc/slices/interfaces/zset' import { bufferToString, createDeleteFieldHeader, @@ -38,7 +39,7 @@ import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEdit import { IColumnSearchState, ITableColumn } from 'uiSrc/components/virtual-table/interfaces' import { StopPropagation } from 'uiSrc/components/virtual-table' import { getColumnWidth } from 'uiSrc/components/virtual-grid' -import { AddMembersToZSetDto, SearchZSetMembersResponse, ZSetMemberDto } from 'apiSrc/modules/browser/dto' +import { AddMembersToZSetDto, SearchZSetMembersResponse } from 'apiSrc/modules/browser/dto' import { stringToBuffer } from 'uiSrc/utils/formatters/bufferFormatters' import PopoverDelete from '../popover-delete/PopoverDelete' @@ -53,7 +54,7 @@ const cellCache = new CellMeasurerCache({ minHeight: rowHeight, }) -interface IZsetMember extends ZSetMemberDto { +interface IZsetMember extends ZsetMember { editing: boolean; } @@ -136,7 +137,7 @@ const ZSetDetails = (props: Props) => { closePopover() } - const handleEditMember = (name = '', editing: boolean) => { + const handleEditMember = (name: RedisResponseBuffer, editing: boolean) => { const newMemberState = members.map((item) => { if (isEqualBuffers(item.name, name)) { return { ...item, editing } @@ -147,7 +148,7 @@ const ZSetDetails = (props: Props) => { cellCache.clearAll() } - const handleApplyEditScore = (name: RedisString, score: string = '') => { + const handleApplyEditScore = (name: RedisResponseBuffer, score: string = '') => { const data: AddMembersToZSetDto = { keyName: key, members: [{ @@ -245,7 +246,7 @@ const ZSetDetails = (props: Props) => { const name = bufferToString(nameItem) const tooltipContent = formatLongName(name) const { value, isValid } = formattingBuffer(nameItem, viewFormat, { expanded }) - const cellContent = value.substring?.(0, 200) ?? value + const cellContent = value?.substring?.(0, 200) ?? value return ( @@ -276,7 +277,6 @@ const ZSetDetails = (props: Props) => { isSortable: true, truncateText: true, render: function Score(_name: string, { name: nameItem, score, editing }: IZsetMember, expanded?: boolean) { - const name = bufferToString(nameItem, viewFormat) const cellContent = score.toString().substring(0, 200) const tooltipContent = formatLongName(score.toString()) if (editing) { @@ -289,7 +289,7 @@ const ZSetDetails = (props: Props) => { fieldName="score" expandable onDecline={() => handleEditMember(nameItem, false)} - onApply={(value) => handleApplyEditScore(nameItem, value, name)} + onApply={(value) => handleApplyEditScore(nameItem, value)} validation={validateScoreNumber} /> diff --git a/redisinsight/ui/src/pages/clusterDetails/ClusterDetailsPage.spec.tsx b/redisinsight/ui/src/pages/clusterDetails/ClusterDetailsPage.spec.tsx new file mode 100644 index 0000000000..f7c081ce28 --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/ClusterDetailsPage.spec.tsx @@ -0,0 +1,36 @@ +import { cloneDeep } from 'lodash' +import React from 'react' +import { CLUSTER_DETAILS_DATA_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers' +import { + getClusterDetails, + getClusterDetailsSuccess +} from 'uiSrc/slices/analytics/clusterDetails' +import { act, cleanup, mockedStore, render } from 'uiSrc/utils/test-utils' + +import ClusterDetailsPage from './ClusterDetailsPage' + +let store: typeof mockedStore + +describe('ClusterDetailsPage', () => { + beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() + }) + + it('should render', async () => { + await act(() => { + expect(render()) + .toBeTruthy() + }) + }) + + it('should call fetchClusterDetailsAction after rendering', async () => { + await act(() => { + render() + }) + + const expectedActions = [getClusterDetails(), getClusterDetailsSuccess(CLUSTER_DETAILS_DATA_MOCK)] + expect(store.getActions()).toEqual([...expectedActions]) + }) +}) diff --git a/redisinsight/ui/src/pages/clusterDetails/ClusterDetailsPage.tsx b/redisinsight/ui/src/pages/clusterDetails/ClusterDetailsPage.tsx new file mode 100644 index 0000000000..121729f742 --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/ClusterDetailsPage.tsx @@ -0,0 +1,127 @@ +import { orderBy } from 'lodash' +import React, { useContext, useEffect, useState } from 'react' +import { useSelector, useDispatch } from 'react-redux' +import { useParams } from 'react-router-dom' +import { ClusterNodeDetails } from 'src/modules/cluster-monitor/models' + +import InstanceHeader from 'uiSrc/components/instance-header' +import { Theme } from 'uiSrc/constants' +import { ThemeContext } from 'uiSrc/contexts/themeContext' +import { clusterDetailsSelector, fetchClusterDetailsAction } from 'uiSrc/slices/analytics/clusterDetails' +import { analyticsSettingsSelector, setAnalyticsViewTab } from 'uiSrc/slices/analytics/settings' +import { appAnalyticsInfoSelector } from 'uiSrc/slices/app/info' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics' +import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' +import { formatLongName, getDbIndex, getLetterByIndex, Nullable, setTitle, } from 'uiSrc/utils' +import { ColorScheme, getRGBColorByScheme, RGBColor } from 'uiSrc/utils/colors' + +import { ClusterDetailsHeader, ClusterDetailsGraphics, ClusterNodesTable } from './components' + +import styles from './styles.module.scss' + +export interface ModifiedClusterNodes extends ClusterNodeDetails { + letter: string + index: number + color: RGBColor +} + +const POLLING_INTERVAL = 5_000 + +const ClusterDetailsPage = () => { + let interval: NodeJS.Timeout + const { instanceId } = useParams<{ instanceId: string }>() + const { + db, + name: connectedInstanceName, + } = useSelector(connectedInstanceSelector) + const { viewTab } = useSelector(analyticsSettingsSelector) + const { identified: analyticsIdentified } = useSelector(appAnalyticsInfoSelector) + const { loading, data } = useSelector(clusterDetailsSelector) + + const [isPageViewSent, setIsPageViewSent] = useState(false) + const [nodes, setNodes] = useState>(null) + + const dispatch = useDispatch() + const { theme } = useContext(ThemeContext) + + const dbName = `${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)}` + setTitle(`${dbName} - Overview`) + + const colorScheme: ColorScheme = { + cHueStart: 180, + cHueRange: 140, + cSaturation: 55, + cLightness: theme === Theme.Dark ? 45 : 55 + } + + useEffect(() => { + dispatch(fetchClusterDetailsAction( + instanceId, + () => {}, + () => clearInterval(interval) + )) + + if (viewTab !== AnalyticsViewTab.ClusterDetails) { + dispatch(setAnalyticsViewTab(AnalyticsViewTab.ClusterDetails)) + } + }, []) + + useEffect(() => { + if (!loading) { + interval = setInterval(() => { + if (document.hidden) return + + dispatch(fetchClusterDetailsAction( + instanceId, + () => {}, + () => clearInterval(interval) + )) + }, POLLING_INTERVAL) + } + return () => clearInterval(interval) + }, [instanceId, loading]) + + useEffect(() => { + if (data) { + const nodes = orderBy(data.nodes, ['asc', 'host']) + const shift = colorScheme.cHueRange / nodes.length + const modifiedNodes = nodes.map((d, index) => ({ + ...d, + letter: getLetterByIndex(index), + index, + color: getRGBColorByScheme(index, shift, colorScheme) + })) + setNodes(modifiedNodes) + } + }, [data]) + + useEffect(() => { + if (connectedInstanceName && !isPageViewSent && analyticsIdentified) { + sendPageView(instanceId) + } + }, [connectedInstanceName, isPageViewSent, analyticsIdentified]) + + const sendPageView = (instanceId: string) => { + sendPageViewTelemetry({ + name: TelemetryPageView.CLUSTER_DETAILS_PAGE, + databaseId: instanceId + }) + setIsPageViewSent(true) + } + + return ( + <> + +
+ +
+ + +
+
+ + ) +} + +export default ClusterDetailsPage diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluser-nodes-table/ClusterNodesTable.spec.tsx b/redisinsight/ui/src/pages/clusterDetails/components/cluser-nodes-table/ClusterNodesTable.spec.tsx new file mode 100644 index 0000000000..1d33d84e73 --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluser-nodes-table/ClusterNodesTable.spec.tsx @@ -0,0 +1,118 @@ +import React from 'react' +import { ModifiedClusterNodes } from 'uiSrc/pages/clusterDetails/ClusterDetailsPage' +import { getLetterByIndex } from 'uiSrc/utils' +import { rgb } from 'uiSrc/utils/colors' +import { render, screen } from 'uiSrc/utils/test-utils' + +import ClusterNodesTable from './ClusterNodesTable' + +const mockNodes = [ + { + id: '1', + host: '0.0.0.1', + port: 6379, + role: 'primary', + slots: ['10923-16383'], + health: 'online', + totalKeys: 1, + usedMemory: 2867968, + opsPerSecond: 1, + connectionsReceived: 13, + connectedClients: 6, + commandsProcessed: 5678, + networkInKbps: 0.02, + networkOutKbps: 0, + cacheHitRatio: 1, + replicationOffset: 6924, + uptimeSec: 5614, + version: '6.2.6', + mode: 'cluster', + replicas: [] + }, + { + id: '2', + host: '0.0.0.2', + port: 6379, + role: 'primary', + slots: ['0-5460'], + health: 'online', + totalKeys: 4, + usedMemory: 2825880, + opsPerSecond: 1, + connectionsReceived: 15, + connectedClients: 4, + commandsProcessed: 5667, + networkInKbps: 0.04, + networkOutKbps: 0, + cacheHitRatio: 1, + replicationOffset: 6910, + uptimeSec: 5609, + version: '6.2.6', + mode: 'cluster', + replicas: [] + }, + { + id: '3', + host: '0.0.0.3', + port: 6379, + role: 'primary', + slots: [ + '5461-10922' + ], + health: 'online', + totalKeys: 10, + usedMemory: 2886960, + opsPerSecond: 0, + connectionsReceived: 18, + connectedClients: 7, + commandsProcessed: 5697, + networkInKbps: 0.02, + networkOutKbps: 0, + cacheHitRatio: 0, + replicationOffset: 6991, + uptimeSec: 5609, + version: '6.2.6', + mode: 'cluster', + replicas: [] + } +].map((d, index) => ({ ...d, letter: getLetterByIndex(index), index, color: [0, 0, 0] })) as ModifiedClusterNodes[] + +describe('ClusterNodesTable', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render loading content', () => { + render() + expect(screen.getByTestId('primary-nodes-table-loading')).toBeInTheDocument() + expect(screen.queryByTestId('primary-nodes-table')).not.toBeInTheDocument() + }) + + it('should render table', () => { + render() + expect(screen.getByTestId('primary-nodes-table')).toBeInTheDocument() + expect(screen.queryByTestId('primary-nodes-table-loading')).not.toBeInTheDocument() + }) + + it('should render table with 3 items', () => { + render() + expect(screen.getAllByTestId('node-letter')).toHaveLength(3) + }) + + it('should highlight max value for total keys', () => { + render() + expect(screen.getByTestId('totalKeys-value-max')).toHaveTextContent(mockNodes[2].totalKeys.toString()) + }) + + it('should not highlight max value for opsPerSecond with equals values', () => { + render() + expect(screen.queryByTestId('opsPerSecond-value-max')).not.toBeInTheDocument() + }) + + it('should render background color for each node', () => { + render() + mockNodes.forEach(({ letter, color }) => { + expect(screen.getByTestId(`node-color-${letter}`)).toHaveStyle({ 'background-color': rgb(color) }) + }) + }) +}) diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluser-nodes-table/ClusterNodesTable.tsx b/redisinsight/ui/src/pages/clusterDetails/components/cluser-nodes-table/ClusterNodesTable.tsx new file mode 100644 index 0000000000..7224fa2397 --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluser-nodes-table/ClusterNodesTable.tsx @@ -0,0 +1,203 @@ +import { + EuiBasicTableColumn, + EuiIcon, + EuiInMemoryTable, + EuiLoadingContent, + EuiToolTip, + PropertySort +} from '@elastic/eui' +import { IconType } from '@elastic/eui/src/components/icon/icon' +import cx from 'classnames' +import { map } from 'lodash' +import React, { useState } from 'react' +import { + InputIconSvg, + KeyIconSvg, + MemoryIconSvg, + OutputIconSvg, + UserIconSvg, + MeasureIconSvg +} from 'uiSrc/components/database-overview/components/icons' +import { ModifiedClusterNodes } from 'uiSrc/pages/clusterDetails/ClusterDetailsPage' +import { formatBytes, Nullable } from 'uiSrc/utils' +import { rgb } from 'uiSrc/utils/colors' +import { numberWithSpaces } from 'uiSrc/utils/numbers' + +import styles from './styles.module.scss' + +const ClusterNodesTable = ({ nodes, loading }: { nodes: Nullable, loading: boolean }) => { + const [sort, setSort] = useState({ field: 'host', direction: 'asc' }) + + const isMaxValue = (field: string, value: number) => { + const values = map(nodes, field) + return Math.max(...values) === value && values.filter((v) => v === value).length === 1 + } + + const headerIconTemplate = (label: string, icon: IconType) => ( +
+ + {label} +
+ ) + + const columns: EuiBasicTableColumn[] = [ + { + name: ( +
+ {`${nodes?.length} Primary nodes`} +
+ ), + field: 'host', + dataType: 'string', + sortable: ({ index }) => index, + render: (value: number, { letter, port, color }) => ( + <> +
+
+ {letter} + {value}:{port} +
+ + ) + }, + { + name: headerIconTemplate('Commands/s', MeasureIconSvg), + field: 'opsPerSecond', + width: '12%', + sortable: true, + align: 'right', + render: (value: number) => { + const isMax = isMaxValue('opsPerSecond', value) + return ( + + {numberWithSpaces(value)} + + ) + } + }, + { + name: headerIconTemplate('Network Input', InputIconSvg), + field: 'networkInKbps', + width: '12%', + sortable: true, + align: 'right', + render: (value: number) => { + const isMax = isMaxValue('networkInKbps', value) + return ( + <> + + {numberWithSpaces(value)} + + kb/s + + ) + } + }, + { + name: headerIconTemplate('Network Output', OutputIconSvg), + field: 'networkOutKbps', + width: '12%', + sortable: true, + align: 'right', + render: (value: number) => { + const isMax = isMaxValue('networkOutKbps', value) + return ( + <> + + {numberWithSpaces(value)} + + kb/s + + ) + } + }, + { + name: headerIconTemplate('Total Memory', MemoryIconSvg), + field: 'usedMemory', + width: '12%', + sortable: true, + align: 'right', + render: (value: number) => { + const [number, size] = formatBytes(value, 3, true) + const isMax = isMaxValue('usedMemory', value) + return ( + + <> + + {number} + + {size} + + + ) + } + }, + { + name: headerIconTemplate('Total Keys', KeyIconSvg), + field: 'totalKeys', + width: '12%', + sortable: true, + align: 'right', + render: (value: number) => { + const isMax = isMaxValue('totalKeys', value) + return ( + + {numberWithSpaces(value)} + + ) + } + }, + { + name: ( +
+ + Clients +
+ ), + field: 'connectedClients', + width: '12%', + sortable: true, + align: 'right', + render: (value: number) => { + const isMax = isMaxValue('connectedClients', value) + return ( + + {numberWithSpaces(value)} + + ) + } + }, + ] + + return ( +
+ {(loading && !nodes) && ( +
+ +
+ )} + {nodes && ( +
+ setSort(sort)} + data-testid="primary-nodes-table" + /> +
+ )} +
+ ) +} + +export default ClusterNodesTable diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluser-nodes-table/index.ts b/redisinsight/ui/src/pages/clusterDetails/components/cluser-nodes-table/index.ts new file mode 100644 index 0000000000..36e8e9e491 --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluser-nodes-table/index.ts @@ -0,0 +1,3 @@ +import ClusterNodesTable from './ClusterNodesTable' + +export default ClusterNodesTable diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluser-nodes-table/styles.module.scss b/redisinsight/ui/src/pages/clusterDetails/components/cluser-nodes-table/styles.module.scss new file mode 100644 index 0000000000..bd22e72f9a --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluser-nodes-table/styles.module.scss @@ -0,0 +1,183 @@ +@import "@elastic/eui/src/global_styling/mixins/helpers"; +@import "@elastic/eui/src/components/table/mixins"; +@import "@elastic/eui/src/global_styling/index"; + +$breakpoint-table: 1232px; + +.wrapper { + max-width: 1920px; + + .loading { + margin-top: 40px; + width: 100%; + + :global { + .euiLoadingContent__singleLine { + height: 36px; + + &:first-child { + height: 42px; + margin-bottom: 18px; + } + } + + .euiLoadingContent__singleLine:last-child:not(:only-child) { + width: 100%; + } + } + } +} + +.tableWrapper { + @include euiScrollBar; + + overflow: auto; + position: relative; + max-height: 100%; +} + +.table.tableNodes { + :global { + .euiTableHeaderCell { + min-width: 144px; + background-color: var(--euiColorEmptyShade); + + @media screen and (max-width: $breakpoint-table) { + min-width: 112px; + } + + .euiTableCellContent { + min-height: 78px; + padding: 12px 12px 18px 24px; + justify-content: flex-start; + align-items: flex-end; + + @media screen and (max-width: $breakpoint-table) { + padding: 12px 6px 18px 12px; + } + + &.euiTableCellContent--alignRight { + padding-left: 12px; + padding-right: 24px; + justify-content: flex-start; + align-items: flex-end; + flex-direction: row-reverse; + + @media screen and (max-width: $breakpoint-table) { + padding-left: 6px; + padding-right: 12px; + } + + .euiTableSortIcon { + margin-right: 4px; + margin-left: 0; + } + } + + &.euiTableCellContent--alignCenter { + justify-content: center; + + .euiTableSortIcon { + margin-right: 0; + margin-left: 4px; + } + } + + .euiTableCellContent__text { + font: normal normal normal 12px/18px Graphik, sans-serif; + } + } + + .euiTableHeaderButton { + border-bottom: 1px solid var(--euiColorLightShade); + outline: 1px solid var(--euiColorEmptyShade); + } + } + + .euiTableCellContent { + position: relative; + padding: 12px 12px 12px 24px; + font: normal normal 500 16px/18px Inconsolata; + + @media screen and (max-width: $breakpoint-table) { + padding: 12px 6px 12px 12px; + } + + &.euiTableCellContent--alignRight { + padding-left: 12px; + padding-right: 24px; + + @media screen and (max-width: $breakpoint-table) { + padding-left: 6px; + padding-right: 12px; + } + } + } + + .euiTableSortIcon { + width: 14px; + height: 14px; + margin-bottom: 2px; + fill: var(--htmlColor) !important; + } + + .euiTableHeaderButton.euiTableHeaderButton-isSorted { + span, div { + color: var(--htmlColor) !important; + } + } + + .euiTableHeaderButton:focus .euiTableCellContent__text { + text-decoration: none; + } + } + + :global(.euiTableCellContent) .maxValue { + color: var(--euiTooltipTitleTextColor); + font-weight: bold; + } + + :global(.euiTableHeaderButton.euiTableHeaderButton-isSorted) .headerIcon { + fill: var(--htmlColor) !important; + } + + .valueUnit { + font: normal normal normal 12px/18px Graphik, sans-serif !important; + margin-left: 4px; + color: var(--euiColorMediumShade) !important; + } + + .nodeName { + margin-right: 12px; + display: block; + min-width: 24px; + font: normal normal 500 13px/18px Graphik, sans-serif !important; + } + + .headerCell { + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: flex-end; + + .headerIcon { + fill: var(--textColorShade); + width: 24px; + height: 20px; + margin-bottom: 4px; + } + } + + .hostPort { + display: inline-flex; + font: normal normal normal 13px/18px Graphik, sans-serif !important; + } + + .nodeColor { + position: absolute; + left: 0; + top: 1px; + bottom: 1px; + width: 3px; + } +} diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/ClusterDetailsGraphics.spec.tsx b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/ClusterDetailsGraphics.spec.tsx new file mode 100644 index 0000000000..c666092bca --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/ClusterDetailsGraphics.spec.tsx @@ -0,0 +1,100 @@ +import React from 'react' +import { ModifiedClusterNodes } from 'uiSrc/pages/clusterDetails/ClusterDetailsPage' +import { render, screen } from 'uiSrc/utils/test-utils' + +import ClusterDetailsGraphics from './ClusterDetailsGraphics' + +const mockNodes = [ + { + id: '1', + host: '0.0.0.1', + port: 6379, + role: 'primary', + slots: ['10923-16383'], + health: 'online', + totalKeys: 1, + usedMemory: 2867968, + opsPerSecond: 1, + connectionsReceived: 13, + connectedClients: 6, + commandsProcessed: 5678, + networkInKbps: 0.02, + networkOutKbps: 0, + cacheHitRatio: 1, + replicationOffset: 6924, + uptimeSec: 5614, + version: '6.2.6', + mode: 'cluster', + replicas: [] + }, + { + id: '2', + host: '0.0.0.2', + port: 6379, + role: 'primary', + slots: ['0-5460'], + health: 'online', + totalKeys: 4, + usedMemory: 2825880, + opsPerSecond: 1, + connectionsReceived: 15, + connectedClients: 4, + commandsProcessed: 5667, + networkInKbps: 0.04, + networkOutKbps: 0, + cacheHitRatio: 1, + replicationOffset: 6910, + uptimeSec: 5609, + version: '6.2.6', + mode: 'cluster', + replicas: [] + }, + { + id: '3', + host: '0.0.0.3', + port: 6379, + role: 'primary', + slots: [ + '5461-10922' + ], + health: 'online', + totalKeys: 10, + usedMemory: 2886960, + opsPerSecond: 0, + connectionsReceived: 18, + connectedClients: 7, + commandsProcessed: 5697, + networkInKbps: 0.02, + networkOutKbps: 0, + cacheHitRatio: 0, + replicationOffset: 6991, + uptimeSec: 5609, + version: '6.2.6', + mode: 'cluster', + replicas: [] + } +].map((d, index) => ({ ...d, letter: 'A', index, color: [0, 0, 0] })) as ModifiedClusterNodes[] + +describe('ClusterDetailsGraphics', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render nothing without nodes', () => { + render() + expect(screen.queryByTestId('cluster-details-graphics-loading')).not.toBeInTheDocument() + expect(screen.queryByTestId('cluster-details-charts')).not.toBeInTheDocument() + }) + + it('should render loading content', () => { + render() + expect(screen.getByTestId('cluster-details-graphics-loading')).toBeInTheDocument() + expect(screen.queryByTestId('cluster-details-charts')).not.toBeInTheDocument() + }) + + it('should render donuts', () => { + render() + expect(screen.getByTestId('donut-memory')).toBeInTheDocument() + expect(screen.queryByTestId('donut-keys')).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/ClusterDetailsGraphics.tsx b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/ClusterDetailsGraphics.tsx new file mode 100644 index 0000000000..3eaa6a4778 --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/ClusterDetailsGraphics.tsx @@ -0,0 +1,114 @@ +import { EuiIcon, EuiTitle } from '@elastic/eui' +import cx from 'classnames' +import { sumBy } from 'lodash' +import React, { useEffect, useState } from 'react' +import { DonutChart } from 'uiSrc/components/charts' +import { ChartData } from 'uiSrc/components/charts/donut-chart/DonutChart' +import { KeyIconSvg, MemoryIconSvg } from 'uiSrc/components/database-overview/components/icons' +import { ModifiedClusterNodes } from 'uiSrc/pages/clusterDetails/ClusterDetailsPage' +import { formatBytes, Nullable } from 'uiSrc/utils' +import { getPercentage, numberWithSpaces } from 'uiSrc/utils/numbers' + +import styles from './styles.module.scss' + +const ClusterDetailsGraphics = ({ nodes, loading }: { nodes: Nullable, loading: boolean }) => { + const [memoryData, setMemoryData] = useState([]) + const [memorySum, setMemorySum] = useState(0) + const [keysData, setKeysData] = useState([]) + const [keysSum, setKeysSum] = useState(0) + + const renderMemoryTooltip = (data: ChartData) => ( +
+
+ {data.name}: + {data.meta?.host}:{data.meta?.port} +
+ + {getPercentage(data.value, memorySum)}% + ( {formatBytes(data.value, 3, false)} ) + +
+ ) + + const renderKeysTooltip = (data: ChartData) => ( +
+
+ {data.name}: + {data.meta?.host}:{data.meta?.port} +
+ + {getPercentage(data.value, keysSum)}% + ( {numberWithSpaces(data.value)} ) + +
+ ) + + useEffect(() => { + if (nodes) { + const memory = nodes.map((n) => ({ value: n.usedMemory, name: n.letter, color: n.color, meta: { ...n } })) + const keys = nodes.map((n) => ({ value: n.totalKeys, name: n.letter, color: n.color, meta: { ...n } })) + + setMemoryData(memory as ChartData[]) + setKeysData(keys as ChartData[]) + + setMemorySum(sumBy(memory, 'value')) + setKeysSum(sumBy(keys, 'value')) + } + }, [nodes]) + + if (loading && !nodes?.length) { + return ( +
+
+
+
+ ) + } + + if (!nodes || nodes.length === 0) { + return null + } + + return ( +
+ +
+ + + Memory + +
+
+
{formatBytes(memorySum, 3)}
+
+ )} + /> + +
+ + + Keys + +
+
+
{numberWithSpaces(keysSum)}
+
+ )} + /> +
+ ) +} + +export default ClusterDetailsGraphics diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/index.ts b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/index.ts new file mode 100644 index 0000000000..316d8707fc --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/index.ts @@ -0,0 +1,3 @@ +import ClusterDetailsGraphics from './ClusterDetailsGraphics' + +export default ClusterDetailsGraphics diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/styles.module.scss b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/styles.module.scss new file mode 100644 index 0000000000..7332f90891 --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-graphics/styles.module.scss @@ -0,0 +1,62 @@ +.wrapper { + background-color: var(--euiColorLightestShade); + border-radius: 16px; + + display: flex; + align-items: center; + justify-content: space-around; + margin-bottom: 24px; + + &.loadingWrapper { + margin-top: 36px; + } + + .chartCenter { + display: flex; + flex-direction: column; + align-items: center; + } + + .chartTitle { + display: flex; + align-items: center; + + .icon { + margin-right: 10px; + } + } + + .titleSeparator { + height: 1px; + border: 0; + background-color: var(--separatorColorLight); + margin: 6px 0; + width: 60px; + } + + .centerCount { + margin-top: 2px; + font-weight: 500; + font-size: 14px; + } + + .preloaderCircle { + width: 180px; + height: 180px; + margin: 60px 0; + border-radius: 100%; + background-color: var(--separatorColor); + } + + .labelTooltip { + font-size: 12px; + + .tooltipPercentage { + margin-right: 6px; + } + + .tooltipTitle { + margin-bottom: 6px; + } + } +} diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-header/ClusterDetailsHeader.spec.tsx b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-header/ClusterDetailsHeader.spec.tsx new file mode 100644 index 0000000000..edcc58d044 --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-header/ClusterDetailsHeader.spec.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import { clusterDetailsSelector } from 'uiSrc/slices/analytics/clusterDetails' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { render, screen } from 'uiSrc/utils/test-utils' + +import ClusterDetailsHeader from './ClusterDetailsHeader' + +jest.mock('uiSrc/slices/analytics/clusterDetails', () => ({ + ...jest.requireActual('uiSrc/slices/analytics/clusterDetails'), + clusterDetailsSelector: jest.fn().mockReturnValue({ + data: null, + loading: false, + error: '', + }), +})) + +jest.mock('uiSrc/slices/instances/instances', () => ({ + ...jest.requireActual('uiSrc/slices/instances/instances'), + connectedInstanceSelector: jest.fn().mockReturnValue({ + username: '', + }), +})) + +describe('ClusterDetailsHeader', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render "EuiLoadingContent" until loading and no data', () => { + (clusterDetailsSelector as jest.Mock).mockImplementation(() => ({ + data: null, + loading: true, + error: '' + })) + + render() + + expect(screen.getByTestId('cluster-details-loading')).toBeInTheDocument() + }) + it('should render "cluster-details-content" after loading and with data', () => { + (clusterDetailsSelector as jest.Mock).mockImplementation(() => ({ + data: { version: '111' }, + loading: false, + error: '' + })) + + const { queryByTestId } = render() + + expect(queryByTestId('cluster-details-loading')).not.toBeInTheDocument() + expect(queryByTestId('cluster-details-username')).not.toBeInTheDocument() + expect(queryByTestId('cluster-details-content')).toBeInTheDocument() + }) + + it('huge username should be truncated', () => { + (clusterDetailsSelector as jest.Mock).mockImplementation(() => ({ + data: { version: '111' }, + loading: false, + error: '' + })); + + (connectedInstanceSelector as jest.Mock).mockImplementation(() => ({ + username: Array.from({ length: 50 }).fill('test').join('') + })) + + const { queryByTestId } = render() + + expect(queryByTestId('cluster-details-username')).toBeInTheDocument() + }) + + it.skip('uptime should be with truncated to first unit', () => { + (clusterDetailsSelector as jest.Mock).mockImplementation(() => ({ + data: { uptimeSec: 11111 }, + loading: false, + error: '' + })) + + const { queryByTestId } = render() + + expect(queryByTestId('cluster-details-uptime')).toHaveTextContent('3 h') + }) +}) diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-header/ClusterDetailsHeader.tsx b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-header/ClusterDetailsHeader.tsx new file mode 100644 index 0000000000..5a4d0c986c --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-header/ClusterDetailsHeader.tsx @@ -0,0 +1,115 @@ +import { + EuiLoadingContent, + EuiText, + EuiToolTip, +} from '@elastic/eui' +import React from 'react' +import { useSelector } from 'react-redux' +import cx from 'classnames' +import { capitalize } from 'lodash' + +import { + truncateNumberToFirstUnit, + formatLongName, + truncateNumberToDuration, +} from 'uiSrc/utils' +import { nullableNumberWithSpaces } from 'uiSrc/utils/numbers' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { ConnectionType, CONNECTION_TYPE_DISPLAY } from 'uiSrc/slices/interfaces' +import AnalyticsTabs from 'uiSrc/components/analytics-tabs' +import { clusterDetailsSelector } from 'uiSrc/slices/analytics/clusterDetails' + +import styles from './styles.module.scss' + +interface IMetrics { + label: string + value: any + border?: 'left' +} + +const MAX_NAME_LENGTH = 30 +const DEFAULT_USERNAME = 'Default' + +const ClusterDetailsHeader = () => { + const { + username = DEFAULT_USERNAME, + connectionType = ConnectionType.Cluster, + } = useSelector(connectedInstanceSelector) + + const { + data, + loading, + } = useSelector(clusterDetailsSelector) + + const metrics: IMetrics[] = [{ + label: 'Type', + value: CONNECTION_TYPE_DISPLAY[connectionType], + }, { + label: 'Version', + value: data?.version || '', + }, { + label: 'User', + value: (username || DEFAULT_USERNAME)?.length < MAX_NAME_LENGTH + ? (username || DEFAULT_USERNAME) + : ( + + {formatLongName(username || DEFAULT_USERNAME)} + + )} + > +
{formatLongName(username || DEFAULT_USERNAME, MAX_NAME_LENGTH, 5)}
+
+ ), + }, { + label: 'Uptime', + border: 'left', + value: ( + + {`${nullableNumberWithSpaces(data?.uptimeSec) || 0} s`} +
+ {`(${truncateNumberToDuration(data?.uptimeSec || 0)})`} + + )} + > +
{truncateNumberToFirstUnit(data?.uptimeSec || 0)}
+
+ ) + }] + + return ( +
+ + + {loading && !data && ( +
+ +
+ )} + {data && ( +
+ {metrics.map(({ value, label, border }) => ( +
+ {value} + {label} +
+ ))} +
+ )} +
+ ) +} + +export default ClusterDetailsHeader diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-header/index.ts b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-header/index.ts new file mode 100644 index 0000000000..e39d5afe78 --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-header/index.ts @@ -0,0 +1,3 @@ +import ClusterDetailsHeader from './ClusterDetailsHeader' + +export default ClusterDetailsHeader diff --git a/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-header/styles.module.scss b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-header/styles.module.scss new file mode 100644 index 0000000000..ac015f280f --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/components/cluster-details-header/styles.module.scss @@ -0,0 +1,43 @@ +.container { + background-color: var(--euiColorEmptyShade); + max-width: 960px; +} + +.content { + padding-top: 24px; + padding-bottom: 36px; + display: flex; + + .label { + font-size: 12px !important; + line-height: 18px !important; + padding-bottom: 1px; + color: var(--euiColorMediumShade) !important; + } +} + +.item { + padding-right: 24px; +} + +.value { + height: 22px; + font-size: 18px !important; + line-height: 18px !important; + font-weight: 500 !important; +} + +.loading { + width: 422px; + padding-top: 30px; + + :global(.euiLoadingContent__singleLine:last-child:not(:only-child)) { + width: 75% !important; + height: 12px !important; + } +} + +.borderLeft { + border-left: 2px solid var(--separatorColorLight); + padding-left: 24px; +} diff --git a/redisinsight/ui/src/pages/clusterDetails/components/index.ts b/redisinsight/ui/src/pages/clusterDetails/components/index.ts new file mode 100644 index 0000000000..a3332921e8 --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/components/index.ts @@ -0,0 +1,9 @@ +import ClusterDetailsHeader from './cluster-details-header' +import ClusterDetailsGraphics from './cluster-details-graphics' +import ClusterNodesTable from './cluser-nodes-table' + +export { + ClusterDetailsHeader, + ClusterDetailsGraphics, + ClusterNodesTable +} diff --git a/redisinsight/ui/src/pages/clusterDetails/index.ts b/redisinsight/ui/src/pages/clusterDetails/index.ts new file mode 100644 index 0000000000..d8f15ce35f --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/index.ts @@ -0,0 +1,3 @@ +import ClusterDetailsPage from './ClusterDetailsPage' + +export default ClusterDetailsPage diff --git a/redisinsight/ui/src/pages/clusterDetails/styles.module.scss b/redisinsight/ui/src/pages/clusterDetails/styles.module.scss new file mode 100644 index 0000000000..8bd111291a --- /dev/null +++ b/redisinsight/ui/src/pages/clusterDetails/styles.module.scss @@ -0,0 +1,19 @@ +@import "@elastic/eui/src/global_styling/mixins/helpers"; +@import "@elastic/eui/src/components/table/mixins"; +@import "@elastic/eui/src/global_styling/index"; + +.main { + margin: 0 16px 0; + height: calc(100% - 70px); + background-color: var(--euiColorEmptyShade); + padding: 18px 24px; +} + +.wrapper { + @include euiScrollBar; + overflow-y: auto; + overflow-x: hidden; + max-height: calc(100% - 134px); + + max-width: 1920px; +} diff --git a/redisinsight/ui/src/pages/home/HomePage.spec.tsx b/redisinsight/ui/src/pages/home/HomePage.spec.tsx index dfbab04b04..c35b5d08f9 100644 --- a/redisinsight/ui/src/pages/home/HomePage.spec.tsx +++ b/redisinsight/ui/src/pages/home/HomePage.spec.tsx @@ -9,7 +9,7 @@ jest.mock('uiSrc/slices/content/create-redis-buttons', () => ({ })) describe('HomePage', () => { - it('should render', () => { - expect(render()).toBeTruthy() + it('should render', async () => { + expect(await render()).toBeTruthy() }) }) diff --git a/redisinsight/ui/src/pages/home/components/AddDatabases/AddDatabasesContainer.tsx b/redisinsight/ui/src/pages/home/components/AddDatabases/AddDatabasesContainer.tsx index 244477e1c5..b1ad7f4aa3 100644 --- a/redisinsight/ui/src/pages/home/components/AddDatabases/AddDatabasesContainer.tsx +++ b/redisinsight/ui/src/pages/home/components/AddDatabases/AddDatabasesContainer.tsx @@ -18,8 +18,8 @@ import { clusterSelector, resetDataRedisCluster } from 'uiSrc/slices/instances/c import { Instance, InstanceType } from 'uiSrc/slices/interfaces' import { sentinelSelector, resetDataSentinel } from 'uiSrc/slices/instances/sentinel' -import InstanceFormWrapper from '../AddInstanceForm/InstanceFormWrapper' import InstanceConnections from './InstanceConnections/InstanceConnections' +import InstanceFormWrapper from '../AddInstanceForm/InstanceFormWrapper' import ClusterConnectionFormWrapper from '../ClusterConnection/ClusterConnectionFormWrapper' import CloudConnectionFormWrapper from '../CloudConnection/CloudConnectionFormWrapper' diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx index 8e928bffa4..9641566c62 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx +++ b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx @@ -288,10 +288,8 @@ const AddStandaloneForm = (props: Props) => { } } - if (isCloneMode && connectionType === ConnectionType.Sentinel) { - if (!values.sentinelMasterName) { - errs.sentinelMasterName = fieldDisplayNames.sentinelMasterName - } + if (isCloneMode && connectionType === ConnectionType.Sentinel && !values.sentinelMasterName) { + errs.sentinelMasterName = fieldDisplayNames.sentinelMasterName } setErrors(errs) @@ -1359,6 +1357,7 @@ const AddStandaloneForm = (props: Props) => { title="Database" isCollapsible initialIsOpen={false} + data-testid="database-nav-group" > {SentinelMasterDatabase()} @@ -1366,6 +1365,7 @@ const AddStandaloneForm = (props: Props) => { title="Sentinel" isCollapsible initialIsOpen={false} + data-testid="sentinel-nav-group" > {SentinelHostPort()} {DatabaseForm()} @@ -1387,6 +1387,7 @@ const AddStandaloneForm = (props: Props) => { title="Database" isCollapsible initialIsOpen={false} + data-testid="database-nav-group-clone" > {SentinelMasterDatabase()} @@ -1394,6 +1395,7 @@ const AddStandaloneForm = (props: Props) => { title="Sentinel" isCollapsible initialIsOpen={false} + data-testid="sentinel-nav-group-clone" > {DatabaseForm()} diff --git a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionForm/ClusterConnectionForm.tsx b/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionForm/ClusterConnectionForm.tsx index fe443aaf84..ecab1fac6e 100644 --- a/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionForm/ClusterConnectionForm.tsx +++ b/redisinsight/ui/src/pages/home/components/ClusterConnection/ClusterConnectionForm/ClusterConnectionForm.tsx @@ -21,12 +21,11 @@ import { import { MAX_PORT_NUMBER, - validateEmail, validateField, validatePortNumber, } from 'uiSrc/utils/validations' import { APPLICATION_NAME } from 'uiSrc/constants' -import { ErrorTextValidation, handlePasteHostName } from 'uiSrc/utils' +import { handlePasteHostName } from 'uiSrc/utils' import validationErrors from 'uiSrc/constants/validationErrors' import { ICredentialsRedisCluster } from 'uiSrc/slices/interfaces' @@ -65,8 +64,6 @@ const fieldDisplayNames: Values = { password: 'Admin Password', } -const initErrorEmail = `Username/email ${ErrorTextValidation.FormatIncorrect}` - const Message = () => ( Your Redis Enterprise databases can be automatically added. Enter the @@ -105,7 +102,6 @@ const ClusterConnectionForm = (props: Props) => { const [errors, setErrors] = useState>( host || port || username || password ? {} : fieldDisplayNames ) - const [errorEmail, setErrorEmail] = useState('') const [initialValues, setInitialValues] = useState({ host, @@ -132,24 +128,6 @@ const ClusterConnectionForm = (props: Props) => { !value && Object.assign(errs, { [key]: fieldDisplayNames[key] }) ) - if ( - values.username - && formik.touched.username - && !validateEmail(values.username) - ) { - setErrorEmail(initErrorEmail) - Object.assign(errs, { username: initErrorEmail }) - } - - if ( - values.username - && formik.touched.username - && validateEmail(values.username) - ) { - setErrorEmail('') - delete errs.username - } - setErrors(errs) return errs } @@ -173,16 +151,6 @@ const ClusterConnectionForm = (props: Props) => { } } - const onBlurUsername = (username: string) => { - formik.setFieldTouched('username') - if (username && !validateEmail(username)) { - setErrorEmail(initErrorEmail) - setErrors(Object.assign(errors, { username: initErrorEmail })) - } else { - setErrorEmail('') - } - } - const AppendHostName = () => ( { { data-testid="username" fullWidth maxLength={200} - isInvalid={!!errorEmail} placeholder="Enter Admin Username" value={formik.values.username} onChange={formik.handleChange} - onBlur={(e: React.FocusEvent) => { - onBlurUsername(e.target.value) - formik.handleBlur(e) - }} /> diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/styles.module.scss b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/styles.module.scss index 12c4c8a076..3422b0bbf0 100644 --- a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/styles.module.scss +++ b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/styles.module.scss @@ -131,3 +131,11 @@ $breakpoint-l: 1400px; height: 24px; color: var(--htmlColor) !important; } + +.actionDeleteBtn { + min-width: 93px !important; + + &:focus { + text-decoration: none !important; + } +} diff --git a/redisinsight/ui/src/pages/home/components/SearchDatabasesList/SearchDatabasesList.tsx b/redisinsight/ui/src/pages/home/components/SearchDatabasesList/SearchDatabasesList.tsx index 0bb3bc89d0..e8abc27bc7 100644 --- a/redisinsight/ui/src/pages/home/components/SearchDatabasesList/SearchDatabasesList.tsx +++ b/redisinsight/ui/src/pages/home/components/SearchDatabasesList/SearchDatabasesList.tsx @@ -3,7 +3,7 @@ import { EuiFieldSearch } from '@elastic/eui' import { useDispatch, useSelector } from 'react-redux' import { instancesSelector, loadInstancesSuccess } from 'uiSrc/slices/instances/instances' -import { Instance } from 'uiSrc/slices/interfaces' +import { CONNECTION_TYPE_DISPLAY, Instance } from 'uiSrc/slices/interfaces' import { lastConnectionFormat } from 'uiSrc/utils' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import styles from './styles.module.scss' @@ -28,8 +28,8 @@ const SearchDatabasesList = () => { visible: item.name?.toLowerCase().indexOf(value) !== -1 || item.host?.toString()?.indexOf(value) !== -1 || item.port?.toString()?.indexOf(value) !== -1 - || item.connectionType?.toString()?.indexOf(value) !== -1 - || item.modules?.toString()?.indexOf(value) !== -1 + || (item.connectionType && CONNECTION_TYPE_DISPLAY[item.connectionType]?.toLowerCase()?.indexOf(value) !== -1) + || item.modules?.map((m) => m.name?.toLowerCase()).join(',').indexOf(value) !== -1 || lastConnectionFormat(item.lastConnection)?.indexOf(value) !== -1 }) ) diff --git a/redisinsight/ui/src/pages/instance/InstancePage.tsx b/redisinsight/ui/src/pages/instance/InstancePage.tsx index eb0f7d34b7..250222ba46 100644 --- a/redisinsight/ui/src/pages/instance/InstancePage.tsx +++ b/redisinsight/ui/src/pages/instance/InstancePage.tsx @@ -22,6 +22,7 @@ import BottomGroupComponents from 'uiSrc/components/bottom-group-components/Bott import { monitorSelector, setMonitorInitialState } from 'uiSrc/slices/cli/monitor' import { setInitialPubSubState } from 'uiSrc/slices/pubsub/pubsub' import { setBulkActionsInitialState } from 'uiSrc/slices/browser/bulkActions' +import { setClusterDetailsInitialState } from 'uiSrc/slices/analytics/clusterDetails' import InstancePageRouter from './InstancePageRouter' import styles from './styles.module.scss' @@ -91,6 +92,7 @@ const InstancePage = ({ routes = [] }: Props) => { dispatch(setBulkActionsInitialState()) dispatch(setAppContextInitialState()) dispatch(resetKeysData()) + dispatch(setClusterDetailsInitialState()) setTimeout(() => { dispatch(resetOutput()) }, 0) diff --git a/redisinsight/ui/src/pages/redisStack/components/edit-connection/EditConnection.tsx b/redisinsight/ui/src/pages/redisStack/components/edit-connection/EditConnection.tsx index 24e2017dae..d61bbe851d 100644 --- a/redisinsight/ui/src/pages/redisStack/components/edit-connection/EditConnection.tsx +++ b/redisinsight/ui/src/pages/redisStack/components/edit-connection/EditConnection.tsx @@ -5,7 +5,7 @@ import { EuiFlexItem, EuiPage, EuiPageBody } from '@elastic/eui' import { getApiErrorMessage, isStatusSuccessful, Nullable, setTitle } from 'uiSrc/utils' import { apiService } from 'uiSrc/services' -import { ApiEndpoints, Pages } from 'uiSrc/constants' +import { ApiEndpoints } from 'uiSrc/constants' import { PageHeader, PagePlaceholder } from 'uiSrc/components' import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { setConnectedInstanceId } from 'uiSrc/slices/instances/instances' diff --git a/redisinsight/ui/src/pages/settings/SettingsPage.spec.tsx b/redisinsight/ui/src/pages/settings/SettingsPage.spec.tsx index f2e67280e4..38902d96a1 100644 --- a/redisinsight/ui/src/pages/settings/SettingsPage.spec.tsx +++ b/redisinsight/ui/src/pages/settings/SettingsPage.spec.tsx @@ -31,4 +31,13 @@ describe('SettingsPage', () => { ).toBeInTheDocument() expect(render()).toBeTruthy() }) + + it('Accordion "Workbench settings" should render', () => { + const { container } = render() + + expect( + container.querySelector('[data-test-subj="accordion-workbench-settings"]') + ).toBeInTheDocument() + expect(render()).toBeTruthy() + }) }) diff --git a/redisinsight/ui/src/pages/settings/SettingsPage.tsx b/redisinsight/ui/src/pages/settings/SettingsPage.tsx index 06608824cd..2d9af00b67 100644 --- a/redisinsight/ui/src/pages/settings/SettingsPage.tsx +++ b/redisinsight/ui/src/pages/settings/SettingsPage.tsx @@ -20,7 +20,7 @@ import { useDispatch, useSelector } from 'react-redux' import { setTitle } from 'uiSrc/utils' import { THEMES } from 'uiSrc/constants' import { useDebouncedEffect } from 'uiSrc/services' -import { ConsentsNotifications, ConsentsPrivacy, AdvancedSettings } from 'uiSrc/components' +import { ConsentsNotifications, ConsentsPrivacy } from 'uiSrc/components' import { sendEventTelemetry, sendPageViewTelemetry, TelemetryEvent, TelemetryPageView } from 'uiSrc/telemetry' import { appAnalyticsInfoSelector } from 'uiSrc/slices/app/info' import { ThemeContext } from 'uiSrc/contexts/themeContext' @@ -30,6 +30,8 @@ import { userSettingsSelector, } from 'uiSrc/slices/user/user-settings' +import { AdvancedSettings, WorkbenchSettings } from './components' + import styles from './styles.module.scss' const SettingsPage = () => { @@ -103,7 +105,18 @@ const SettingsPage = () => {
) - const AdvancedSettingsNavGroup = () => ( + const WorkbenchSettingsGroup = () => ( +
+ {loading && ( +
+ +
+ )} + +
+ ) + + const AdvancedSettingsGroup = () => (
{loading && (
@@ -112,7 +125,7 @@ const SettingsPage = () => { )} - These settings should only be changed if you understand their impact. + Advanced settings should only be changed if you understand their impact. @@ -146,6 +159,15 @@ const SettingsPage = () => { > {PrivacySettings()} + + {WorkbenchSettingsGroup()} + { initialIsOpen={false} data-test-subj="accordion-advanced-settings" > - {AdvancedSettingsNavGroup()} + {AdvancedSettingsGroup()} diff --git a/redisinsight/ui/src/components/advanced-settings/AdvancedSettings.spec.tsx b/redisinsight/ui/src/pages/settings/components/advanced-settings/AdvancedSettings.spec.tsx similarity index 82% rename from redisinsight/ui/src/components/advanced-settings/AdvancedSettings.spec.tsx rename to redisinsight/ui/src/pages/settings/components/advanced-settings/AdvancedSettings.spec.tsx index 2c70cb89a6..64e7ad1af4 100644 --- a/redisinsight/ui/src/components/advanced-settings/AdvancedSettings.spec.tsx +++ b/redisinsight/ui/src/pages/settings/components/advanced-settings/AdvancedSettings.spec.tsx @@ -20,14 +20,10 @@ describe('AdvancedSettings', () => { it('should render', () => { expect(render()).toBeTruthy() }) + it('should Keys-to-scan-value render ', () => { render() expect(screen.getByTestId(/keys-to-scan-value/)).toBeInTheDocument() }) - it('should pipeline-bunch render ', () => { - render() - - expect(screen.getByTestId(/pipeline-bunch/)).toBeInTheDocument() - }) }) diff --git a/redisinsight/ui/src/pages/settings/components/advanced-settings/AdvancedSettings.tsx b/redisinsight/ui/src/pages/settings/components/advanced-settings/AdvancedSettings.tsx new file mode 100644 index 0000000000..93852940c9 --- /dev/null +++ b/redisinsight/ui/src/pages/settings/components/advanced-settings/AdvancedSettings.tsx @@ -0,0 +1,43 @@ +import { EuiSpacer } from '@elastic/eui' +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' + +import { validateCountNumber } from 'uiSrc/utils' +import { SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' +import { SettingItem } from 'uiSrc/components' +import { updateUserConfigSettingsAction, userSettingsConfigSelector } from 'uiSrc/slices/user/user-settings' + +const AdvancedSettings = () => { + const { scanThreshold = '' } = useSelector(userSettingsConfigSelector) ?? {} + + const dispatch = useDispatch() + + const handleApplyKeysToScanChanges = (value: string) => { + // eslint-disable-next-line no-nested-ternary + const data = value ? (+value < SCAN_COUNT_DEFAULT ? SCAN_COUNT_DEFAULT : +value) : null + + dispatch( + updateUserConfigSettingsAction( + { scanThreshold: data }, + ) + ) + } + + return ( + <> + + + + ) +} + +export default AdvancedSettings diff --git a/redisinsight/ui/src/pages/settings/components/advanced-settings/index.ts b/redisinsight/ui/src/pages/settings/components/advanced-settings/index.ts new file mode 100644 index 0000000000..633a1956c0 --- /dev/null +++ b/redisinsight/ui/src/pages/settings/components/advanced-settings/index.ts @@ -0,0 +1,3 @@ +import AdvancedSettings from './AdvancedSettings' + +export default AdvancedSettings diff --git a/redisinsight/ui/src/pages/settings/components/index.ts b/redisinsight/ui/src/pages/settings/components/index.ts new file mode 100644 index 0000000000..0413500097 --- /dev/null +++ b/redisinsight/ui/src/pages/settings/components/index.ts @@ -0,0 +1,7 @@ +import AdvancedSettings from './advanced-settings' +import WorkbenchSettings from './workbench-settings' + +export { + AdvancedSettings, + WorkbenchSettings +} diff --git a/redisinsight/ui/src/pages/settings/components/workbench-settings/WorkbenchSettings.spec.tsx b/redisinsight/ui/src/pages/settings/components/workbench-settings/WorkbenchSettings.spec.tsx new file mode 100644 index 0000000000..61b5d95d76 --- /dev/null +++ b/redisinsight/ui/src/pages/settings/components/workbench-settings/WorkbenchSettings.spec.tsx @@ -0,0 +1,42 @@ +import { fireEvent } from '@testing-library/react' +import { cloneDeep } from 'lodash' +import React from 'react' +import { setWorkbenchCleanUp } from 'uiSrc/slices/user/user-settings' +import { + cleanup, + mockedStore, + render, + screen, +} from 'uiSrc/utils/test-utils' + +import WorkbenchSettings from './WorkbenchSettings' + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('WorkbenchSettings', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call proper actions after click on switch wb clear mode', () => { + render() + + const afterRenderActions = [...store.getActions()] + + fireEvent.click(screen.getByTestId('switch-workbench-cleanup')) + + expect(store.getActions()) + .toEqual([...afterRenderActions, setWorkbenchCleanUp(false)]) + }) + + it('should pipeline-bunch render ', () => { + render() + + expect(screen.getByTestId(/pipeline-bunch/)).toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/pages/settings/components/workbench-settings/WorkbenchSettings.tsx b/redisinsight/ui/src/pages/settings/components/workbench-settings/WorkbenchSettings.tsx new file mode 100644 index 0000000000..64663b9429 --- /dev/null +++ b/redisinsight/ui/src/pages/settings/components/workbench-settings/WorkbenchSettings.tsx @@ -0,0 +1,85 @@ +import { EuiFormRow, EuiLink, EuiSpacer, EuiSwitch, EuiTitle } from '@elastic/eui' +import { toNumber } from 'lodash' +import React from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { SettingItem } from 'uiSrc/components' +import { PIPELINE_COUNT_DEFAULT } from 'uiSrc/constants/api' +import styles from 'uiSrc/pages/settings/styles.module.scss' +import { + setWorkbenchCleanUp, + updateUserConfigSettingsAction, userSettingsConfigSelector, + userSettingsWBSelector +} from 'uiSrc/slices/user/user-settings' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { validateNumber } from 'uiSrc/utils' + +const WorkbenchSettings = () => { + const { cleanup } = useSelector(userSettingsWBSelector) + const { batchSize = PIPELINE_COUNT_DEFAULT } = useSelector(userSettingsConfigSelector) ?? {} + + const dispatch = useDispatch() + + const onSwitchWbCleanUp = (val: boolean) => { + dispatch(setWorkbenchCleanUp(val)) + sendEventTelemetry({ + event: TelemetryEvent.SETTINGS_WORKBENCH_EDITOR_CLEAR_CHANGED, + eventData: { + currentValue: !val, + newValue: val, + } + }) + } + + const handleApplyPipelineCountChanges = (value: string) => { + dispatch( + updateUserConfigSettingsAction( + { batchSize: toNumber(value) }, + ) + ) + } + + return ( + <> + +

Editor Cleanup

+
+ + + onSwitchWbCleanUp(e.target.checked)} + className={styles.switchOption} + data-testid="switch-workbench-cleanup" + /> + + + validateNumber(value)} + title="Pipeline Mode" + testid="pipeline-bunch" + placeholder={`${PIPELINE_COUNT_DEFAULT}`} + label="Commands in pipeline:" + summary={( + <> + {'Sets the size of a command batch for the '} + + pipeline + + {' mode in Workbench. 0 or 1 pipelines every command.'} + + )} + /> + + ) +} + +export default WorkbenchSettings diff --git a/redisinsight/ui/src/pages/settings/components/workbench-settings/index.ts b/redisinsight/ui/src/pages/settings/components/workbench-settings/index.ts new file mode 100644 index 0000000000..d8a1a3d618 --- /dev/null +++ b/redisinsight/ui/src/pages/settings/components/workbench-settings/index.ts @@ -0,0 +1,3 @@ +import WorkbenchSettings from './WorkbenchSettings' + +export default WorkbenchSettings diff --git a/redisinsight/ui/src/pages/settings/styles.module.scss b/redisinsight/ui/src/pages/settings/styles.module.scss index 608d8c48a0..c5a0e550bc 100644 --- a/redisinsight/ui/src/pages/settings/styles.module.scss +++ b/redisinsight/ui/src/pages/settings/styles.module.scss @@ -97,6 +97,6 @@ } .smallText { - font: normal normal normal 14px/24px Graphik !important; + font: normal normal normal 14px/24px Graphik, sans-serif !important; letter-spacing: -0.14px; } diff --git a/redisinsight/ui/src/pages/slowLog/SlowLogPage.spec.tsx b/redisinsight/ui/src/pages/slowLog/SlowLogPage.spec.tsx index 822a83e38d..9a90d17ee3 100644 --- a/redisinsight/ui/src/pages/slowLog/SlowLogPage.spec.tsx +++ b/redisinsight/ui/src/pages/slowLog/SlowLogPage.spec.tsx @@ -1,12 +1,11 @@ import React from 'react' -import { mock } from 'ts-mockito' -import { slowLogSelector } from 'uiSrc/slices/slowlog/slowlog' +import { slowLogSelector } from 'uiSrc/slices/analytics/slowlog' import { render, screen } from 'uiSrc/utils/test-utils' import SlowLogPage from './SlowLogPage' -jest.mock('uiSrc/slices/slowlog/slowlog', () => ({ - ...jest.requireActual('uiSrc/slices/slowlog/slowlog'), +jest.mock('uiSrc/slices/analytics/slowlog', () => ({ + ...jest.requireActual('uiSrc/slices/analytics/slowlog'), slowLogSelector: jest.fn().mockReturnValue({ data: [], config: null, diff --git a/redisinsight/ui/src/pages/slowLog/SlowLogPage.tsx b/redisinsight/ui/src/pages/slowLog/SlowLogPage.tsx index 6840bbb59b..c09d490aa6 100644 --- a/redisinsight/ui/src/pages/slowLog/SlowLogPage.tsx +++ b/redisinsight/ui/src/pages/slowLog/SlowLogPage.tsx @@ -26,12 +26,16 @@ import { getSlowLogConfigAction, slowLogConfigSelector, slowLogSelector -} from 'uiSrc/slices/slowlog/slowlog' +} from 'uiSrc/slices/analytics/slowlog' import { sendPageViewTelemetry, sendEventTelemetry, TelemetryEvent, TelemetryPageView } from 'uiSrc/telemetry' import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' import { numberWithSpaces } from 'uiSrc/utils/numbers' +import AnalyticsTabs from 'uiSrc/components/analytics-tabs' +import { analyticsSettingsSelector, setAnalyticsViewTab } from 'uiSrc/slices/analytics/settings' +import { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics' import { SlowLog } from 'apiSrc/modules/slow-log/models' + import { EmptySlowLog, SlowLogTable, Actions } from './components' import styles from './styles.module.scss' @@ -52,6 +56,7 @@ const SlowLogPage = () => { const { data, loading, durationUnit, config } = useSelector(slowLogSelector) const { slowlogLogSlowerThan = 0, slowlogMaxLen } = useSelector(slowLogConfigSelector) const { identified: analyticsIdentified } = useSelector(appAnalyticsInfoSelector) + const { viewTab } = useSelector(analyticsSettingsSelector) const { instanceId } = useParams<{ instanceId: string }>() const [count, setCount] = useState(DEFAULT_COUNT_VALUE) @@ -65,6 +70,9 @@ const SlowLogPage = () => { useEffect(() => { getConfig() + if (viewTab !== AnalyticsViewTab.SlowLog) { + dispatch(setAnalyticsViewTab(AnalyticsViewTab.SlowLog)) + } }, []) useEffect(() => { @@ -126,9 +134,12 @@ const SlowLogPage = () => {
- -

Slow Log

-
+ {connectionType === ConnectionType.Cluster && } + {connectionType !== ConnectionType.Cluster && ( + +

Slow Log

+
+ )}
diff --git a/redisinsight/ui/src/pages/slowLog/components/Actions/Actions.tsx b/redisinsight/ui/src/pages/slowLog/components/Actions/Actions.tsx index 39744ae66e..431ed26fe1 100644 --- a/redisinsight/ui/src/pages/slowLog/components/Actions/Actions.tsx +++ b/redisinsight/ui/src/pages/slowLog/components/Actions/Actions.tsx @@ -15,7 +15,7 @@ import cx from 'classnames' import { useParams } from 'react-router-dom' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { DurationUnits } from 'uiSrc/constants' -import { slowLogSelector } from 'uiSrc/slices/slowlog/slowlog' +import { slowLogSelector } from 'uiSrc/slices/analytics/slowlog' import AutoRefresh from 'uiSrc/pages/browser/components/auto-refresh' import { Nullable } from 'uiSrc/utils' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' diff --git a/redisinsight/ui/src/pages/slowLog/components/Actions/styles.module.scss b/redisinsight/ui/src/pages/slowLog/components/Actions/styles.module.scss index a62e4dfa3d..0eb156e684 100644 --- a/redisinsight/ui/src/pages/slowLog/components/Actions/styles.module.scss +++ b/redisinsight/ui/src/pages/slowLog/components/Actions/styles.module.scss @@ -39,7 +39,7 @@ } .popoverTitle { - font: normal normal 500 13px/22px Graphik; + font: normal normal 500 13px/22px Graphik, sans-serif; color: var(--euiColorFullShade) !important; } diff --git a/redisinsight/ui/src/pages/slowLog/components/SlowLogConfig/SlowLogConfig.spec.tsx b/redisinsight/ui/src/pages/slowLog/components/SlowLogConfig/SlowLogConfig.spec.tsx index 71d70a44b8..0b8379c301 100644 --- a/redisinsight/ui/src/pages/slowLog/components/SlowLogConfig/SlowLogConfig.spec.tsx +++ b/redisinsight/ui/src/pages/slowLog/components/SlowLogConfig/SlowLogConfig.spec.tsx @@ -18,8 +18,8 @@ beforeEach(() => { store.clearActions() }) -jest.mock('uiSrc/slices/slowlog/slowlog', () => ({ - ...jest.requireActual('uiSrc/slices/slowlog/slowlog'), +jest.mock('uiSrc/slices/analytics/slowlog', () => ({ + ...jest.requireActual('uiSrc/slices/analytics/slowlog'), slowLogConfigSelector: jest.fn().mockReturnValue({ slowlogMaxLen: slowlogMaxLenMock, slowlogLogSlowerThan: slowlogLogSlowerThanMock, diff --git a/redisinsight/ui/src/pages/slowLog/components/SlowLogConfig/SlowLogConfig.tsx b/redisinsight/ui/src/pages/slowLog/components/SlowLogConfig/SlowLogConfig.tsx index 72f71bc26a..56b137d720 100644 --- a/redisinsight/ui/src/pages/slowLog/components/SlowLogConfig/SlowLogConfig.tsx +++ b/redisinsight/ui/src/pages/slowLog/components/SlowLogConfig/SlowLogConfig.tsx @@ -24,7 +24,7 @@ import { import { ConnectionType } from 'uiSrc/slices/interfaces' import { ConfigDBStorageItem } from 'uiSrc/constants/storage' import { setDBConfigStorageField } from 'uiSrc/services' -import { patchSlowLogConfigAction, slowLogConfigSelector, slowLogSelector } from 'uiSrc/slices/slowlog/slowlog' +import { patchSlowLogConfigAction, slowLogConfigSelector, slowLogSelector } from 'uiSrc/slices/analytics/slowlog' import { errorValidateNegativeInteger, validateNumber } from 'uiSrc/utils' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { numberWithSpaces } from 'uiSrc/utils/numbers' diff --git a/redisinsight/ui/src/pages/slowLog/components/styles.module.scss b/redisinsight/ui/src/pages/slowLog/components/styles.module.scss index 983f2e4c81..57e3cad3ad 100644 --- a/redisinsight/ui/src/pages/slowLog/components/styles.module.scss +++ b/redisinsight/ui/src/pages/slowLog/components/styles.module.scss @@ -49,7 +49,7 @@ padding: 18px; .noFoundTitle { - font: normal normal 500 18px/24px Graphik; + font: normal normal 500 18px/24px Graphik, sans-serif; margin-bottom: 12px; } diff --git a/redisinsight/ui/src/pages/slowLog/styles.module.scss b/redisinsight/ui/src/pages/slowLog/styles.module.scss index ace7865e25..eabb8ac342 100644 --- a/redisinsight/ui/src/pages/slowLog/styles.module.scss +++ b/redisinsight/ui/src/pages/slowLog/styles.module.scss @@ -2,7 +2,7 @@ margin: 0 16px 0; height: calc(100% - 70px); background-color: var(--euiColorEmptyShade); - padding: 24px 18px; + padding: 18px 24px; .title { font-size: 16px; @@ -10,7 +10,8 @@ } .actionsLine { - margin-bottom: 12px; + margin-top: 6px; + margin-bottom: 6px; } .countSelectWrapper { @@ -18,9 +19,15 @@ } .countSelect { - width: 86px; + min-width: 58px; + max-width: 84px; height: 30px; padding-left: 12px; padding-right: 32px; + border: none !important; + + & ~ :global(.euiFormControlLayoutIcons) svg { + width: 12px; + } } } diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/Group/styles.scss b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/Group/styles.scss index 39c2c212d7..8cdcd5e6e6 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/Group/styles.scss +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/Group/styles.scss @@ -4,7 +4,7 @@ border-bottom: 1px solid transparent; transition: border-color 0.2s ease-in-out; .group-header { - font: normal normal medium 11px/15px Graphik; + font: normal normal medium 11px/15px Graphik, sans-serif; letter-spacing: -0.12px; overflow: hidden; white-space: nowrap; @@ -53,7 +53,7 @@ max-height: 30px; } .euiListGroupItem__label { - font: normal normal normal 14px/20px Graphik; + font: normal normal normal 14px/20px Graphik, sans-serif; letter-spacing: -0.14px; } } diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalLink/styles.module.scss b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalLink/styles.module.scss index d1c4c1ac6e..f9c214d10a 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalLink/styles.module.scss +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalLink/styles.module.scss @@ -1,7 +1,7 @@ .link { width: auto; white-space: nowrap; - font: normal normal normal 13px/30px Graphik; + font: normal normal normal 13px/30px Graphik, sans-serif; letter-spacing: -0.13px; font-weight: normal !important; & > button { diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/InternalPage.tsx b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/InternalPage.tsx index 4c02f85e13..6781c26094 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/InternalPage.tsx +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/InternalPage.tsx @@ -21,8 +21,8 @@ import { import { IEnablementAreaItem } from 'uiSrc/slices/interfaces' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import styles from './styles.module.scss' import './styles/main.scss' +import styles from './styles.module.scss' export interface Props { onClose: () => void; diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/styles/_helpers.scss b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/styles/_helpers.scss index da6f5501fb..af2693c6c3 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/styles/_helpers.scss +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/styles/_helpers.scss @@ -1,14 +1,14 @@ @import './variables'; @mixin font{ - font: normal normal normal 14px/24px Graphik; + font: normal normal normal 14px/24px Graphik, sans-serif; letter-spacing: -0.14px; @media only screen and (max-width: ($l-breakpoint + px)) { - font: normal normal normal 13px/18px Graphik; + font: normal normal normal 13px/18px Graphik, sans-serif; letter-spacing: -0.13px; } @media only screen and (max-width: ($m-breakpoint + px)) { - font: normal normal normal 12px/18px Graphik; + font: normal normal normal 12px/18px Graphik, sans-serif; letter-spacing: -0.12px; } } diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/styles/_typography.scss b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/styles/_typography.scss index d9638b2b18..a063acb096 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/styles/_typography.scss +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/InternalPage/styles/_typography.scss @@ -3,7 +3,7 @@ h1, h2, h3, h4, h5, h6 { color: var(--euiColorGhost); - font: normal normal 500 14px/24px Graphik; + font: normal normal 500 14px/24px Graphik, sans-serif; margin-bottom: ($margin-size-pace * 3 + px); margin-top: ($margin-size-pace * 3 + px); letter-spacing: 0; diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/LazyInternalPage/LazyInternalPage.tsx b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/LazyInternalPage/LazyInternalPage.tsx index 73d8a6f0ee..0320ca04e5 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/LazyInternalPage/LazyInternalPage.tsx +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/components/LazyInternalPage/LazyInternalPage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useCallback } from 'react' +import React, { useEffect, useState } from 'react' import { startCase } from 'lodash' import { useHistory } from 'react-router-dom' import { useDispatch, useSelector } from 'react-redux' diff --git a/redisinsight/ui/src/pages/workbench/components/module-not-loaded/ModuleNotLoaded.tsx b/redisinsight/ui/src/pages/workbench/components/module-not-loaded/ModuleNotLoaded.tsx index 2663703e37..75ec496cc8 100644 --- a/redisinsight/ui/src/pages/workbench/components/module-not-loaded/ModuleNotLoaded.tsx +++ b/redisinsight/ui/src/pages/workbench/components/module-not-loaded/ModuleNotLoaded.tsx @@ -129,14 +129,12 @@ const ModuleNotLoaded = ({ content = {} }: Props) => {
{(!!summaryImgPath || !!summaryImgDark || !!summaryImgLight) && ( -
- redisearch table -
+ redisearch table )} {!!summaryText &&
{parse(summaryText)}
}
diff --git a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx index c247bf4cb9..84124c30c7 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx @@ -6,7 +6,7 @@ import { Theme } from 'uiSrc/constants' import { Nullable } from 'uiSrc/utils' import QueryCard from 'uiSrc/components/query-card' import { CommandExecutionUI } from 'uiSrc/slices/interfaces' -import { RunQueryMode } from 'uiSrc/slices/interfaces/workbench' +import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' import { ThemeContext } from 'uiSrc/contexts/themeContext' import MultiPlayIconDark from 'uiSrc/assets/img/multi_play_icon_dark.svg' import MultiPlayIconLight from 'uiSrc/assets/img/multi_play_icon_light.svg' @@ -15,6 +15,7 @@ import styles from './styles.module.scss' export interface Props { items: CommandExecutionUI[] activeMode: RunQueryMode + activeResultsMode?: ResultsMode scrollDivRef: React.Ref onQueryReRun: (query: string, commandId?: Nullable, clearEditor?: boolean) => void onQueryDelete: (commandId: string) => void @@ -24,6 +25,7 @@ const WBResults = (props: Props) => { const { items = [], activeMode, + activeResultsMode, onQueryReRun, onQueryDelete, onQueryOpen, @@ -48,17 +50,36 @@ const WBResults = (props: Props) => { return (
- {items.map(({ command = '', isOpen = false, result = undefined, id = '', loading, createdAt, mode }) => ( + {items.map(( + { + command = '', + isOpen = false, + result = undefined, + summary = undefined, + id = '', + loading, + createdAt, + mode, + resultsMode, + emptyCommand, + isNotStored + } + ) => ( onQueryOpen(id)} onQueryReRun={() => onQueryReRun(command, null, false)} onQueryDelete={() => onQueryDelete(id)} diff --git a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResultsWrapper.tsx b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResultsWrapper.tsx index d3287aa3a8..a3c4f16a2f 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResultsWrapper.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResultsWrapper.tsx @@ -1,12 +1,13 @@ import React from 'react' import { Nullable } from 'uiSrc/utils' import { CommandExecutionUI } from 'uiSrc/slices/interfaces' -import { RunQueryMode } from 'uiSrc/slices/interfaces/workbench' +import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' import WBResults from './WBResults' export interface Props { items: CommandExecutionUI[] activeMode: RunQueryMode + activeResultsMode: ResultsMode scrollDivRef: React.Ref onQueryReRun: (query: string, commandId?: Nullable, clearEditor?: boolean) => void onQueryOpen: (commandId: string) => void diff --git a/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.tsx b/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.tsx index c2cecb2e49..78f9a0e8ff 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.tsx @@ -14,7 +14,7 @@ import { appContextWorkbench } from 'uiSrc/slices/app/context' import { CommandExecutionUI } from 'uiSrc/slices/interfaces' -import { RunQueryMode } from 'uiSrc/slices/interfaces/workbench' +import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' import WBResultsWrapper from '../../wb-results' import EnablementAreaWrapper from '../../enablement-area' @@ -34,10 +34,12 @@ export interface Props { scriptEl: Nullable scrollDivRef: Ref activeMode: RunQueryMode + resultsMode: ResultsMode onSubmit: (query?: string, commandId?: Nullable, clearEditor?: boolean) => void onQueryOpen: (commandId?: string) => void onQueryDelete: (commandId: string) => void onQueryChangeMode: () => void + onChangeGroupMode: () => void } const WBView = (props: Props) => { @@ -48,14 +50,16 @@ const WBView = (props: Props) => { setScriptEl, scriptEl, activeMode, + resultsMode, onSubmit, onQueryOpen, onQueryDelete, onQueryChangeMode, + onChangeGroupMode, scrollDivRef, } = props const [isMinimized, setIsMinimized] = useState( - (localStorageService?.get(BrowserStorageItem.isEnablementAreaMinimized) ?? 'false') === 'true' + localStorageService?.get(BrowserStorageItem.isEnablementAreaMinimized) ?? false ) const [isCodeBtnDisabled, setIsCodeBtnDisabled] = useState(false) @@ -101,16 +105,18 @@ const WBView = (props: Props) => { scrollable={false} className={styles.queryPanel} initialSize={vertical[verticalPanelIds.firstPanelId] ?? 20} - style={{ minHeight: '140px' }} + style={{ minHeight: '140px', overflow: 'hidden' }} > @@ -132,6 +138,7 @@ const WBView = (props: Props) => { + resultsMode: ResultsMode } let state: IState = { @@ -57,6 +58,7 @@ let state: IState = { blockingCommands: [], visualizations: [], scriptEl: null, + resultsMode: ResultsMode.Default, } const WBViewWrapper = () => { @@ -65,6 +67,7 @@ const WBViewWrapper = () => { const { loading, items } = useSelector(workbenchResultsSelector) const { unsupportedCommands, blockingCommands } = useSelector(cliSettingsSelector) const { batchSize = PIPELINE_COUNT_DEFAULT } = useSelector(userSettingsConfigSelector) ?? {} + const { cleanup: cleanupWB } = useSelector(userSettingsWBSelector) const { script: scriptContext } = useSelector(appContextWorkbench) const [script, setScript] = useState(scriptContext) @@ -73,6 +76,9 @@ const WBViewWrapper = () => { const [activeRunQueryMode, setActiveRunQueryMode] = useState( (localStorageService?.get(BrowserStorageItem.RunQueryMode) ?? RunQueryMode.ASCII) ) + const [resultsMode, setResultsMode] = useState( + (localStorageService?.get(BrowserStorageItem.wbGroupMode) ?? ResultsMode.Default) + ) const instance = useSelector(connectedInstanceSelector) const { visualizations = [] } = useSelector(appPluginsSelector) @@ -85,6 +91,7 @@ const WBViewWrapper = () => { visualizations, batchSize, activeRunQueryMode, + resultsMode, } const scrollDivRef: Ref = useRef(null) const scriptRef = useRef(script) @@ -120,6 +127,10 @@ const WBViewWrapper = () => { localStorageService.set(BrowserStorageItem.RunQueryMode, activeRunQueryMode) }, [activeRunQueryMode]) + useEffect(() => { + localStorageService.set(BrowserStorageItem.wbGroupMode, resultsMode) + }, [resultsMode]) + const handleChangeQueryRunMode = () => { setActiveRunQueryMode( activeRunQueryMode === RunQueryMode.ASCII @@ -138,37 +149,55 @@ const WBViewWrapper = () => { }) } + const handleChangeGroupMode = () => { + setResultsMode( + resultsMode === ResultsMode.Default + ? ResultsMode.GroupMode + : ResultsMode.Default + ) + } + const handleSubmit = ( commandInit: string = script, commandId?: Nullable, ) => { const { loading, batchSize } = state const isNewCommand = () => !commandId - const [commands, ...rest] = chunk(splitMonacoValuePerLines(commandInit), batchSize > 1 ? batchSize : 1) - const multiCommands = rest.map((command) => getMultiCommands(command)) - const commandLine = without( - commands.map((command) => removeMonacoComments(decode(command).trim())), + const getChunkSize = () => { + if (state.resultsMode === ResultsMode.GroupMode) { + return splitMonacoValuePerLines(commandInit).length + } + return batchSize > 1 ? batchSize : 1 + } + + const commandsForExecuting = without( + splitMonacoValuePerLines(commandInit) + .map((command) => removeMonacoComments(decode(command).trim())), '' ) - if (!commandLine.length || loading) { + const [commands, ...rest] = chunk(commandsForExecuting, getChunkSize()) + const multiCommands = rest.map((command) => getMultiCommands(command)) + + if (!commands?.length || loading) { setMultiCommands(multiCommands) return } isNewCommand() && scrollResults('start') - sendCommand(reverse(commandLine), multiCommands) + sendCommand(commands, multiCommands) } const sendCommand = ( commands: string[], multiCommands: string[] = [], ) => { - const { activeRunQueryMode } = state + const { activeRunQueryMode, resultsMode } = state const { connectionType, host, port } = state.instance if (connectionType !== ConnectionType.Cluster) { dispatch(sendWBCommandAction({ + resultsMode, commands, multiCommands, mode: activeRunQueryMode, @@ -191,6 +220,7 @@ const WBViewWrapper = () => { commands, options, mode: state.activeRunQueryMode, + resultsMode, multiCommands, onSuccessAction: (multiCommands) => onSuccess(multiCommands), }) @@ -222,12 +252,12 @@ const WBViewWrapper = () => { setScript('') } - const sourceValueSubmit = (value?: string, commandId?: Nullable) => { + const sourceValueSubmit = (value?: string, commandId?: Nullable, clearEditor = true) => { if (state.loading || (!value && !script)) return handleSubmit(value, commandId) setTimeout(() => { - resetCommand() + (cleanupWB && clearEditor) && resetCommand() }, 0) } @@ -244,6 +274,8 @@ const WBViewWrapper = () => { onQueryOpen={handleQueryOpen} onQueryDelete={handleQueryDelete} onQueryChangeMode={handleChangeQueryRunMode} + resultsMode={resultsMode} + onChangeGroupMode={handleChangeGroupMode} /> ) } diff --git a/redisinsight/ui/src/services/routing.ts b/redisinsight/ui/src/services/routing.ts index b53a17f29d..133ab69619 100644 --- a/redisinsight/ui/src/services/routing.ts +++ b/redisinsight/ui/src/services/routing.ts @@ -10,7 +10,7 @@ export const registerRouter = (reactRouter: any): any => { router = reactRouter } -export const getRouterLinkProps = (to: any) => { +export const getRouterLinkProps = (to: any, customOnClick?: () => void) => { const location = typeof to === 'string' ? createLocation(to, null, '', router?.history?.location) : to @@ -18,6 +18,8 @@ export const getRouterLinkProps = (to: any) => { const href = router?.history?.createHref(location) const onClick = (event: any) => { + customOnClick?.() + if (event.defaultPrevented) { return } diff --git a/redisinsight/ui/src/services/storage.ts b/redisinsight/ui/src/services/storage.ts index 622ea02b68..80c620cb8b 100644 --- a/redisinsight/ui/src/services/storage.ts +++ b/redisinsight/ui/src/services/storage.ts @@ -15,17 +15,13 @@ class StorageService { } catch (error) { console.error(`getItem from storage error: ${error}`) } - const numPatt = new RegExp(/^\d+$/) - const jsonPatt = new RegExp(/[[{].*[}\]]/) if (item) { - if (jsonPatt.test(item)) { + try { return JSON.parse(item) + } catch (e) { + return item } - if (numPatt.test(item)) { - return parseFloat(item) - } - return item } return null } diff --git a/redisinsight/ui/src/setup-env.ts b/redisinsight/ui/src/setup-env.ts new file mode 100644 index 0000000000..a256f846a4 --- /dev/null +++ b/redisinsight/ui/src/setup-env.ts @@ -0,0 +1,4 @@ +process.env.BASE_API_URL = 'http://localhost' +process.env.RESOURCES_BASE_URL = 'http://localhost' +process.env.API_PORT = '5001' +process.env.API_PREFIX = 'api' diff --git a/redisinsight/ui/src/setup-tests.ts b/redisinsight/ui/src/setup-tests.ts index 264828a905..7dbbe57dd4 100644 --- a/redisinsight/ui/src/setup-tests.ts +++ b/redisinsight/ui/src/setup-tests.ts @@ -1 +1,17 @@ import '@testing-library/jest-dom/extend-expect' +import 'whatwg-fetch' + +import { mswServer } from 'uiSrc/mocks/server' + +beforeAll(() => { + mswServer.listen() +}) + +afterEach(() => { + mswServer.resetHandlers() +}) + +afterAll(() => { + // server.printHandlers() + mswServer.close() +}) diff --git a/redisinsight/ui/src/slices/analytics/clusterDetails.ts b/redisinsight/ui/src/slices/analytics/clusterDetails.ts new file mode 100644 index 0000000000..1d02328c04 --- /dev/null +++ b/redisinsight/ui/src/slices/analytics/clusterDetails.ts @@ -0,0 +1,79 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { AxiosError } from 'axios' +import { ApiEndpoints, } from 'uiSrc/constants' +import { apiService, } from 'uiSrc/services' +import { addErrorNotification } from 'uiSrc/slices/app/notifications' +import { StateClusterDetails } from 'uiSrc/slices/interfaces/analytics' +import { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils' + +import { ClusterDetails } from 'apiSrc/modules/cluster-monitor/models/cluster-details' +import { AppDispatch, RootState } from '../store' + +export const initialState: StateClusterDetails = { + loading: false, + error: '', + data: null, +} + +const clusterDetailsSlice = createSlice({ + name: 'clusterDetails', + initialState, + reducers: { + setClusterDetailsInitialState: () => initialState, + getClusterDetails: (state) => { + state.loading = true + }, + getClusterDetailsSuccess: (state, { payload }: PayloadAction) => { + state.loading = false + state.data = payload + }, + getClusterDetailsError: (state, { payload }) => { + state.loading = false + state.error = payload + }, + } +}) + +export const clusterDetailsSelector = (state: RootState) => state.analytics.clusterDetails + +export const { + setClusterDetailsInitialState, + getClusterDetails, + getClusterDetailsSuccess, + getClusterDetailsError, +} = clusterDetailsSlice.actions + +// The reducer +export default clusterDetailsSlice.reducer + +// Asynchronous thunk action +export function fetchClusterDetailsAction( + instanceId: string, + onSuccessAction?: (data: ClusterDetails) => void, + onFailAction?: () => void, +) { + return async (dispatch: AppDispatch) => { + try { + dispatch(getClusterDetails()) + + const { data, status } = await apiService.get( + getUrl( + instanceId, + ApiEndpoints.CLUSTER_DETAILS + ) + ) + + if (isStatusSuccessful(status)) { + dispatch(getClusterDetailsSuccess(data)) + + onSuccessAction?.(data) + } + } catch (_err) { + const error = _err as AxiosError + const errorMessage = getApiErrorMessage(error) + dispatch(addErrorNotification(error)) + dispatch(getClusterDetailsError(errorMessage)) + onFailAction?.() + } + } +} diff --git a/redisinsight/ui/src/slices/analytics/settings.ts b/redisinsight/ui/src/slices/analytics/settings.ts new file mode 100644 index 0000000000..0b6068f3f4 --- /dev/null +++ b/redisinsight/ui/src/slices/analytics/settings.ts @@ -0,0 +1,28 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { AnalyticsViewTab, StateAnalyticsSettings } from 'uiSrc/slices/interfaces/analytics' +import { RootState } from 'uiSrc/slices/store' + +export const initialState: StateAnalyticsSettings = { + viewTab: AnalyticsViewTab.ClusterDetails, +} + +const analyticsSettings = createSlice({ + name: 'analyticsSettings', + initialState, + reducers: { + setInitialAnalyticsSettings: () => initialState, + + setAnalyticsViewTab: (state, { payload }: PayloadAction) => { + state.viewTab = payload + }, + } +}) + +export const { + setInitialAnalyticsSettings, + setAnalyticsViewTab, +} = analyticsSettings.actions + +export const analyticsSettingsSelector = (state: RootState) => state.analytics.settings + +export default analyticsSettings.reducer diff --git a/redisinsight/ui/src/slices/slowlog/slowlog.ts b/redisinsight/ui/src/slices/analytics/slowlog.ts similarity index 97% rename from redisinsight/ui/src/slices/slowlog/slowlog.ts rename to redisinsight/ui/src/slices/analytics/slowlog.ts index 9d78d53e98..63b07bf6ee 100644 --- a/redisinsight/ui/src/slices/slowlog/slowlog.ts +++ b/redisinsight/ui/src/slices/analytics/slowlog.ts @@ -3,7 +3,7 @@ import { AxiosError } from 'axios' import { ApiEndpoints, DEFAULT_SLOWLOG_DURATION_UNIT, DurationUnits } from 'uiSrc/constants' import { apiService, getDBConfigStorageField } from 'uiSrc/services' import { addErrorNotification } from 'uiSrc/slices/app/notifications' -import { StateSlowLog } from 'uiSrc/slices/interfaces/slowlog' +import { StateSlowLog } from 'uiSrc/slices/interfaces/analytics' import { ConfigDBStorageItem } from 'uiSrc/constants/storage' import { getApiErrorMessage, getUrl, isStatusSuccessful, Nullable } from 'uiSrc/utils' import { SlowLog, SlowLogConfig } from 'apiSrc/modules/slow-log/models' @@ -72,8 +72,8 @@ const slowLogSlice = createSlice({ } }) -export const slowLogSelector = (state: RootState) => state.slowlog -export const slowLogConfigSelector = (state: RootState) => state.slowlog.config || {} +export const slowLogSelector = (state: RootState) => state.analytics.slowlog +export const slowLogConfigSelector = (state: RootState) => state.analytics.slowlog.config || {} export const { setSlowLogInitialState, diff --git a/redisinsight/ui/src/slices/browser/hash.ts b/redisinsight/ui/src/slices/browser/hash.ts index db6f2f3815..eb742db39b 100644 --- a/redisinsight/ui/src/slices/browser/hash.ts +++ b/redisinsight/ui/src/slices/browser/hash.ts @@ -19,7 +19,7 @@ import { updateSelectedKeyRefreshTime, } from './keys' import { AppDispatch, RootState } from '../store' -import { RedisResponseBuffer, RedisString, StateHash } from '../interfaces' +import { HashField, RedisResponseBuffer, StateHash } from '../interfaces' import { addErrorNotification, addMessageNotification } from '../app/notifications' export const initialState: StateHash = { @@ -27,7 +27,7 @@ export const initialState: StateHash = { error: '', data: { total: 0, - key: '', + key: undefined, keyName: '', fields: [], nextCursor: 0, @@ -46,7 +46,7 @@ const hashSlice = createSlice({ reducers: { setHashInitialState: () => initialState, - setHashFields: (state, { payload }: PayloadAction) => { + setHashFields: (state, { payload }: PayloadAction) => { state.data.fields = payload }, @@ -137,7 +137,7 @@ const hashSlice = createSlice({ state.loading = false state.error = payload }, - removeFieldsFromList: (state, { payload }: { payload: RedisString[] }) => { + removeFieldsFromList: (state, { payload }: { payload: RedisResponseBuffer[] }) => { remove(state.data?.fields, ({ field }) => payload.findIndex((item) => isEqualBuffers(item, field)) > -1) @@ -146,7 +146,7 @@ const hashSlice = createSlice({ total: state.data.total - 1, } }, - updateFieldsInList: (state, { payload }: { payload: HashFieldDto[] }) => { + updateFieldsInList: (state, { payload }: { payload: HashField[] }) => { const newFieldsState = state.data.fields.map((listItem) => { const index = payload.findIndex( (item) => isEqualBuffers(item.field, listItem.field) diff --git a/redisinsight/ui/src/slices/browser/keys.ts b/redisinsight/ui/src/slices/browser/keys.ts index b9c434aea1..8e97bea651 100644 --- a/redisinsight/ui/src/slices/browser/keys.ts +++ b/redisinsight/ui/src/slices/browser/keys.ts @@ -11,11 +11,12 @@ import { getUrl, isStatusSuccessful, Maybe, + bufferToString, + isEqualBuffers } from 'uiSrc/utils' import { DEFAULT_SEARCH_MATCH, SCAN_COUNT_DEFAULT } from 'uiSrc/constants/api' import { getBasedOnViewTypeEvent, sendEventTelemetry, TelemetryEvent, getAdditionalAddedEventData, getMatchType } from 'uiSrc/telemetry' import successMessages from 'uiSrc/components/notifications/success-messages' -import bufferToString, { isEqualBuffers } from 'uiSrc/utils/formatters/bufferFormatters' import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' import { @@ -68,7 +69,7 @@ export const initialState: KeysStore = { error: '', data: null, length: 0, - viewFormat: defaultViewFormat, + viewFormat: localStorageService?.get(BrowserStorageItem.viewFormat) ?? defaultViewFormat, }, addKey: { loading: false, @@ -85,6 +86,9 @@ export const initialKeyInfo = { length: 0, } +const getInitialSelectedKeyState = (state: KeysStore) => + ({ ...initialState.selectedKey, viewFormat: state.selectedKey.viewFormat }) + // A slice for recipes const keysSlice = createSlice({ name: 'keys', @@ -130,6 +134,12 @@ const keysSlice = createSlice({ state.error = payload }, + setLastBatchKeys: (state, { payload }) => { + const newKeys = state.data.keys + newKeys.splice(-payload.length, payload.length, ...payload) + state.data.keys = newKeys + }, + loadKeyInfoSuccess: (state, { payload }) => { state.selectedKey = { ...state.selectedKey, @@ -206,7 +216,6 @@ const keysSlice = createSlice({ state.selectedKey = { ...state.selectedKey, loading: false, - viewFormat: defaultViewFormat, // data: null, } }, @@ -306,11 +315,17 @@ const keysSlice = createSlice({ }, resetKeyInfo: (state) => { - state.selectedKey = cloneDeep(initialState.selectedKey) + state.selectedKey = cloneDeep(getInitialSelectedKeyState(state as KeysStore)) }, // reset keys for keys slice - resetKeys: () => cloneDeep(initialState), + resetKeys: (state) => cloneDeep( + { + ...initialState, + viewType: localStorageService?.get(BrowserStorageItem.browserViewType) ?? KeyViewType.Browser, + selectedKey: getInitialSelectedKeyState(state as KeysStore) + } + ), resetKeysData: (state) => { // state.data.keys = [] @@ -327,6 +342,7 @@ const keysSlice = createSlice({ setViewFormat: (state, { payload }: PayloadAction) => { state.selectedKey.viewFormat = payload + localStorageService?.set(BrowserStorageItem.viewFormat, payload) } }, }) @@ -347,6 +363,7 @@ export const { defaultSelectedKeyAction, defaultSelectedKeyActionSuccess, defaultSelectedKeyActionFailure, + setLastBatchKeys, addKey, addKeySuccess, addKeyFailure, @@ -384,8 +401,6 @@ export let sourceKeysFetch: Nullable = null export function setInitialStateByType(type: string) { return (dispatch: AppDispatch) => { - dispatch(setViewFormat(defaultViewFormat)) - if (type === KeyTypes.Hash) { dispatch(setHashInitialState()) } diff --git a/redisinsight/ui/src/slices/browser/list.ts b/redisinsight/ui/src/slices/browser/list.ts index 58cf3ce175..fb53f850c0 100644 --- a/redisinsight/ui/src/slices/browser/list.ts +++ b/redisinsight/ui/src/slices/browser/list.ts @@ -10,7 +10,6 @@ import { getApiErrorMessage, isStatusSuccessful, Maybe, - bufferToString, } from 'uiSrc/utils' import { SetListElementDto, @@ -33,7 +32,7 @@ import { import { StateList } from '../interfaces/list' import { AppDispatch, RootState } from '../store' import { addErrorNotification, addMessageNotification } from '../app/notifications' -import { RedisResponseBuffer, RedisString } from '../interfaces' +import { RedisResponseBuffer } from '../interfaces' export const initialState: StateList = { loading: false, @@ -60,7 +59,7 @@ const listSlice = createSlice({ reducers: { setListInitialState: () => initialState, - setListElements: (state, { payload } : PayloadAction) => { + setListElements: (state, { payload } : PayloadAction) => { state.data.elements = payload }, // load List elements diff --git a/redisinsight/ui/src/slices/browser/set.ts b/redisinsight/ui/src/slices/browser/set.ts index d87d8ec104..c1a4a3cb41 100644 --- a/redisinsight/ui/src/slices/browser/set.ts +++ b/redisinsight/ui/src/slices/browser/set.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { first, remove } from 'lodash' +import { remove } from 'lodash' import { apiService } from 'uiSrc/services' import { ApiEndpoints } from 'uiSrc/constants' @@ -19,7 +19,7 @@ import { updateSelectedKeyRefreshTime, } from './keys' import { AppDispatch, RootState } from '../store' -import { InitialStateSet, RedisResponseBuffer, RedisString } from '../interfaces' +import { InitialStateSet, RedisResponseBuffer } from '../interfaces' import { addErrorNotification, addMessageNotification } from '../app/notifications' export const initialState: InitialStateSet = { @@ -27,7 +27,7 @@ export const initialState: InitialStateSet = { error: '', data: { total: 0, - key: '', + key: undefined, keyName: '', members: [], nextCursor: 0, @@ -41,7 +41,7 @@ const setSlice = createSlice({ initialState, reducers: { - setSetMembers: (state, { payload }: PayloadAction) => { + setSetMembers: (state, { payload }: PayloadAction) => { state.data.members = payload }, // load Set members diff --git a/redisinsight/ui/src/slices/browser/zset.ts b/redisinsight/ui/src/slices/browser/zset.ts index a653b41cd2..3ce9c6551d 100644 --- a/redisinsight/ui/src/slices/browser/zset.ts +++ b/redisinsight/ui/src/slices/browser/zset.ts @@ -1,4 +1,4 @@ -import { cloneDeep, first, isNull, remove } from 'lodash' +import { cloneDeep, isNull, remove } from 'lodash' import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { apiService } from 'uiSrc/services' @@ -31,7 +31,7 @@ export const initialState: StateZset = { error: '', data: { total: 0, - key: '', + key: undefined, keyName: '', members: [], nextCursor: 0, diff --git a/redisinsight/ui/src/slices/cli/cli-settings.ts b/redisinsight/ui/src/slices/cli/cli-settings.ts index 97894a09ee..920ade26eb 100644 --- a/redisinsight/ui/src/slices/cli/cli-settings.ts +++ b/redisinsight/ui/src/slices/cli/cli-settings.ts @@ -182,6 +182,7 @@ export default cliSettingsSlice.reducer // Asynchronous thunk action export function createCliClientAction( + instanceId: string, onWorkbenchClick: () => void, onSuccessAction?: () => void, onFailAction?: (message: string) => void @@ -195,7 +196,7 @@ export function createCliClientAction( try { const { data, status } = await apiService.post( - getUrl(state.connections.instances.connectedInstance?.id ?? '', ApiEndpoints.CLI) + getUrl(instanceId ?? '', ApiEndpoints.CLI) ) if (isStatusSuccessful(status)) { diff --git a/redisinsight/ui/src/slices/interfaces/analytics.ts b/redisinsight/ui/src/slices/interfaces/analytics.ts new file mode 100644 index 0000000000..b129e06354 --- /dev/null +++ b/redisinsight/ui/src/slices/interfaces/analytics.ts @@ -0,0 +1,28 @@ +import { DurationUnits } from 'uiSrc/constants' +import { Nullable } from 'uiSrc/utils' +import { SlowLog, SlowLogConfig } from 'apiSrc/modules/slow-log/models' +import { ClusterDetails } from 'apiSrc/modules/cluster-monitor/models/cluster-details' + +export interface StateSlowLog { + loading: boolean + error: string + data: SlowLog[] + lastRefreshTime: Nullable, + config: Nullable, + durationUnit: DurationUnits +} + +export interface StateClusterDetails { + loading: boolean + error: string + data: Nullable +} + +export interface StateAnalyticsSettings { + viewTab: AnalyticsViewTab +} + +export enum AnalyticsViewTab { + ClusterDetails = 'ClusterDetails', + SlowLog = 'SlowLog', +} diff --git a/redisinsight/ui/src/slices/interfaces/hash.ts b/redisinsight/ui/src/slices/interfaces/hash.ts index 4449afd0d6..2669d0bd8d 100644 --- a/redisinsight/ui/src/slices/interfaces/hash.ts +++ b/redisinsight/ui/src/slices/interfaces/hash.ts @@ -1,11 +1,21 @@ +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces/app' import { ModifiedGetHashMembersResponse } from 'uiSrc/slices/interfaces/instances' +export interface HashField { + field: RedisResponseBuffer + value: RedisResponseBuffer +} + +export interface StateHashData extends ModifiedGetHashMembersResponse { + fields: HashField[] +} + export interface StateHash { - loading: boolean; - error: string; - data: ModifiedGetHashMembersResponse; + loading: boolean + error: string + data: StateHashData updateValue: { - loading: boolean; - error: string; - }; + loading: boolean + error: string + } } diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index 9f216f7f51..ccb2f04e95 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -1,3 +1,4 @@ +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces/app' import { Maybe, Nullable } from 'uiSrc/utils' import { GetHashFieldsResponse } from 'apiSrc/modules/browser/dto/hash.dto' import { GetSetMembersResponse } from 'apiSrc/modules/browser/dto/set.dto' @@ -143,6 +144,13 @@ export enum RedisCustomModulesName { IpTables = 'iptables-input-filter', } +const RediSearchModulesText = [ + RedisDefaultModules.Search, + RedisDefaultModules.SearchLight, + RedisDefaultModules.FT, + RedisDefaultModules.FTL +].reduce((prev, next) => ({ ...prev, [next]: 'RediSearch' }), {}) + // Enums don't allow to use dynamic key export const DATABASE_LIST_MODULES_TEXT = Object.freeze({ [RedisDefaultModules.AI]: 'RedisAI', @@ -150,13 +158,10 @@ export const DATABASE_LIST_MODULES_TEXT = Object.freeze({ [RedisDefaultModules.Gears]: 'RedisGears', [RedisDefaultModules.Bloom]: 'RedisBloom', [RedisDefaultModules.ReJSON]: 'RedisJSON', - [RedisDefaultModules.Search]: 'RediSearch', - [RedisDefaultModules.SearchLight]: 'RediSearch Light', - [RedisDefaultModules.FT]: 'RediSearch', - [RedisDefaultModules.FTL]: 'RediSearch Light', [RedisDefaultModules.TimeSeries]: 'RedisTimeSeries', [RedisCustomModulesName.Proto]: 'redis-protobuf', - [RedisCustomModulesName.IpTables]: 'RedisPushIpTables' + [RedisCustomModulesName.IpTables]: 'RedisPushIpTables', + ...RediSearchModulesText }) export enum AddRedisClusterDatabaseOptions { @@ -316,17 +321,17 @@ export interface ILoadedSentinel { } export interface ModifiedGetSetMembersResponse extends GetSetMembersResponse { - key?: string + key?: RedisResponseBuffer match?: string } export interface ModifiedZsetMembersResponse extends SearchZSetMembersResponse { - key?: string + key?: RedisResponseBuffer match?: string } export interface ModifiedGetHashMembersResponse extends GetHashFieldsResponse { - key?: string + key?: RedisResponseBuffer match?: string } diff --git a/redisinsight/ui/src/slices/interfaces/slowlog.ts b/redisinsight/ui/src/slices/interfaces/slowlog.ts deleted file mode 100644 index bde1f4ee50..0000000000 --- a/redisinsight/ui/src/slices/interfaces/slowlog.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { SlowLog, SlowLogConfig } from 'apiSrc/modules/slow-log/models' -import { DurationUnits } from 'uiSrc/constants' -import { Nullable } from 'uiSrc/utils' - -export interface StateSlowLog { - loading: boolean - error: string - data: SlowLog[] - lastRefreshTime: Nullable, - config: Nullable, - durationUnit: DurationUnits -} diff --git a/redisinsight/ui/src/slices/interfaces/user.ts b/redisinsight/ui/src/slices/interfaces/user.ts index 79c9df813b..f38b1a3a7e 100644 --- a/redisinsight/ui/src/slices/interfaces/user.ts +++ b/redisinsight/ui/src/slices/interfaces/user.ts @@ -2,9 +2,12 @@ import { Nullable } from 'uiSrc/utils' import { GetAgreementsSpecResponse, GetAppSettingsResponse } from 'apiSrc/dto/settings.dto' export interface StateUserSettings { - loading: boolean; - error: string; - isShowConceptsPopup: boolean; - config: Nullable; - spec: Nullable; + loading: boolean + error: string + isShowConceptsPopup: boolean + config: Nullable + spec: Nullable + workbench: { + cleanup: boolean + } } diff --git a/redisinsight/ui/src/slices/interfaces/workbench.ts b/redisinsight/ui/src/slices/interfaces/workbench.ts index e46a76351e..96d3660416 100644 --- a/redisinsight/ui/src/slices/interfaces/workbench.ts +++ b/redisinsight/ui/src/slices/interfaces/workbench.ts @@ -10,6 +10,7 @@ export interface StateWorkbenchSettings { export interface StateWorkbenchResults { loading: boolean + processing: boolean error: string items: CommandExecutionUI[] } @@ -38,9 +39,21 @@ export interface CommandExecutionUI extends Partial { loading?: boolean isOpen?: boolean error?: string + emptyCommand: boolean } export enum RunQueryMode { Raw = 'RAW', ASCII = 'ASCII', } + +export enum ResultsMode { + Default = 'DEFAULT', + GroupMode = 'GROUP_MODE', +} + +export interface ResultsSummary { + total: number + success: number + fail: number +} diff --git a/redisinsight/ui/src/slices/interfaces/zset.ts b/redisinsight/ui/src/slices/interfaces/zset.ts index 5a6cd335db..fac7508605 100644 --- a/redisinsight/ui/src/slices/interfaces/zset.ts +++ b/redisinsight/ui/src/slices/interfaces/zset.ts @@ -1,16 +1,23 @@ +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces/app' import { ModifiedZsetMembersResponse } from 'uiSrc/slices/interfaces/instances' +export interface ZsetMember { + name: RedisResponseBuffer + score: number +} + export interface StateZsetData extends ModifiedZsetMembersResponse { - sortOrder?: string; + sortOrder?: string + members: ZsetMember[] } export interface StateZset { - loading: boolean; - searching: boolean; - error: string; - data: StateZsetData; + loading: boolean + searching: boolean + error: string + data: StateZsetData updateScore: { - loading: boolean; - error: string; + loading: boolean + error: string }; } diff --git a/redisinsight/ui/src/slices/store.ts b/redisinsight/ui/src/slices/store.ts index c6165994ab..2a923f4c23 100644 --- a/redisinsight/ui/src/slices/store.ts +++ b/redisinsight/ui/src/slices/store.ts @@ -30,8 +30,10 @@ import workbenchResultsReducer from './workbench/wb-results' import workbenchGuidesReducer from './workbench/wb-guides' import workbenchTutorialsReducer from './workbench/wb-tutorials' import contentCreateRedisButtonReducer from './content/create-redis-buttons' -import slowLogReducer from './slowlog/slowlog' import pubSubReducer from './pubsub/pubsub' +import slowLogReducer from './analytics/slowlog' +import analyticsSettingsReducer from './analytics/settings' +import clusterDetailsReducer from './analytics/clusterDetails' export const history = createBrowserHistory() @@ -79,7 +81,11 @@ export const rootReducer = combineReducers({ content: combineReducers({ createRedisButtons: contentCreateRedisButtonReducer, }), - slowlog: slowLogReducer, + analytics: combineReducers({ + settings: analyticsSettingsReducer, + slowlog: slowLogReducer, + clusterDetails: clusterDetailsReducer, + }), pubsub: pubSubReducer, }) diff --git a/redisinsight/ui/src/slices/tests/analytics/clusterDetails.spec.ts b/redisinsight/ui/src/slices/tests/analytics/clusterDetails.spec.ts new file mode 100644 index 0000000000..b81560bce7 --- /dev/null +++ b/redisinsight/ui/src/slices/tests/analytics/clusterDetails.spec.ts @@ -0,0 +1,167 @@ +import { cloneDeep } from 'lodash' +import { AxiosError } from 'axios' +import { apiService } from 'uiSrc/services' +import { cleanup, mockedStore, initialStateDefault } from 'uiSrc/utils/test-utils' +import { addErrorNotification } from 'uiSrc/slices/app/notifications' + +import { DEFAULT_ERROR_MESSAGE } from 'uiSrc/utils' +import { CLUSTER_DETAILS_DATA_MOCK, INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers' +import reducer, { + initialState, + getClusterDetails, + getClusterDetailsSuccess, + getClusterDetailsError, + clusterDetailsSelector, + setClusterDetailsInitialState, + fetchClusterDetailsAction, +} from '../../analytics/clusterDetails' + +let store: typeof mockedStore + +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('clusterDetails slice', () => { + describe('reducer, actions and selectors', () => { + it('should return the initial state on first run', () => { + // Arrange + const nextState = initialState + + // Act + const result = reducer(undefined, {}) + + // Assert + expect(result).toEqual(nextState) + }) + }) + + describe('setUserSettingsInitialState', () => { + it('should properly set the initial state', () => { + // Arrange + const state = { + ...initialState + } + + // Act + const nextState = reducer(initialState, setClusterDetailsInitialState()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + analytics: { clusterDetails: nextState }, + }) + expect(clusterDetailsSelector(rootState)).toEqual(state) + }) + }) + + describe('getClusterDetails', () => { + it('should properly set state before the fetch data', () => { + // Arrange + const state = { + ...initialState, + loading: true + } + + // Act + const nextState = reducer(initialState, getClusterDetails()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + analytics: { clusterDetails: nextState }, + }) + expect(clusterDetailsSelector(rootState)).toEqual(state) + }) + }) + + describe('getClusterDetailsSuccess', () => { + it('should properly set state after success fetch data', () => { + // Arrange + const state = { + ...initialState, + loading: false, + data: CLUSTER_DETAILS_DATA_MOCK, + } + + // Act + const nextState = reducer(initialState, getClusterDetailsSuccess(CLUSTER_DETAILS_DATA_MOCK)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + analytics: { clusterDetails: nextState }, + }) + expect(clusterDetailsSelector(rootState)).toEqual(state) + }) + }) + + describe('getClusterDetailsError', () => { + it('should properly set state after failed fetch data', () => { + // Arrange + const error = 'Some error' + const state = { + ...initialState, + loading: false, + error + } + + // Act + const nextState = reducer(initialState, getClusterDetailsError(error)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + analytics: { clusterDetails: nextState }, + }) + expect(clusterDetailsSelector(rootState)).toEqual(state) + }) + }) + + // thunks + describe('thunks', () => { + describe('fetchClusterDetailsAction', () => { + it('succeed to fetch data', async () => { + // Act + + const responsePayload = { + status: 200, + data: CLUSTER_DETAILS_DATA_MOCK + } + + apiService.get = jest.fn().mockResolvedValue(responsePayload) + + await store.dispatch(fetchClusterDetailsAction(INSTANCE_ID_MOCK)) + + // Assert + const expectedActions = [ + getClusterDetails(), + getClusterDetailsSuccess(CLUSTER_DETAILS_DATA_MOCK), + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('failed to fetch data', async () => { + const responsePayload = { + response: { + status: 500, + data: { message: DEFAULT_ERROR_MESSAGE }, + }, + } + + apiService.get = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch(fetchClusterDetailsAction(INSTANCE_ID_MOCK)) + + // Assert + const expectedActions = [ + getClusterDetails(), + addErrorNotification(responsePayload as AxiosError), + getClusterDetailsError(DEFAULT_ERROR_MESSAGE) + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + }) + }) +}) diff --git a/redisinsight/ui/src/slices/tests/analytics/settings.spec.ts b/redisinsight/ui/src/slices/tests/analytics/settings.spec.ts new file mode 100644 index 0000000000..626d5e66d9 --- /dev/null +++ b/redisinsight/ui/src/slices/tests/analytics/settings.spec.ts @@ -0,0 +1,42 @@ +import { initialStateDefault } from 'uiSrc/utils/test-utils' +import { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics' + +import reducer, { + analyticsSettingsSelector, + initialState, + setAnalyticsViewTab, +} from '../../analytics/settings' + +describe('analytics settings slice', () => { + describe('reducer, actions and selectors', () => { + it('should return the initial state on first run', () => { + // Arrange + const nextState = initialState + + // Act + const result = reducer(undefined, {}) + + // Assert + expect(result).toEqual(nextState) + }) + }) + + describe('setAnalyticsViewTab', () => { + it('should properly set the AnalyticsViewTab.SlowLog', () => { + // Arrange + const state = { + ...initialState, + viewTab: AnalyticsViewTab.SlowLog + } + + // Act + const nextState = reducer(initialState, setAnalyticsViewTab(AnalyticsViewTab.SlowLog)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + analytics: { settings: nextState }, + }) + expect(analyticsSettingsSelector(rootState)).toEqual(state) + }) + }) +}) diff --git a/redisinsight/ui/src/slices/tests/slowlog/slowlog.spec.ts b/redisinsight/ui/src/slices/tests/analytics/slowlog.spec.ts similarity index 95% rename from redisinsight/ui/src/slices/tests/slowlog/slowlog.spec.ts rename to redisinsight/ui/src/slices/tests/analytics/slowlog.spec.ts index a6cef02ba9..e43759d067 100644 --- a/redisinsight/ui/src/slices/tests/slowlog/slowlog.spec.ts +++ b/redisinsight/ui/src/slices/tests/analytics/slowlog.spec.ts @@ -1,12 +1,12 @@ import { cloneDeep } from 'lodash' import { AxiosError } from 'axios' -import { SlowLog, SlowLogConfig } from 'src/modules/slow-log/models' import { DEFAULT_SLOWLOG_DURATION_UNIT } from 'uiSrc/constants' - import { apiService } from 'uiSrc/services' import { cleanup, mockedStore, initialStateDefault } from 'uiSrc/utils/test-utils' import { addErrorNotification } from 'uiSrc/slices/app/notifications' +import { SlowLog, SlowLogConfig } from 'apiSrc/modules/slow-log/models' + import reducer, { initialState, getSlowLogConfig, @@ -24,7 +24,7 @@ import reducer, { patchSlowLogConfigAction, setSlowLogInitialState, slowLogSelector -} from '../../slowlog/slowlog' +} from '../../analytics/slowlog' const timestamp = 1629128049027 let store: typeof mockedStore @@ -70,7 +70,7 @@ describe('slowLog slice', () => { // Assert const rootState = Object.assign(initialStateDefault, { - slowlog: nextState, + analytics: { slowlog: nextState }, }) expect(slowLogSelector(rootState)).toEqual(state) }) @@ -89,7 +89,7 @@ describe('slowLog slice', () => { // Assert const rootState = Object.assign(initialStateDefault, { - slowlog: nextState, + analytics: { slowlog: nextState }, }) expect(slowLogSelector(rootState)).toEqual(state) }) @@ -120,7 +120,7 @@ describe('slowLog slice', () => { // Assert const rootState = Object.assign(initialStateDefault, { - slowlog: nextState, + analytics: { slowlog: nextState }, }) expect(slowLogSelector(rootState)).toEqual(state) }) @@ -141,7 +141,7 @@ describe('slowLog slice', () => { // Assert const rootState = Object.assign(initialStateDefault, { - slowlog: nextState, + analytics: { slowlog: nextState }, }) expect(slowLogSelector(rootState)).toEqual(state) }) @@ -160,7 +160,7 @@ describe('slowLog slice', () => { // Assert const rootState = Object.assign(initialStateDefault, { - slowlog: nextState, + analytics: { slowlog: nextState }, }) expect(slowLogSelector(rootState)).toEqual(state) }) @@ -180,7 +180,7 @@ describe('slowLog slice', () => { // Assert const rootState = Object.assign(initialStateDefault, { - slowlog: nextState, + analytics: { slowlog: nextState }, }) expect(slowLogSelector(rootState)).toEqual(state) }) @@ -201,7 +201,7 @@ describe('slowLog slice', () => { // Assert const rootState = Object.assign(initialStateDefault, { - slowlog: nextState, + analytics: { slowlog: nextState }, }) expect(slowLogSelector(rootState)).toEqual(state) }) @@ -220,7 +220,7 @@ describe('slowLog slice', () => { // Assert const rootState = Object.assign(initialStateDefault, { - slowlog: nextState, + analytics: { slowlog: nextState }, }) expect(slowLogSelector(rootState)).toEqual(state) }) @@ -244,7 +244,7 @@ describe('slowLog slice', () => { // Assert const rootState = Object.assign(initialStateDefault, { - slowlog: nextState, + analytics: { slowlog: nextState }, }) expect(slowLogSelector(rootState)).toEqual(state) }) @@ -265,7 +265,7 @@ describe('slowLog slice', () => { // Assert const rootState = Object.assign(initialStateDefault, { - slowlog: nextState, + analytics: { slowlog: nextState }, }) expect(slowLogSelector(rootState)).toEqual(state) }) diff --git a/redisinsight/ui/src/slices/tests/app/context.spec.ts b/redisinsight/ui/src/slices/tests/app/context.spec.ts index 88df65b542..054d13b672 100644 --- a/redisinsight/ui/src/slices/tests/app/context.spec.ts +++ b/redisinsight/ui/src/slices/tests/app/context.spec.ts @@ -34,7 +34,9 @@ import reducer, { setBrowserTreeDelimiter } from '../../app/context' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore beforeEach(() => { diff --git a/redisinsight/ui/src/slices/tests/app/info.spec.ts b/redisinsight/ui/src/slices/tests/app/info.spec.ts index db38aaf25e..44b7f9268d 100644 --- a/redisinsight/ui/src/slices/tests/app/info.spec.ts +++ b/redisinsight/ui/src/slices/tests/app/info.spec.ts @@ -7,6 +7,10 @@ import { } from 'uiSrc/utils/test-utils' import { apiService } from 'uiSrc/services' +import { mswServer } from 'uiSrc/mocks/server' +import { errorHandlers } from 'uiSrc/mocks/res/responseComposition' +import { DEFAULT_ERROR_MESSAGE } from 'uiSrc/utils' +import { APP_INFO_DATA_MOCK } from 'uiSrc/mocks/handlers/app/infoHandlers' import reducer, { initialState, setAnalyticsIdentified, @@ -139,22 +143,14 @@ describe('slices', () => { describe('getServerInfoSuccess', () => { it('should properly set state after success', () => { - // Arrange - const data = { - id: 'id1', - createDateTime: '2000-01-01T00:00:00.000Z', - appVersion: '2.0.0', - osPlatform: 'win32', - buildType: 'ELECTRON' - } const state = { ...initialState, loading: false, - server: data + server: APP_INFO_DATA_MOCK } // Act - const nextState = reducer(initialState, getServerInfoSuccess(data)) + const nextState = reducer(initialState, getServerInfoSuccess(APP_INFO_DATA_MOCK)) // Assert const rootState = Object.assign(initialStateDefault, { @@ -190,40 +186,20 @@ describe('slices', () => { // thunks describe('fetchServerInfo', () => { it('succeed to fetch server info', async () => { - // Arrange - const data = { - id: 'id1', - createDateTime: '2000-01-01T00:00:00.000Z', - appVersion: '2.0.0', - osPlatform: 'win32', - buildType: 'ELECTRON' - } - const responsePayload = { status: 200, data } - - apiService.get = jest.fn().mockResolvedValue(responsePayload) - // Act await store.dispatch(fetchServerInfo(jest.fn())) // Assert const expectedActions = [ getServerInfo(), - getServerInfoSuccess(data), + getServerInfoSuccess(APP_INFO_DATA_MOCK), ] expect(mockedStore.getActions()).toEqual(expectedActions) }) it('failed to fetch server info', async () => { - // Arrange - const errorMessage = 'Something was wrong!' - const responsePayload = { - response: { - status: 500, - data: { message: errorMessage }, - }, - } - apiService.get = jest.fn().mockRejectedValue(responsePayload) + mswServer.use(...errorHandlers) // Act await store.dispatch(fetchServerInfo(jest.fn(), jest.fn())) @@ -231,7 +207,7 @@ describe('slices', () => { // Assert const expectedActions = [ getServerInfo(), - getServerInfoFailure(errorMessage), + getServerInfoFailure(DEFAULT_ERROR_MESSAGE), ] expect(mockedStore.getActions()).toEqual(expectedActions) diff --git a/redisinsight/ui/src/slices/tests/app/notifications.spec.ts b/redisinsight/ui/src/slices/tests/app/notifications.spec.ts index cd3a36c33d..a6dc94b4bb 100644 --- a/redisinsight/ui/src/slices/tests/app/notifications.spec.ts +++ b/redisinsight/ui/src/slices/tests/app/notifications.spec.ts @@ -33,7 +33,9 @@ import reducer, { setNewNotificationAction } from '../../app/notifications' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore beforeEach(() => { diff --git a/redisinsight/ui/src/slices/tests/browser/hash.spec.ts b/redisinsight/ui/src/slices/tests/browser/hash.spec.ts index 71ff6b31e3..8372a53d03 100644 --- a/redisinsight/ui/src/slices/tests/browser/hash.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/hash.spec.ts @@ -43,7 +43,9 @@ import reducer, { } from '../../browser/hash' import { addErrorNotification, addMessageNotification } from '../../app/notifications' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore let dateNow: jest.SpyInstance diff --git a/redisinsight/ui/src/slices/tests/browser/keys.spec.ts b/redisinsight/ui/src/slices/tests/browser/keys.spec.ts index 8740dd19eb..cade1f38b1 100644 --- a/redisinsight/ui/src/slices/tests/browser/keys.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/keys.spec.ts @@ -1,6 +1,6 @@ import { cloneDeep } from 'lodash' import { AxiosError } from 'axios' -import { KeyTypes } from 'uiSrc/constants' +import { KeyTypes, KeyValueFormat } from 'uiSrc/constants' import { apiService } from 'uiSrc/services' import { parseKeysListResponse, stringToBuffer } from 'uiSrc/utils' import { cleanup, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' @@ -54,11 +54,16 @@ import reducer, { addListKey, addStringKey, addZsetKey, + setLastBatchKeys, updateSelectedKeyRefreshTime, + resetKeyInfo, + resetKeys, } from '../../browser/keys' import { getString } from '../../browser/string' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore let dateNow: jest.SpyInstance @@ -373,6 +378,39 @@ describe('keys slice', () => { }) }) + describe('setLastBatchKeys', () => { + it('should properly set the state', () => { + // Arrange + const strToKey = (name:string) => ({ name, nameString: name, ttl: 1, size: 1, type: 'hash' }) + const data = ['44', '55', '66'].map(strToKey) + + const state = { + ...initialState, + data: { + ...initialState.data, + keys: ['1', '2', '3', '44', '55', '66'].map(strToKey), + } + } + + const prevState = { + ...initialState, + data: { + ...initialState.data, + keys: ['1', '2', '3', '4', '5', '6'].map(strToKey), + } + } + + // Act + const nextState = reducer(prevState, setLastBatchKeys(data)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { keys: nextState }, + }) + expect(keysSelector(rootState)).toEqual(state) + }) + }) + describe('refreshKeyInfo', () => { it('should properly set the state', () => { // Arrange @@ -744,6 +782,52 @@ describe('keys slice', () => { }) }) + describe('resetKeyInfo', () => { + it('should properly save viewFormat', () => { + // Arrange + const viewFormat = KeyValueFormat.HEX + const initialStateMock = { + ...initialState, + selectedKey: { + ...initialState.selectedKey, + viewFormat + } + } + + // Act + const nextState = reducer(initialStateMock, resetKeyInfo()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { keys: nextState }, + }) + expect(keysSelector(rootState)).toEqual(initialStateMock) + }) + }) + + describe('resetKeys', () => { + it('should properly save viewFormat', () => { + // Arrange + const viewFormat = KeyValueFormat.HEX + const initialStateMock = { + ...initialState, + selectedKey: { + ...initialState.selectedKey, + viewFormat + } + } + + // Act + const nextState = reducer(initialStateMock, resetKeys()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { keys: nextState }, + }) + expect(keysSelector(rootState)).toEqual(initialStateMock) + }) + }) + describe('thunks', () => { describe('fetchKeys', () => { it('call both loadKeys and loadKeysSuccess when fetch is successed', async () => { diff --git a/redisinsight/ui/src/slices/tests/browser/list.spec.ts b/redisinsight/ui/src/slices/tests/browser/list.spec.ts index 443b1c0dad..1d761fe958 100644 --- a/redisinsight/ui/src/slices/tests/browser/list.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/list.spec.ts @@ -51,7 +51,9 @@ import reducer, { } from '../../browser/list' import { addErrorNotification, addMessageNotification } from '../../app/notifications' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore let dateNow: jest.SpyInstance @@ -187,7 +189,7 @@ describe('list slice', () => { const data = { keyName: 'list', key: 'list', - elements: ['1', '23', '432'].map((element, i) => (stringToBuffer(element))), + elements: ['1', '23', '432'].map((element) => (stringToBuffer(element))), // elements: ['1', '23', '432'].map((element, i) => ({ element: stringToBuffer(element), index: i })), total: 1, } diff --git a/redisinsight/ui/src/slices/tests/browser/rejson.spec.ts b/redisinsight/ui/src/slices/tests/browser/rejson.spec.ts index 0ca9036c16..390b4ae6ce 100644 --- a/redisinsight/ui/src/slices/tests/browser/rejson.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/rejson.spec.ts @@ -33,7 +33,9 @@ import reducer, { import { addErrorNotification, addMessageNotification } from '../../app/notifications' import { refreshKeyInfo } from '../../browser/keys' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore let storeWithSelectedKey: typeof mockedStore diff --git a/redisinsight/ui/src/slices/tests/browser/set.spec.ts b/redisinsight/ui/src/slices/tests/browser/set.spec.ts index 553e9de75c..e75f43f663 100644 --- a/redisinsight/ui/src/slices/tests/browser/set.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/set.spec.ts @@ -39,7 +39,9 @@ import reducer, { deleteSetMembers, } from '../../browser/set' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore let dateNow: jest.SpyInstance @@ -123,7 +125,7 @@ describe('set slice', () => { it('should properly set the state with empty data', () => { // Arrange const data: any = { - keyName: '' + keyName: 'key' } const state = { @@ -131,7 +133,8 @@ describe('set slice', () => { error: '', data: { ...initialState.data, - ...data + ...data, + key: data.keyName }, } @@ -155,7 +158,7 @@ describe('set slice', () => { error: data, data: { total: 0, - key: '', + key: undefined, keyName: '', members: [], nextCursor: 0, @@ -182,7 +185,7 @@ describe('set slice', () => { error: '', data: { total: 0, - key: '', + key: undefined, keyName: '', members: [], nextCursor: 0, @@ -206,7 +209,7 @@ describe('set slice', () => { // Arrange const data = { - key: '', + key: undefined, keyName: '', nextCursor: 0, total: 0, @@ -269,7 +272,7 @@ describe('set slice', () => { error: data, data: { total: 0, - key: '', + key: undefined, keyName: '', members: [], nextCursor: 0, @@ -398,7 +401,7 @@ describe('set slice', () => { error: data, data: { total: 0, - key: '', + key: undefined, keyName: '', members: [], nextCursor: 0, diff --git a/redisinsight/ui/src/slices/tests/browser/stream.spec.ts b/redisinsight/ui/src/slices/tests/browser/stream.spec.ts index 237b7d82c3..91e4e177cd 100644 --- a/redisinsight/ui/src/slices/tests/browser/stream.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/stream.spec.ts @@ -75,7 +75,9 @@ import { StreamViewType } from 'uiSrc/slices/interfaces/stream' import { bufferToString, stringToBuffer } from 'uiSrc/utils' import { addErrorNotification, addMessageNotification } from '../../app/notifications' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore diff --git a/redisinsight/ui/src/slices/tests/browser/string.spec.ts b/redisinsight/ui/src/slices/tests/browser/string.spec.ts index ae298c5ada..70eafd4339 100644 --- a/redisinsight/ui/src/slices/tests/browser/string.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/string.spec.ts @@ -25,7 +25,10 @@ beforeEach(() => { store = cloneDeep(mockedStore) store.clearActions() }) -jest.mock('uiSrc/services') + +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) describe('string slice', () => { describe('reducer, actions and selectors', () => { diff --git a/redisinsight/ui/src/slices/tests/browser/zset.spec.ts b/redisinsight/ui/src/slices/tests/browser/zset.spec.ts index 04b4b119f0..639cc2412e 100644 --- a/redisinsight/ui/src/slices/tests/browser/zset.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/zset.spec.ts @@ -54,7 +54,9 @@ import reducer, { refreshZsetMembersAction, } from '../../browser/zset' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore let dateNow: jest.SpyInstance @@ -418,7 +420,7 @@ describe('zset slice', () => { // Arrange const data = { - key: '', + key: undefined, keyName: '', match: '', nextCursor: 0, diff --git a/redisinsight/ui/src/slices/tests/cli/cli-output.spec.ts b/redisinsight/ui/src/slices/tests/cli/cli-output.spec.ts index 71b0cefa88..bb5fe89847 100644 --- a/redisinsight/ui/src/slices/tests/cli/cli-output.spec.ts +++ b/redisinsight/ui/src/slices/tests/cli/cli-output.spec.ts @@ -8,7 +8,7 @@ import { apiService } from 'uiSrc/services' import { cliTexts } from 'uiSrc/constants/cliOutput' import { cliParseTextResponseWithOffset, cliParseTextResponseWithRedirect } from 'uiSrc/utils/cliHelper' import ApiErrors from 'uiSrc/constants/apiErrors' -import { updateCliClientAction } from 'uiSrc/slices/cli/cli-settings' +import { processCliClient, updateCliClientAction } from 'uiSrc/slices/cli/cli-settings' import reducer, { concatToOutput, initialState, @@ -23,7 +23,9 @@ import reducer, { updateCliCommandHistory, } from '../../cli/cli-output' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) jest.mock('uiSrc/slices/cli/cli-settings', () => ({ ...jest.requireActual('uiSrc/slices/cli/cli-settings'), updateCliClientAction: jest.fn() @@ -352,10 +354,8 @@ describe('cliOutput slice', () => { sendCliCommand(), sendCliCommandFailure(responsePayload.response.data.message), concatToOutput(cliParseTextResponseWithOffset(errorMessage, command, CommandExecutionStatus.Fail)), - concatToOutput(['\n']), - concatToOutput(['\n']) + processCliClient(), ] - expect(updateCliClientAction).toHaveBeenCalled() expect(clearStoreActions(tempStore.getActions())).toEqual(clearStoreActions(expectedActions)) }) }) @@ -482,10 +482,8 @@ describe('cliOutput slice', () => { sendCliCommand(), sendCliCommandFailure(responsePayload.response.data.message), concatToOutput(cliParseTextResponseWithOffset(errorMessage, command, CommandExecutionStatus.Fail)), - concatToOutput(['\n']), - concatToOutput(['\n']) + processCliClient(), ] - expect(updateCliClientAction).toHaveBeenCalled() expect(clearStoreActions(tempStore.getActions())).toEqual(clearStoreActions(expectedActions)) }) }) diff --git a/redisinsight/ui/src/slices/tests/cli/cli-settings.spec.ts b/redisinsight/ui/src/slices/tests/cli/cli-settings.spec.ts index 2b383d0c36..e06b8e6851 100644 --- a/redisinsight/ui/src/slices/tests/cli/cli-settings.spec.ts +++ b/redisinsight/ui/src/slices/tests/cli/cli-settings.spec.ts @@ -7,6 +7,7 @@ import { ConnectionSuccessOutputText, InitOutputText, } from 'uiSrc/constants/cliOutput' +import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers' import reducer, { initialState, toggleCli, @@ -32,11 +33,8 @@ import reducer, { resetCliHelperSettings, goBackFromCommand, } from '../../cli/cli-settings' -jest.mock('uiSrc/constants/cliOutput', () => ({ - ...jest.requireActual('uiSrc/constants/cliOutput'), - InitOutputText: jest.fn().mockReturnValue([]), -})) - +let mathRandom: jest.SpyInstance +const random = 0.91911 let store: typeof mockedStore beforeEach(() => { cleanup() @@ -53,6 +51,14 @@ jest.mock('uiSrc/services', () => ({ })) describe('cliSettings slice', () => { + beforeAll(() => { + mathRandom = jest.spyOn(Math, 'random').mockImplementation(() => random) + }) + + afterAll(() => { + mathRandom.mockRestore() + }) + describe('toggleCliHelper', () => { it('default state.isShowHelper should be falsy', () => { // Arrange @@ -479,17 +485,17 @@ describe('cliSettings slice', () => { apiService.post = jest.fn().mockResolvedValue(responsePayload) // Act - await store.dispatch(createCliClientAction()) + await store.dispatch(createCliClientAction(INSTANCE_ID_MOCK, () => {})) // Assert const expectedActions = [ processCliClient(), - concatToOutput(InitOutputText()), + concatToOutput(InitOutputText('', 0, 0, true, () => {})), processCliClientSuccess(responsePayload.data?.uuid), concatToOutput(ConnectionSuccessOutputText), setCliDbIndex(0) ] - expect(store.getActions()).toEqual(expectedActions) + expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions)) }) it('call both createCliClientAction and processCliClientFailure when fetch is fail', async () => { @@ -505,12 +511,12 @@ describe('cliSettings slice', () => { apiService.post = jest.fn().mockRejectedValueOnce(responsePayload) // Act - await store.dispatch(createCliClientAction()) + await store.dispatch(createCliClientAction(INSTANCE_ID_MOCK, () => {})) // Assert const expectedActions = [ processCliClient(), - concatToOutput(InitOutputText()), + concatToOutput(InitOutputText('', 0, 0, true, () => {})), processCliClientFailure(responsePayload.response.data.message), concatToOutput(cliTexts.CLI_ERROR_MESSAGE(errorMessage)) ] diff --git a/redisinsight/ui/src/slices/tests/instances/caCerts.spec.ts b/redisinsight/ui/src/slices/tests/instances/caCerts.spec.ts index b6210295f8..7df36d60a4 100644 --- a/redisinsight/ui/src/slices/tests/instances/caCerts.spec.ts +++ b/redisinsight/ui/src/slices/tests/instances/caCerts.spec.ts @@ -16,7 +16,9 @@ import reducer, { } from '../../instances/caCerts' import { addErrorNotification } from '../../app/notifications' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore beforeEach(() => { diff --git a/redisinsight/ui/src/slices/tests/instances/clientCerts.spec.ts b/redisinsight/ui/src/slices/tests/instances/clientCerts.spec.ts index 2508a5de7a..6700477037 100644 --- a/redisinsight/ui/src/slices/tests/instances/clientCerts.spec.ts +++ b/redisinsight/ui/src/slices/tests/instances/clientCerts.spec.ts @@ -10,7 +10,9 @@ import reducer, { fetchClientCerts, } from '../../instances/clientCerts' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) describe('clientCerts slice', () => { describe('reducer, actions and selectors', () => { diff --git a/redisinsight/ui/src/slices/tests/instances/cloud.spec.ts b/redisinsight/ui/src/slices/tests/instances/cloud.spec.ts index a114fb9966..fdbddca167 100644 --- a/redisinsight/ui/src/slices/tests/instances/cloud.spec.ts +++ b/redisinsight/ui/src/slices/tests/instances/cloud.spec.ts @@ -33,7 +33,9 @@ import reducer, { import { LoadedCloud } from '../../interfaces' import { addErrorNotification } from '../../app/notifications' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore let account: GetCloudAccountShortInfoResponse diff --git a/redisinsight/ui/src/slices/tests/instances/cluster.spec.ts b/redisinsight/ui/src/slices/tests/instances/cluster.spec.ts index 279984129b..5f3652306c 100644 --- a/redisinsight/ui/src/slices/tests/instances/cluster.spec.ts +++ b/redisinsight/ui/src/slices/tests/instances/cluster.spec.ts @@ -26,7 +26,9 @@ import reducer, { import { addErrorNotification } from '../../app/notifications' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore let defaultCredentials: ClusterConnectionDetailsDto diff --git a/redisinsight/ui/src/slices/tests/instances/instances.spec.ts b/redisinsight/ui/src/slices/tests/instances/instances.spec.ts index 9c5a0c8abe..6ed614404a 100644 --- a/redisinsight/ui/src/slices/tests/instances/instances.spec.ts +++ b/redisinsight/ui/src/slices/tests/instances/instances.spec.ts @@ -42,7 +42,9 @@ import { addErrorNotification, addMessageNotification, IAddInstanceErrorPayload import { ConnectionType, InitialStateInstances, Instance } from '../../interfaces' import { loadMastersSentinel } from '../../instances/sentinel' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) jest.mock('uiSrc/constants') let store: typeof mockedStore diff --git a/redisinsight/ui/src/slices/tests/instances/sentinel.spec.ts b/redisinsight/ui/src/slices/tests/instances/sentinel.spec.ts index e526b23d0a..72a2aabfe2 100644 --- a/redisinsight/ui/src/slices/tests/instances/sentinel.spec.ts +++ b/redisinsight/ui/src/slices/tests/instances/sentinel.spec.ts @@ -36,7 +36,9 @@ import reducer, { import { addErrorNotification, addMessageNotification } from '../../app/notifications' import { LoadedSentinel, ModifiedSentinelMaster } from '../../interfaces' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore let masters: SentinelMaster[] diff --git a/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts b/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts index 2c541a0004..8da0c28be6 100644 --- a/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts +++ b/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts @@ -11,6 +11,7 @@ import { apiService } from 'uiSrc/services' import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { ClusterNodeRole, CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli' import { SendClusterCommandDto } from 'apiSrc/modules/cli/dto/cli.dto' +import { EMPTY_COMMAND } from 'uiSrc/constants' import reducer, { initialState, sendWBCommand, @@ -32,7 +33,9 @@ import reducer, { fetchWBHistoryAction, } from '../../workbench/wb-results' -jest.mock('uiSrc/services') +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) let store: typeof mockedStore beforeEach(() => { @@ -51,7 +54,7 @@ const initialStateWithItems = { describe('workbench results slice', () => { describe('sendWBCommand', () => { - it('should properly set loading = true, isOpen = true, error = ""', () => { + it('should properly set state', () => { // Arrange const mockPayload = { commands: ['command', 'command2'], @@ -60,6 +63,7 @@ describe('workbench results slice', () => { const state = { ...initialState, loading: true, + processing: true, items: mockPayload.commands.map((command, i) => ({ command, id: mockPayload.commandId + i, @@ -202,22 +206,77 @@ describe('workbench results slice', () => { }) }) + describe('loadWBHistorySuccess', () => { + it('should properly set history items', () => { + // Arrange + const mockCommandExecution = [{ mode: null, id: 'e3553f5a-0fdf-4282-8406-8b377c2060d2', databaseId: '3f795233-e26a-463b-a116-58cf620b18f2', command: 'get test', role: null, nodeOptions: null, createdAt: '2022-06-10T15:47:13.000Z', emptyCommand: false }] + const state = { + ...initialStateWithItems, + items: mockCommandExecution + } + + // Act + const nextState = reducer(initialStateWithItems, loadWBHistorySuccess(mockCommandExecution)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + workbench: { + results: nextState, + }, + }) + expect(workbenchResultsSelector(rootState)).toEqual(state) + }) + + it(`if command=null should properly set history items with command=${EMPTY_COMMAND}`, () => { + // Arrange + const mockCommandExecution = [{ mode: null, id: 'e3553f5a-0fdf-4282-8406-8b377c2060d2', databaseId: '3f795233-e26a-463b-a116-58cf620b18f2', command: null, role: null, nodeOptions: null, createdAt: '2022-06-10T15:47:13.000Z' }] + + const state = { + ...initialStateWithItems, + items: [{ mode: null, id: 'e3553f5a-0fdf-4282-8406-8b377c2060d2', databaseId: '3f795233-e26a-463b-a116-58cf620b18f2', command: EMPTY_COMMAND, role: null, nodeOptions: null, createdAt: '2022-06-10T15:47:13.000Z', emptyCommand: true }] + } + + // Act + const nextState = reducer(initialStateWithItems, loadWBHistorySuccess(mockCommandExecution)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + workbench: { + results: nextState, + }, + }) + expect(workbenchResultsSelector(rootState)).toEqual(state) + }) + }) + describe('thunks', () => { - describe('Standalone Cli command', () => { + describe('Standalone Cli commands', () => { it('call both sendWBCommandAction and sendWBCommandSuccess when response status is successed', async () => { // Arrange - const commands = ['keys *'] + const commands = ['keys *', 'set 1 1'] const commandId = `${Date.now()}` - const data = [{ - command: 'command', - databaseId: '123', - id: commandId + (commands.length - 1), - createdAt: new Date(), - result: [{ - response: 'test', - status: CommandExecutionStatus.Success - }] - }] + const data = [ + { + command: 'keys *', + databaseId: '123', + id: commandId + (commands.length - 1), + createdAt: new Date(), + result: [{ + response: 'test', + status: CommandExecutionStatus.Success + }] + }, + { + command: 'set 1 1', + databaseId: '123', + id: commandId + (commands.length - 1), + createdAt: new Date(), + result: [{ + response: 'test', + status: CommandExecutionStatus.Success + }] + } + ] const responsePayload = { data, status: 200 } apiService.post = jest.fn().mockResolvedValue(responsePayload) @@ -235,9 +294,9 @@ describe('workbench results slice', () => { it('call both sendWBCommandAction and sendWBCommandSuccess when response status is fail', async () => { // Arrange - const command = 'keys *' + const commands = ['keys *'] const commandId = `${Date.now()}` - const data = { + const data = [{ command: 'command', databaseId: '123', id: commandId, @@ -246,17 +305,17 @@ describe('workbench results slice', () => { response: 'test', status: CommandExecutionStatus.Fail }] - } + }] const responsePayload = { data, status: 200 } apiService.post = jest.fn().mockResolvedValue(responsePayload) // Act - await store.dispatch(sendWBCommandAction({ command, commandId })) + await store.dispatch(sendWBCommandAction({ commands, commandId })) // Assert const expectedActions = [ - sendWBCommand({ command, commandId }), + sendWBCommand({ commands, commandId }), sendWBCommandSuccess({ data, commandId }) ] @@ -265,7 +324,7 @@ describe('workbench results slice', () => { it('call both sendWBCommandAction and processWBCommandFailure when fetch is fail', async () => { // Arrange - const command = 'keys *' + const commands = ['keys *'] const commandId = `${Date.now()}` const errorMessage = 'Could not connect to aoeu:123, please check the connection details.' const responsePayload = { @@ -278,13 +337,13 @@ describe('workbench results slice', () => { apiService.post = jest.fn().mockRejectedValueOnce(responsePayload) // Act - await store.dispatch(sendWBCommandAction({ command, commandId })) + await store.dispatch(sendWBCommandAction({ commands, commandId })) // Assert const expectedActions = [ - sendWBCommand({ command, commandId }), + sendWBCommand({ commands, commandId }), addErrorNotification(responsePayload as AxiosError), - processWBCommandFailure({ command, error: responsePayload.response.data.message }), + processWBCommandFailure({ id: commandId, error: responsePayload.response.data.message }), ] expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions)) }) @@ -332,8 +391,8 @@ describe('workbench results slice', () => { it('call both sendWBCommandClusterAction and sendWBCommandSuccess when response status is fail', async () => { // Arrange - const command = 'keys *' - const data = { + const commands = ['keys *'] + const data = [{ command: 'command', databaseId: '123', id: commandId, @@ -342,17 +401,17 @@ describe('workbench results slice', () => { response: 'test', status: CommandExecutionStatus.Fail }] - } + }] const responsePayload = { data, status: 200 } apiService.post = jest.fn().mockResolvedValue(responsePayload) // Act - await store.dispatch(sendWBCommandClusterAction({ command, options, commandId })) + await store.dispatch(sendWBCommandClusterAction({ commands, options, commandId })) // Assert const expectedActions = [ - sendWBCommand({ command, commandId }), + sendWBCommand({ commands, commandId }), sendWBCommandSuccess({ data, commandId }) ] expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions)) @@ -360,7 +419,7 @@ describe('workbench results slice', () => { it('call both sendWBCommandClusterAction and processWBCommandFailure when fetch is fail', async () => { // Arrange - const command = 'keys *' + const commands = ['keys *'] const errorMessage = 'Could not connect to aoeu:123, please check the connection details.' const responsePayload = { response: { @@ -372,13 +431,13 @@ describe('workbench results slice', () => { apiService.post = jest.fn().mockRejectedValueOnce(responsePayload) // Act - await store.dispatch(sendWBCommandAction({ command, options, commandId })) + await store.dispatch(sendWBCommandAction({ commands, options, commandId })) // Assert const expectedActions = [ - sendWBCommand({ command, commandId }), + sendWBCommand({ commands, commandId }), addErrorNotification(responsePayload as AxiosError), - processWBCommandFailure({ command, error: responsePayload.response.data.message }), + processWBCommandFailure({ id: commandId, error: responsePayload.response.data.message }), ] expect(clearStoreActions(store.getActions())).toEqual(clearStoreActions(expectedActions)) }) diff --git a/redisinsight/ui/src/slices/user/user-settings.ts b/redisinsight/ui/src/slices/user/user-settings.ts index e8a48f4896..606912e961 100644 --- a/redisinsight/ui/src/slices/user/user-settings.ts +++ b/redisinsight/ui/src/slices/user/user-settings.ts @@ -1,6 +1,6 @@ import { createSlice } from '@reduxjs/toolkit' -import { apiService } from 'uiSrc/services' -import { ApiEndpoints } from 'uiSrc/constants' +import { apiService, localStorageService } from 'uiSrc/services' +import { ApiEndpoints, BrowserStorageItem } from 'uiSrc/constants' import { getApiErrorMessage, isStatusSuccessful } from 'uiSrc/utils' import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { GetAgreementsSpecResponse, GetAppSettingsResponse, UpdateSettingsDto } from 'apiSrc/dto/settings.dto' @@ -14,6 +14,9 @@ export const initialState: StateUserSettings = { isShowConceptsPopup: false, config: null, spec: null, + workbench: { + cleanup: localStorageService?.get(BrowserStorageItem.wbCleanUp) ?? true + } } // A slice for recipes @@ -59,6 +62,10 @@ const userSettingsSlice = createSlice({ state.loading = false state.error = payload }, + setWorkbenchCleanUp: (state, { payload }) => { + localStorageService.set(BrowserStorageItem.wbCleanUp, payload) + state.workbench.cleanup = payload + } }, }) @@ -75,11 +82,13 @@ export const { getUserSettingsSpec, getUserSettingsSpecSuccess, getUserSettingsSpecFailure, + setWorkbenchCleanUp } = userSettingsSlice.actions // A selector export const userSettingsSelector = (state: RootState) => state.user.settings export const userSettingsConfigSelector = (state: RootState) => state.user.settings.config +export const userSettingsWBSelector = (state: RootState) => state.user.settings.workbench // The reducer export default userSettingsSlice.reducer diff --git a/redisinsight/ui/src/slices/workbench/wb-results.ts b/redisinsight/ui/src/slices/workbench/wb-results.ts index d2484484e2..21a9cb68ef 100644 --- a/redisinsight/ui/src/slices/workbench/wb-results.ts +++ b/redisinsight/ui/src/slices/workbench/wb-results.ts @@ -1,10 +1,11 @@ import { createSlice } from '@reduxjs/toolkit' import { AxiosError } from 'axios' +import { reverse } from 'lodash' import { apiService } from 'uiSrc/services' -import { ApiEndpoints } from 'uiSrc/constants' +import { ApiEndpoints, EMPTY_COMMAND } from 'uiSrc/constants' import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { CliOutputFormatterType } from 'uiSrc/constants/cliOutput' -import { RunQueryMode } from 'uiSrc/slices/interfaces/workbench' +import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' import { getApiErrorMessage, getUrl, @@ -22,6 +23,7 @@ import { export const initialState: StateWorkbenchResults = { loading: false, + processing: false, error: '', items: [], } @@ -39,7 +41,8 @@ const workbenchResultsSlice = createSlice({ }, loadWBHistorySuccess: (state, { payload }:{ payload: CommandExecution[] }) => { - state.items = payload + state.items = payload.map((item) => + ({ ...item, command: item.command || EMPTY_COMMAND, emptyCommand: !item.command })) state.loading = false }, @@ -68,6 +71,7 @@ const workbenchResultsSlice = createSlice({ return item }) state.loading = false + state.processing = false }, sendWBCommand: (state, { payload: { commands, commandId } }: @@ -89,10 +93,12 @@ const workbenchResultsSlice = createSlice({ state.items = newItems state.loading = true + state.processing = true }, sendWBCommandSuccess: (state, - { payload: { data, commandId } }: { payload: { data: CommandExecution[], commandId: string } }) => { + { payload: { data, commandId, processing } }: + { payload: { data: CommandExecution[], commandId: string, processing?: boolean } }) => { state.items = [...state.items].map((item) => { let newItem = item data.forEach((command, i) => { @@ -104,6 +110,7 @@ const workbenchResultsSlice = createSlice({ }) state.loading = false + state.processing = (state.processing && processing) || false }, fetchWBCommandSuccess: (state, { payload }: { payload: CommandExecution }) => { @@ -131,6 +138,10 @@ const workbenchResultsSlice = createSlice({ resetWBHistoryItems: (state) => { state.items = [] + }, + + stopProcessing: (state) => { + state.processing = false } }, }) @@ -149,6 +160,7 @@ export const { toggleOpenWBResult, deleteWBCommandSuccess, resetWBHistoryItems, + stopProcessing } = workbenchResultsSlice.actions // A selector @@ -187,6 +199,7 @@ export function sendWBCommandAction({ commands = [], multiCommands = [], mode = RunQueryMode.ASCII, + resultsMode = ResultsMode.Default, commandId = `${Date.now()}`, onSuccessAction, onFailAction, @@ -195,6 +208,7 @@ export function sendWBCommandAction({ multiCommands?: string[] commandId?: string mode: RunQueryMode + resultsMode?: ResultsMode onSuccessAction?: (multiCommands: string[]) => void onFailAction?: () => void }) { @@ -203,21 +217,25 @@ export function sendWBCommandAction({ const state = stateInit() const { id = '' } = state.connections.instances.connectedInstance - dispatch(sendWBCommand({ commands, commandId })) + dispatch(sendWBCommand({ + commands: resultsMode === ResultsMode.GroupMode ? [`${commands.length} - Command(s)`] : commands, + commandId + })) const { data, status } = await apiService.post( getUrl( id, - ApiEndpoints.WORKBENCH_COMMANDS_EXECUTION, + ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS, ), { commands, mode, + resultsMode } ) if (isStatusSuccessful(status)) { - dispatch(sendWBCommandSuccess({ commandId, data })) + dispatch(sendWBCommandSuccess({ commandId, data: reverse(data), processing: !!multiCommands?.length })) onSuccessAction?.(multiCommands) } @@ -237,6 +255,7 @@ export function sendWBCommandClusterAction({ multiCommands = [], options, mode = RunQueryMode.ASCII, + resultsMode = ResultsMode.Default, commandId = `${Date.now()}`, onSuccessAction, onFailAction, @@ -245,7 +264,8 @@ export function sendWBCommandClusterAction({ options: CreateCommandExecutionDto commandId?: string multiCommands?: string[] - mode: RunQueryMode, + mode?: RunQueryMode, + resultsMode?: ResultsMode onSuccessAction?: (multiCommands: string[]) => void onFailAction?: () => void }) { @@ -254,23 +274,27 @@ export function sendWBCommandClusterAction({ const state = stateInit() const { id = '' } = state.connections.instances.connectedInstance - dispatch(sendWBCommand({ commands, commandId })) + dispatch(sendWBCommand({ + commands: resultsMode === ResultsMode.GroupMode ? [`${commands.length} - Commands`] : commands, + commandId + })) const { data, status } = await apiService.post( getUrl( id, - ApiEndpoints.WORKBENCH_COMMANDS_EXECUTION, + ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS, ), { ...options, commands, mode, - outputFormat: CliOutputFormatterType.Raw, + resultsMode, + outputFormat: CliOutputFormatterType.Raw } ) if (isStatusSuccessful(status)) { - dispatch(sendWBCommandSuccess({ commandId, data })) + dispatch(sendWBCommandSuccess({ commandId, data: reverse(data) })) onSuccessAction?.(multiCommands) } diff --git a/redisinsight/ui/src/styles/components/_table.scss b/redisinsight/ui/src/styles/components/_table.scss index 6aea2c6e01..b5173ee337 100644 --- a/redisinsight/ui/src/styles/components/_table.scss +++ b/redisinsight/ui/src/styles/components/_table.scss @@ -51,15 +51,44 @@ table { } .inMemoryTableDefault { - > div:first-child { - @include euiScrollBar; - overflow-x: auto; + &:not(.stickyHeader) { + > div:first-child { + @include euiScrollBar; + overflow-x: auto; + } } &.euiBasicTable-loading table { overflow: hidden; } + &.noHeaderBorders { + .euiTableRow { + &:not(:first-child) { + .euiTableRowCell { + border-top: 0 !important; + } + } + } + .euiTableRowCell { + &:not(:last-child) { + border-right: 0 !important; + } + } + + .euiTableHeaderCell { + border: 0 !important; + } + } + + &.stickyHeader { + .euiTableHeaderCell { + position: sticky; + top: 0; + z-index: 1; + } + } + table { overflow: initial; table-layout: auto; @@ -82,7 +111,7 @@ table { } .euiTableCellContent span { - color: var(--textColorShade) !important; + color: var(--textColorShade); padding-top: 1px; max-width: 100%; overflow: hidden; @@ -91,12 +120,12 @@ table { } .euiTableHeaderCell { - letter-spacing: 0px; + letter-spacing: 0; border: 1px solid var(--tableLightestBorderColor); .euiTableCellContent__text { color: var(--htmlColor) !important; - font: normal normal 500 14px/17px Graphik, sans-serif !important; + font: normal normal 500 14px/17px Graphik, sans-serif; letter-spacing: -0.14px; } } diff --git a/redisinsight/ui/src/styles/components/_tabs.scss b/redisinsight/ui/src/styles/components/_tabs.scss index 041b5765b2..b03e7f503e 100644 --- a/redisinsight/ui/src/styles/components/_tabs.scss +++ b/redisinsight/ui/src/styles/components/_tabs.scss @@ -45,3 +45,32 @@ width: 2px; } } + +.tabs-active-borders { + .euiTab { + border-radius: 0; + padding: 8px 12px !important; + border-bottom: 1px solid var(--separatorColor); + color: var(--euiTextSubduedColor) !important; + + &.euiTab-isSelected { + color: var(--euiColorPrimary) !important; + background-color: inherit !important; + border-bottom: 2px solid var(--euiColorPrimary); + } + + .euiTab__content { + font-size: 13px !important; + line-height: 18px !important; + font-weight: 500 !important; + } + } + + .euiTab + .euiTab { + margin-left: 0 !important; + + &::after { + display: none !important; + } + } +} diff --git a/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss b/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss index ef84933a8c..c2d607ae55 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss @@ -101,6 +101,7 @@ --iconsDefaultHoverColor: #{$iconsDefaultHoverColor}; --separatorColor: #{$separatorColor}; + --separatorColorLight: #{$separatorColorLight}; --separatorNavigationColor: #{$separatorNavigationColor}; --separatorDropdownColor: #{$separatorDropdownColor}; diff --git a/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss b/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss index cdb3199254..8e193cb5c4 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss @@ -60,6 +60,7 @@ $controlsLabelColor: #b5b6c0; $iconsDefaultColor: #b5b6c0; $iconsDefaultHoverColor: #dfe5ef; $separatorColor: #3d3d3d; +$separatorColorLight: #555555; $separatorNavigationColor: #465282; $separatorDropdownColor: #8b90a3; @@ -126,7 +127,7 @@ $rsSubmitBtn: #1ae26e; // Workbench $wbRunResultsBg: #000; $wbHoverIconColor: #ffffff; -$wbActiveIconColor: #8BA2FF; +$wbActiveIconColor: #8ba2ff; // PubSub $pubSubClientsBadge: #008000; diff --git a/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss b/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss index b5079e11ef..34742493f6 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss @@ -103,6 +103,7 @@ --iconsDefaultHoverColor: #{$iconsDefaultHoverColor}; --separatorColor: #{$separatorColor}; + --separatorColorLight: #{$separatorColorLight}; --separatorNavigationColor: #{$separatorNavigationColor}; --separatorDropdownColor: #{$separatorDropdownColor}; diff --git a/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss b/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss index 5c4f469680..53996430d2 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss @@ -57,6 +57,7 @@ $controlsLabelHoverColor: #173369; $iconsDefaultColor: #728baf; $iconsDefaultHoverColor: #173369; $separatorColor: #cdd7e6; +$separatorColorLight: #B2B9D1; $separatorNavigationColor: #465282; $separatorDropdownColor: #8b90a3; diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index b7a3fac4a4..4844f49fa1 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -67,6 +67,7 @@ export enum TelemetryEvent { BROWSER_KEY_FIELD_VALUE_EXPANDED = 'BROWSER_KEY_FIELD_VALUE_EXPANDED', BROWSER_KEY_FIELD_VALUE_COLLAPSED = 'BROWSER_KEY_FIELD_VALUE_COLLAPSED', BROWSER_KEY_DETAILS_FORMATTER_CHANGED = 'BROWSER_KEY_DETAILS_FORMATTER_CHANGED', + BROWSER_WORKBENCH_LINK_CLICKED = 'BROWSER_WORKBENCH_LINK_CLICKED', CLI_OPENED = 'CLI_OPENED', CLI_CLOSED = 'CLI_CLOSED', @@ -83,6 +84,7 @@ export enum TelemetryEvent { SETTINGS_COLOR_THEME_CHANGED = 'SETTINGS_COLOR_THEME_CHANGED', SETTINGS_NOTIFICATION_MESSAGES_ENABLED = 'SETTINGS_NOTIFICATION_MESSAGES_ENABLED', SETTINGS_NOTIFICATION_MESSAGES_DISABLED = 'SETTINGS_NOTIFICATION_MESSAGES_DISABLED', + SETTINGS_WORKBENCH_EDITOR_CLEAR_CHANGED = 'SETTINGS_WORKBENCH_EDITOR_CLEAR_CHANGED', WORKBENCH_ENABLEMENT_AREA_GUIDE_OPENED = 'WORKBENCH_ENABLEMENT_AREA_GUIDE_OPENED', WORKBENCH_ENABLEMENT_AREA_COMMAND_CLICKED = 'WORKBENCH_ENABLEMENT_AREA_COMMAND_CLICKED', @@ -147,6 +149,7 @@ export enum TelemetryEvent { TREE_VIEW_KEY_FIELD_VALUE_EXPANDED = 'TREE_VIEW_KEY_FIELD_VALUE_EXPANDED', TREE_VIEW_KEY_FIELD_VALUE_COLLAPSED = 'TREE_VIEW_KEY_FIELD_VALUE_COLLAPSED', TREE_VIEW_KEY_DETAILS_FORMATTER_CHANGED = 'TREE_VIEW_KEY_DETAILS_FORMATTER_CHANGED', + TREE_VIEW_WORKBENCH_LINK_CLICKED = 'TREE_VIEW_WORKBENCH_LINK_CLICKED', SLOWLOG_LOADED = 'SLOWLOG_LOADED', SLOWLOG_CLEARED = 'SLOWLOG_CLEARED', @@ -178,4 +181,6 @@ export enum TelemetryEvent { BULK_ACTIONS_OPENED = 'BULK_ACTIONS_OPENED', BULK_ACTIONS_WARNING = 'BULK_ACTIONS_WARNING', BULK_ACTIONS_CANCELLED = 'BULK_ACTIONS_CANCELLED', + + USER_SURVEY_LINK_CLICKED = 'USER_SURVEY_LINK_CLICKED' } diff --git a/redisinsight/ui/src/telemetry/pageViews.ts b/redisinsight/ui/src/telemetry/pageViews.ts index 70e0582aa7..7af5a7744e 100644 --- a/redisinsight/ui/src/telemetry/pageViews.ts +++ b/redisinsight/ui/src/telemetry/pageViews.ts @@ -5,5 +5,6 @@ export enum TelemetryPageView { BROWSER_PAGE = 'Browser', WORKBENCH_PAGE = 'Workbench', SLOWLOG_PAGE = 'Slow Log', + CLUSTER_DETAILS_PAGE = 'Overview', PUBSUB_PAGE = 'Pub/Sub' } diff --git a/redisinsight/ui/src/types/index.d.ts b/redisinsight/ui/src/types/index.d.ts index 52959c093c..68b728d20c 100644 --- a/redisinsight/ui/src/types/index.d.ts +++ b/redisinsight/ui/src/types/index.d.ts @@ -16,5 +16,9 @@ declare global { ASCIIToBuffer: (reply: string) => RedisResponseBuffer stringToBuffer: (reply: string) => RedisResponseBuffer bufferToString: (reply: RedisString) => string + hexToBuffer: (reply: string) => RedisResponseBuffer, + bufferToHex: (reply: RedisResponseBuffer) => string, + bufferToBinary: (reply: RedisResponseBuffer) => string, + binaryToBuffer: (reply: string) => RedisResponseBuffer } } diff --git a/redisinsight/ui/src/utils/apiResponse.ts b/redisinsight/ui/src/utils/apiResponse.ts index 0ce0ac4132..f864dee951 100644 --- a/redisinsight/ui/src/utils/apiResponse.ts +++ b/redisinsight/ui/src/utils/apiResponse.ts @@ -2,10 +2,12 @@ import { AxiosError } from 'axios' import { first, isArray, get } from 'lodash' import { AddRedisDatabaseStatus, IBulkOperationResult } from 'uiSrc/slices/interfaces' +export const DEFAULT_ERROR_MESSAGE = 'Something was wrong!' + export function getApiErrorMessage(error: AxiosError): string { const errorMessage = error?.response?.data?.message if (!error || !error.response) { - return 'Something was wrong!' + return DEFAULT_ERROR_MESSAGE } if (isArray(errorMessage)) { return first(errorMessage) diff --git a/redisinsight/ui/src/utils/calculateTextareaLines.ts b/redisinsight/ui/src/utils/calculateTextareaLines.ts new file mode 100644 index 0000000000..5bb515f529 --- /dev/null +++ b/redisinsight/ui/src/utils/calculateTextareaLines.ts @@ -0,0 +1,5 @@ +const APPROXIMATE_WIDTH_OF_SIGN = 7.05 + +export const calculateTextareaLines = (text: string, width: number = 1, signWidth = APPROXIMATE_WIDTH_OF_SIGN) => + text?.split('\n') + .reduce((prev, current) => Math.ceil((current.length * signWidth) / width) + prev, 0) || 1 diff --git a/redisinsight/ui/src/utils/cliHelper.tsx b/redisinsight/ui/src/utils/cliHelper.tsx index 73cf3b9eba..550ed00c1f 100644 --- a/redisinsight/ui/src/utils/cliHelper.tsx +++ b/redisinsight/ui/src/utils/cliHelper.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { Fragment } from 'react' import { Dispatch, PayloadAction } from '@reduxjs/toolkit' import parse from 'html-react-parser' @@ -19,6 +19,12 @@ export enum CliPrefix { QueryCard = 'query-card', } +interface IGroupModeCommand { + command: string + response: string + status: CommandExecutionStatus +} + const cliParseTextResponseWithRedirect = ( text: string = '', command: string = '', @@ -73,10 +79,34 @@ const cliCommandWrapper = (command: string) => ( ) +const wbSummaryCommand = (command: string) => ( + + {`> ${command} \n`} + +) + const clearOutput = (dispatch: any) => { dispatch(resetOutput()) } +const cliParseCommandsGroupResult = ( + result: IGroupModeCommand, + index: number +) => { + const executionCommand = wbSummaryCommand(result.command) + const executionResult = cliParseTextResponse(result.response || '(nil)', result.command, result.status) + return ( + + {executionCommand} + {executionResult} + {'\n'} + + ) +} + const updateCliHistoryStorage = ( command: string = '', dispatch: Dispatch> @@ -154,6 +184,7 @@ export { cliParseTextResponse, cliParseTextResponseWithOffset, cliParseTextResponseWithRedirect, + cliParseCommandsGroupResult, cliCommandOutput, bashTextValue, cliCommandWrapper, diff --git a/redisinsight/ui/src/utils/colors.ts b/redisinsight/ui/src/utils/colors.ts new file mode 100644 index 0000000000..d7886092fd --- /dev/null +++ b/redisinsight/ui/src/utils/colors.ts @@ -0,0 +1,48 @@ +export type RGBColor = [number, number, number] + +export interface ColorScheme { + cHueStart: number + cHueRange: number + cSaturation: number + cLightness: number +} + +const HSLToRGB = (h: number, sI: number, lI: number): RGBColor => { + const s = sI / 100 + const l = lI / 100 + const k = (n: number) => (n + h / 30) % 12 + const a = s * Math.min(l, 1 - l) + const f = (n: number) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))) + + return [255 * f(0), 255 * f(8), 255 * f(4)] +} + +const PBC = (r: number, g: number, b: number): number => + Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)) / 255 + +const correctBrightness = (rgb: RGBColor, cLightness: number) => 1 / ((PBC(...rgb) * 100) / cLightness) + +const applyBrightnessToRGB = (rgb: RGBColor, cLightness: number): RGBColor => { + const [r, g, b] = rgb + return [ + Math.round(r * correctBrightness([r, g, b], cLightness)), + Math.round(g * correctBrightness([r, g, b], cLightness)), + Math.round(b * correctBrightness([r, g, b], cLightness)) + ] as RGBColor +} + +const getRGBColorByScheme = (index: number, shift: number, colorScheme: ColorScheme): RGBColor => { + const nc = index * shift + colorScheme.cHueStart + const rgb: RGBColor = HSLToRGB(nc, colorScheme.cSaturation, colorScheme.cLightness) + return applyBrightnessToRGB(rgb, colorScheme.cLightness) +} + +const rgb = (rgb: RGBColor) => `rgb(${rgb.join(', ')})` + +export { + HSLToRGB, + correctBrightness, + applyBrightnessToRGB, + getRGBColorByScheme, + rgb +} diff --git a/redisinsight/ui/src/utils/commands.ts b/redisinsight/ui/src/utils/commands.ts index f902164bce..636ed03a77 100644 --- a/redisinsight/ui/src/utils/commands.ts +++ b/redisinsight/ui/src/utils/commands.ts @@ -1,7 +1,6 @@ import { flatten, isArray, isEmpty, isNumber, reject, toNumber, isNaN, isInteger } from 'lodash' import { CommandArgsType, - CommandPrefix, ICommandArg, ICommandArgGenerated } from 'uiSrc/constants' diff --git a/redisinsight/ui/src/utils/formatters/bufferFormatters.ts b/redisinsight/ui/src/utils/formatters/bufferFormatters.ts index 567285cc38..3cd745c3ec 100644 --- a/redisinsight/ui/src/utils/formatters/bufferFormatters.ts +++ b/redisinsight/ui/src/utils/formatters/bufferFormatters.ts @@ -1,4 +1,5 @@ import { isString } from 'lodash' +import { ObjectInputStream } from 'java-object-serialization' import { KeyValueFormat } from 'uiSrc/constants' import { Buffer } from 'buffer' // eslint-disable-next-line import/order @@ -35,6 +36,14 @@ const bufferToHex = (reply: RedisResponseBuffer): string => { return result } +const bufferToBinary = (reply: RedisResponseBuffer): string => + Array.from(reply.data).reduce((str, byte) => str + byte.toString(2).padStart(8, '0'), '') + +const binaryToBuffer = (reply: string) => { + const data: number[] = reply.match(/.{1,8}/g)?.map((v) => parseInt(v, 2)) || [] + return anyToBuffer(data) +} + const bufferToASCII = (reply: RedisResponseBuffer): string => { let result = '' reply.data.forEach((byte: number) => { @@ -128,6 +137,14 @@ const hexToBuffer = (data: string): RedisResponseBuffer => { return { type: RedisResponseBufferType.Buffer, data: result } } +const bufferToJava = (reply: RedisResponseBuffer) => { + const stream = new ObjectInputStream(new Uint8Array(reply.data)) + const decoded = stream.readObject() + const { fields } = decoded + const fieldsArray = Array.from(fields, ([key, value]) => ({ [key]: value })) + return { ...decoded, fields: fieldsArray } +} + const bufferToString = (data: RedisString = '', formatResult: KeyValueFormat = KeyValueFormat.Unicode): string => { if (!isString(data) && data?.type === RedisResponseBufferType.Buffer) { switch (formatResult) { @@ -146,8 +163,6 @@ const bufferToString = (data: RedisString = '', formatResult: KeyValueFormat = K return data?.toString() } -export default bufferToString - export { bufferToUTF8, bufferToASCII, @@ -161,6 +176,9 @@ export { UintArrayToString, hexToBuffer, anyToBuffer, + bufferToBinary, + binaryToBuffer, + bufferToJava } window.ri = { @@ -171,6 +189,10 @@ window.ri = { UintArrayToString, stringToBuffer, bufferToString, + bufferToHex, + hexToBuffer, + bufferToBinary, + binaryToBuffer } // for BE libraries which work with Buffer diff --git a/redisinsight/ui/src/utils/formatters/utils.ts b/redisinsight/ui/src/utils/formatters/utils.ts index 446f59fb2f..7edcd1a833 100644 --- a/redisinsight/ui/src/utils/formatters/utils.ts +++ b/redisinsight/ui/src/utils/formatters/utils.ts @@ -11,3 +11,8 @@ export const bufferFormatRangeItems = ( return newItems } + +export const replaceBigIntWithString = (obj: Object) => JSON.parse(JSON.stringify(obj, (_, value) => ( + typeof value === 'bigint' + ? value.toString() + : value))) diff --git a/redisinsight/ui/src/utils/formatters/valueFormatters.tsx b/redisinsight/ui/src/utils/formatters/valueFormatters.tsx index 24a3cbc42e..06f7e706f2 100644 --- a/redisinsight/ui/src/utils/formatters/valueFormatters.tsx +++ b/redisinsight/ui/src/utils/formatters/valueFormatters.tsx @@ -1,26 +1,55 @@ import { decode, encode } from '@msgpack/msgpack' +// eslint-disable-next-line import/order +import { Buffer } from 'buffer' +import { isUndefined } from 'lodash' +import { serialize, unserialize } from 'php-serialize' +import { getData } from 'rawproto' +import jpickle from 'jpickle' + import JSONViewer from 'uiSrc/components/json-viewer/JSONViewer' import { KeyValueFormat } from 'uiSrc/constants' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import { anyToBuffer, bufferToASCII, bufferToUTF8, stringToBuffer, bufferToHex, hexToBuffer } from 'uiSrc/utils' +import { + anyToBuffer, + bufferToASCII, + bufferToBinary, + bufferToHex, + bufferToUTF8, + bufferToJava, + hexToBuffer, + stringToBuffer, + binaryToBuffer, + replaceBigIntWithString +} from 'uiSrc/utils' import { reSerializeJSON } from 'uiSrc/utils/formatters/json' export interface FormattingProps { - expanded: boolean + expanded?: boolean } const isTextViewFormatter = (format: KeyValueFormat) => [ KeyValueFormat.Unicode, - KeyValueFormat.ASCII, - KeyValueFormat.HEX -].includes(format) -const isJsonViewFormatter = (format: KeyValueFormat) => !isTextViewFormatter(format) - -const isNonUnicodeFormatter = (format: KeyValueFormat) => [ KeyValueFormat.ASCII, KeyValueFormat.HEX, KeyValueFormat.Binary, ].includes(format) +const isJsonViewFormatter = (format: KeyValueFormat) => !isTextViewFormatter(format) +const isFormatEditable = (format: KeyValueFormat) => ![ + KeyValueFormat.Protobuf, + KeyValueFormat.JAVA, + KeyValueFormat.Pickle, +].includes(format) + +const isNonUnicodeFormatter = (format: KeyValueFormat, isValid: boolean) => { + if (format === KeyValueFormat.Msgpack) { + return isValid + } + return [ + KeyValueFormat.ASCII, + KeyValueFormat.HEX, + KeyValueFormat.Binary, + ].includes(format) +} const bufferToUnicode = (reply: RedisResponseBuffer): string => bufferToUTF8(reply) @@ -37,38 +66,73 @@ const formattingBuffer = ( props?: FormattingProps ): { value: JSX.Element | string, isValid: boolean } => { switch (format) { - case KeyValueFormat.JSON: { - return bufferToJSON(reply, props as FormattingProps) + case KeyValueFormat.ASCII: return { value: bufferToASCII(reply), isValid: true } + case KeyValueFormat.HEX: return { value: bufferToHex(reply), isValid: true } + case KeyValueFormat.Binary: return { value: bufferToBinary(reply), isValid: true } + case KeyValueFormat.JSON: return bufferToJSON(reply, props as FormattingProps) + case KeyValueFormat.Msgpack: { + try { + const decoded = decode(reply.data) + const value = JSON.stringify(decoded) + return JSONViewer({ value, ...props }) + } catch (e) { + return { value: bufferToUTF8(reply), isValid: false } + } } - case KeyValueFormat.HEX: { - return { value: bufferToHex(reply), isValid: true } + case KeyValueFormat.PHP: { + try { + const decoded = unserialize(Buffer.from(reply.data), { encoding: 'binary' }) + const value = JSON.stringify(decoded) + return JSONViewer({ value, ...props }) + } catch (e) { + return { value: bufferToUTF8(reply), isValid: false } + } } - case KeyValueFormat.ASCII: { - return { value: bufferToASCII(reply), isValid: true } + case KeyValueFormat.JAVA: { + try { + const decoded = bufferToJava(reply) + const value = JSON.stringify(replaceBigIntWithString(decoded)) + return JSONViewer({ value, ...props }) + } catch (e) { + return { value: bufferToUTF8(reply), isValid: false } + } } - case KeyValueFormat.Msgpack: { + case KeyValueFormat.Protobuf: { try { - const decoded = decode(reply.data) + const decoded = getData(Buffer.from(reply.data)) const value = JSON.stringify(decoded) return JSONViewer({ value, ...props }) } catch (e) { return { value: bufferToUTF8(reply), isValid: false } } } - default: { - return { value: bufferToUnicode(reply), isValid: true } + case KeyValueFormat.Pickle: { + try { + const decoded = jpickle.loads(bufferToUTF8(reply)) + + if (isUndefined(decoded)) { + return { + value: bufferToUTF8(reply), + isValid: false + } + } + + const value = JSON.stringify(decoded) + return JSONViewer({ value, ...props }) + } catch (e) { + return { value: bufferToUTF8(reply), isValid: false } + } } + default: return { value: bufferToUnicode(reply), isValid: true } } } const bufferToSerializedFormat = (format: KeyValueFormat, value: RedisResponseBuffer, space?: number): string => { switch (format) { - case KeyValueFormat.JSON: { - return reSerializeJSON(bufferToUTF8(value), space) - } - case KeyValueFormat.ASCII: { - return bufferToASCII(value) - } + case KeyValueFormat.ASCII: return bufferToASCII(value) + case KeyValueFormat.HEX: return bufferToHex(value) + case KeyValueFormat.Binary: return bufferToBinary(value) + case KeyValueFormat.JSON: return reSerializeJSON(bufferToUTF8(value), space) case KeyValueFormat.Msgpack: { try { const decoded = decode(value.data) @@ -78,17 +142,34 @@ const bufferToSerializedFormat = (format: KeyValueFormat, value: RedisResponseBu return bufferToUTF8(value) } } - case KeyValueFormat.HEX: { - return bufferToHex(value) - } - default: { - return bufferToUTF8(value) + case KeyValueFormat.PHP: { + try { + const decoded = unserialize(Buffer.from(value.data), { encoding: 'binary' }) + const stringified = JSON.stringify(decoded) + return reSerializeJSON(stringified, space) + } catch (e) { + return bufferToUTF8(value) + } } + default: return bufferToUTF8(value) } } const stringToSerializedBufferFormat = (format: KeyValueFormat, value: string): RedisResponseBuffer => { switch (format) { + case KeyValueFormat.HEX: { + if ((value.match(/([0-9]|[a-f])/gim) || []).length === value.length && (value.length % 2 === 0)) { + return hexToBuffer(value) + } + return stringToBuffer(value) + } + case KeyValueFormat.Binary: { + const str = value.replace(/ /g, '') + if (str.length % 8 === 0 && /^[0-1]+$/g.test(str)) { + return binaryToBuffer(str) + } + return stringToBuffer(value) + } case KeyValueFormat.JSON: { return stringToBuffer(reSerializeJSON(value)) } @@ -101,11 +182,14 @@ const stringToSerializedBufferFormat = (format: KeyValueFormat, value: string): return stringToBuffer(value, format) } } - case KeyValueFormat.HEX: { - if ((value.match(/([0-9]|[a-f])/gim) || []).length === value.length && (value.length % 2 === 0)) { - return hexToBuffer(value) + case KeyValueFormat.PHP: { + try { + const json = JSON.parse(value) + const serialized = serialize(json) + return stringToBuffer(serialized) + } catch (e) { + return stringToBuffer(value, format) } - return stringToBuffer(value) } default: { return stringToBuffer(value, format) @@ -117,6 +201,7 @@ export { formattingBuffer, isTextViewFormatter, isJsonViewFormatter, + isFormatEditable, bufferToSerializedFormat, stringToSerializedBufferFormat, isNonUnicodeFormatter, diff --git a/redisinsight/ui/src/utils/getLetterByIndex.ts b/redisinsight/ui/src/utils/getLetterByIndex.ts new file mode 100644 index 0000000000..f2c570010a --- /dev/null +++ b/redisinsight/ui/src/utils/getLetterByIndex.ts @@ -0,0 +1,8 @@ +const getLetterByIndex = (index: number): string => { + const mod = index % 26 + const pow = index / 26 | 0 + const out = String.fromCharCode(65 + mod) + return pow ? getLetterByIndex(pow - 1) + out : out +} + +export default getLetterByIndex diff --git a/redisinsight/ui/src/utils/index.ts b/redisinsight/ui/src/utils/index.ts index 6d0dab29da..d6372ac753 100644 --- a/redisinsight/ui/src/utils/index.ts +++ b/redisinsight/ui/src/utils/index.ts @@ -7,6 +7,7 @@ import replaceSpaces from './replaceSpaces' import setFavicon from './setFavicon' import setTitle from './setPageTitle' import formatToText from './cliTextFormatter' +import getLetterByIndex from './getLetterByIndex' export * from './common' export * from './validations' @@ -53,4 +54,5 @@ export { setFavicon, setTitle, formatToText, + getLetterByIndex } diff --git a/redisinsight/ui/src/utils/modules.ts b/redisinsight/ui/src/utils/modules.ts index bff03f93d8..2156b7a6dc 100644 --- a/redisinsight/ui/src/utils/modules.ts +++ b/redisinsight/ui/src/utils/modules.ts @@ -21,22 +21,18 @@ const PREDEFINED_MODULE_NAMES_ORDER: string[] = [ ] // @ts-ignore -const PREDEFINED_MODULES_ORDER = PREDEFINED_MODULE_NAMES_ORDER.map(module => DATABASE_LIST_MODULES_TEXT[module]) +const PREDEFINED_MODULES_ORDER = PREDEFINED_MODULE_NAMES_ORDER.map((module) => DATABASE_LIST_MODULES_TEXT[module]) -export const sortModules = (modules: IDatabaseModule[]) => { - return modules.sort((a, b) => { - if (!a.moduleName && !a.abbreviation) return 1 - if (!b.moduleName && !b.abbreviation) return -1 - if (PREDEFINED_MODULES_ORDER.indexOf(a.moduleName) === -1) return 1 - if (PREDEFINED_MODULES_ORDER.indexOf(b.moduleName) === -1) return -1 - return PREDEFINED_MODULES_ORDER.indexOf(a.moduleName) - PREDEFINED_MODULES_ORDER.indexOf(b.moduleName) - }) -} +export const sortModules = (modules: IDatabaseModule[]) => modules.sort((a, b) => { + if (!a.moduleName && !a.abbreviation) return 1 + if (!b.moduleName && !b.abbreviation) return -1 + if (PREDEFINED_MODULES_ORDER.indexOf(a.moduleName) === -1) return 1 + if (PREDEFINED_MODULES_ORDER.indexOf(b.moduleName) === -1) return -1 + return PREDEFINED_MODULES_ORDER.indexOf(a.moduleName) - PREDEFINED_MODULES_ORDER.indexOf(b.moduleName) +}) -export const sortModulesByName = (modules: RedisModuleDto[]) => { - return [...modules].sort((a, b) => { - if (PREDEFINED_MODULE_NAMES_ORDER.indexOf(a.name) === -1) return 1 - if (PREDEFINED_MODULE_NAMES_ORDER.indexOf(b.name) === -1) return -1 - return PREDEFINED_MODULE_NAMES_ORDER.indexOf(a.name) - PREDEFINED_MODULE_NAMES_ORDER.indexOf(b.name) - }) -} +export const sortModulesByName = (modules: RedisModuleDto[]) => [...modules].sort((a, b) => { + if (PREDEFINED_MODULE_NAMES_ORDER.indexOf(a.name) === -1) return 1 + if (PREDEFINED_MODULE_NAMES_ORDER.indexOf(b.name) === -1) return -1 + return PREDEFINED_MODULE_NAMES_ORDER.indexOf(a.name) - PREDEFINED_MODULE_NAMES_ORDER.indexOf(b.name) +}) diff --git a/redisinsight/ui/src/utils/monacoActions.ts b/redisinsight/ui/src/utils/monacoActions.ts index 55a71882ba..c76ace9720 100644 --- a/redisinsight/ui/src/utils/monacoActions.ts +++ b/redisinsight/ui/src/utils/monacoActions.ts @@ -1,7 +1,8 @@ import * as monacoEditor from 'monaco-editor' export enum MonacoAction { - Submit = 'submit' + Submit = 'submit', + ChangeGroupMode = 'change-group-mode' } export const getMonacoAction = ( diff --git a/redisinsight/ui/src/utils/numbers.ts b/redisinsight/ui/src/utils/numbers.ts index eb8189d872..cc4b67112f 100644 --- a/redisinsight/ui/src/utils/numbers.ts +++ b/redisinsight/ui/src/utils/numbers.ts @@ -11,3 +11,8 @@ export const nullableNumberWithSpaces = (number: Nullable = 0) => { } return numberWithSpaces(number) } + +export const getPercentage = (value = 0, sum = 1, round = false, decimals = 2) => { + const percent = parseFloat(((value / sum) * 100).toFixed(decimals)) + return round ? Math.round(percent) : percent +} diff --git a/redisinsight/ui/src/utils/plugins.ts b/redisinsight/ui/src/utils/plugins.ts index bc6d7f651e..ba85b043a9 100644 --- a/redisinsight/ui/src/utils/plugins.ts +++ b/redisinsight/ui/src/utils/plugins.ts @@ -1,10 +1,10 @@ import { IPluginVisualization } from 'uiSrc/slices/interfaces' import { getBaseApiUrl } from 'uiSrc/utils/common' -export const getVisualizationsByCommand = (query: string, visualizations: IPluginVisualization[]) => +export const getVisualizationsByCommand = (query: string = '', visualizations: IPluginVisualization[]) => visualizations.filter((visualization: IPluginVisualization) => visualization.matchCommands.some((matchCommand) => - query.startsWith(matchCommand) || (new RegExp(`^${matchCommand}`, 'i')).test(query))) + query?.startsWith(matchCommand) || (new RegExp(`^${matchCommand}`, 'i')).test(query))) export const urlForAsset = (basePluginUrl: string, path: string) => { const baseApiUrl = getBaseApiUrl() diff --git a/redisinsight/ui/src/utils/streamUtils.ts b/redisinsight/ui/src/utils/streamUtils.ts index 850de2aa15..1f02343d22 100644 --- a/redisinsight/ui/src/utils/streamUtils.ts +++ b/redisinsight/ui/src/utils/streamUtils.ts @@ -3,7 +3,7 @@ import { orderBy } from 'lodash' import { SortOrder } from 'uiSrc/constants' import { SCAN_STREAM_START_DEFAULT, SCAN_STREAM_END_DEFAULT } from 'uiSrc/constants/api' import { ClaimPendingEntryDto, ConsumerDto, ConsumerGroupDto, PendingEntryDto } from 'apiSrc/modules/browser/dto/stream.dto' -import { RedisString } from 'uiSrc/slices/interfaces' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { isEqualBuffers } from './formatters' export enum ClaimTimeOptions { @@ -80,7 +80,7 @@ export const prepareDataForClaimRequest = ( }) } -export const updateConsumerGroups = (groups: ConsumerGroupDto[], groupName: RedisString, consumers: ConsumerDto[]) => +export const updateConsumerGroups = (groups: ConsumerGroupDto[], groupName: RedisResponseBuffer, consumers: ConsumerDto[]) => groups?.map((group: ConsumerGroupDto) => { if (isEqualBuffers(group.name, groupName)) { group.consumers = consumers?.length @@ -89,7 +89,7 @@ export const updateConsumerGroups = (groups: ConsumerGroupDto[], groupName: Redi return group }) -export const updateConsumers = (consumers: ConsumerDto[], consumerName: RedisString, messages: PendingEntryDto[]) => +export const updateConsumers = (consumers: ConsumerDto[], consumerName: RedisResponseBuffer, messages: PendingEntryDto[]) => consumers?.map((consumer: ConsumerDto) => { if (isEqualBuffers(consumer.name, consumerName)) { consumer.pending = messages?.length diff --git a/redisinsight/ui/src/utils/test-utils.tsx b/redisinsight/ui/src/utils/test-utils.tsx index 3131b12177..3ede2a6426 100644 --- a/redisinsight/ui/src/utils/test-utils.tsx +++ b/redisinsight/ui/src/utils/test-utils.tsx @@ -37,8 +37,12 @@ import { initialState as initialStateWBResults } from 'uiSrc/slices/workbench/wb import { initialState as initialStateWBEGuides } from 'uiSrc/slices/workbench/wb-guides' import { initialState as initialStateWBETutorials } from 'uiSrc/slices/workbench/wb-tutorials' import { initialState as initialStateCreateRedisButtons } from 'uiSrc/slices/content/create-redis-buttons' -import { initialState as initialStateSlowLog } from 'uiSrc/slices/slowlog/slowlog' +import { initialState as initialStateSlowLog } from 'uiSrc/slices/analytics/slowlog' +import { initialState as initialClusterDetails } from 'uiSrc/slices/analytics/clusterDetails' +import { initialState as initialStateAnalyticsSettings } from 'uiSrc/slices/analytics/settings' import { initialState as initialStatePubSub } from 'uiSrc/slices/pubsub/pubsub' +import { RESOURCES_BASE_URL } from 'uiSrc/services/resourcesService' +import { apiService } from 'uiSrc/services' interface Options { initialState?: RootState; @@ -92,7 +96,11 @@ const initialStateDefault: RootState = { content: { createRedisButtons: cloneDeep(initialStateCreateRedisButtons) }, - slowlog: cloneDeep(initialStateSlowLog), + analytics: { + settings: cloneDeep(initialStateAnalyticsSettings), + slowlog: cloneDeep(initialStateSlowLog), + clusterDetails: cloneDeep(initialClusterDetails), + }, pubsub: cloneDeep(initialStatePubSub), } @@ -150,13 +158,13 @@ jest.mock('react-router-dom', () => ({ }), })) -// mock useDispatch -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - usDispatch: () => ({ - dispatch: jest.fn, - }), -})) +// // mock useDispatch +// jest.mock('react-redux', () => ({ +// ...jest.requireActual('react-redux'), +// usDispatch: () => ({ +// dispatch: jest.fn, +// }), +// })) // mock jest.mock( @@ -189,6 +197,10 @@ Object.defineProperty(window, 'sessionStorage', { value: sessionStorageMock }) const scrollIntoViewMock = jest.fn() window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock +export const getMswResourceURL = (path: string = '') => RESOURCES_BASE_URL.concat(path) +export const getMswURL = (path: string = '') => + apiService.defaults.baseURL?.concat(path.startsWith('/') ? path.slice(1) : path) ?? '' + // re-export everything export * from '@testing-library/react' // override render method diff --git a/redisinsight/ui/src/utils/tests/apiResponse.spec.ts b/redisinsight/ui/src/utils/tests/apiResponse.spec.ts index c6124e888f..cad392e017 100644 --- a/redisinsight/ui/src/utils/tests/apiResponse.spec.ts +++ b/redisinsight/ui/src/utils/tests/apiResponse.spec.ts @@ -1,4 +1,4 @@ -import { getApiErrorMessage } from 'uiSrc/utils' +import { DEFAULT_ERROR_MESSAGE, getApiErrorMessage } from 'uiSrc/utils' import { AxiosError } from 'axios' const error = { response: { data: { message: 'error' } } } as AxiosError @@ -7,7 +7,7 @@ const errors = { response: { data: { message: ['error1', 'error2'] } } } as Axio describe('getApiErrorMessage', () => { it('should return proper message', () => { expect(getApiErrorMessage(error)).toEqual('error') - expect(getApiErrorMessage(null)).toEqual('Something was wrong!') + expect(getApiErrorMessage(null)).toEqual(DEFAULT_ERROR_MESSAGE) expect(getApiErrorMessage(errors)).toEqual('error1') }) }) diff --git a/redisinsight/ui/src/utils/tests/cliHelper.spec.ts b/redisinsight/ui/src/utils/tests/cliHelper.spec.ts index c40f478fc6..96aae59744 100644 --- a/redisinsight/ui/src/utils/tests/cliHelper.spec.ts +++ b/redisinsight/ui/src/utils/tests/cliHelper.spec.ts @@ -1,5 +1,11 @@ -import { getDbIndexFromSelectQuery, getCommandNameFromQuery } from 'uiSrc/utils' +import { + getDbIndexFromSelectQuery, + getCommandNameFromQuery, + cliParseCommandsGroupResult, + CliPrefix +} from 'uiSrc/utils' import { MOCK_COMMANDS_SPEC } from 'uiSrc/constants' +import { render, screen } from 'uiSrc/utils/test-utils' const getDbIndexFromSelectQueryTests = [ { input: 'select 0', expected: 0 }, @@ -49,3 +55,29 @@ describe('getCommandNameFromQuery', () => { expect(getCommandNameFromQuery(...input)).toEqual(expected) }) }) + +describe('cliParseCommandsGroupResult', () => { + const mockResult = { + command: 'command', + response: 'response', + status: 'success' + } + const mockIndex = 0 + render(cliParseCommandsGroupResult(mockResult, mockIndex)) + + expect(screen.queryByTestId('wb-command')).toBeInTheDocument() + expect(screen.getByText('> command')).toBeInTheDocument() + expect(screen.queryByTestId(`${CliPrefix.Cli}-output-response-success`)).toBeInTheDocument() +}) + +describe('cliParseCommandsGroupResult error status', () => { + const mockResult = { + command: 'command', + response: 'response', + status: 'fail' + } + const mockIndex = 1 + render(cliParseCommandsGroupResult(mockResult, mockIndex)) + + expect(screen.queryByTestId(`${CliPrefix.Cli}-output-response-fail`)).toBeInTheDocument() +}) diff --git a/redisinsight/ui/src/utils/tests/colors.spec.ts b/redisinsight/ui/src/utils/tests/colors.spec.ts new file mode 100644 index 0000000000..42618f637a --- /dev/null +++ b/redisinsight/ui/src/utils/tests/colors.spec.ts @@ -0,0 +1,37 @@ +import { ColorScheme, getRGBColorByScheme, rgb } from 'uiSrc/utils/colors' + +const colorScheme: ColorScheme = { + cHueStart: 180, + cHueRange: 140, + cSaturation: 55, + cLightness: 45 +} + +const RGBColorsTests: any[] = [ + // colors for length 3 + [0, 0, [39, 135, 135]], + [1, 140 / 3, [66, 101, 226]], + [2, 140 / 3, [143, 60, 208]], + + // other colors + [1, 140 / 3, [66, 101, 226]], + [2, 140 / 4, [101, 72, 248]], + [3, 140 / 5, [129, 65, 224]], + [4, 140 / 6, [143, 60, 208]], + [5, 140 / 7, [151, 57, 197]], +] + +describe('getRGBColorByScheme', () => { + it.each(RGBColorsTests)('for input: %s (index), %s (shift), should be output: %s', + (index, shift, expected) => { + const result = getRGBColorByScheme(index, shift, colorScheme) + expect(result).toEqual(expected) + }) +}) + +describe('rgb', () => { + it('should return proper rgb string color', () => { + expect(rgb([0, 0, 0])).toEqual('rgb(0, 0, 0)') + expect(rgb([100, 30, 10])).toEqual('rgb(100, 30, 10)') + }) +}) diff --git a/redisinsight/ui/src/utils/tests/formatters/bufferFormatters.spec.ts b/redisinsight/ui/src/utils/tests/formatters/bufferFormatters.spec.ts index d5fe5f9b9c..f4e2ff114d 100644 --- a/redisinsight/ui/src/utils/tests/formatters/bufferFormatters.spec.ts +++ b/redisinsight/ui/src/utils/tests/formatters/bufferFormatters.spec.ts @@ -9,16 +9,19 @@ import { isEqualBuffers, hexToBuffer, bufferToHex, + bufferToBinary, + binaryToBuffer, + bufferToJava } from 'uiSrc/utils' const defaultValues = [ - { unicode: 'test', ascii: 'test', hex: '74657374', uint8Array: [116, 101, 115, 116] }, - { unicode: 'test test', ascii: 'test test', hex: '746573742074657374', uint8Array: [116, 101, 115, 116, 32, 116, 101, 115, 116] }, - { unicode: '嘿', ascii: '\\xe5\\x98\\xbf', hex: 'e598bf', uint8Array: [229, 152, 191] }, - { unicode: '\xea12 \x12 p5', ascii: '\\xc3\\xaa12 \\x12 p5', hex: 'c3aa31322012207035', uint8Array: [195, 170, 49, 50, 32, 18, 32, 112, 53] }, - { unicode: 'hi \n hi \t', ascii: 'hi \\n hi \\t', hex: '6869200a2068692009', uint8Array: [104, 105, 32, 10, 32, 104, 105, 32, 9] }, - { unicode: '!@#54ueo\'6&*(){', ascii: '!@#54ueo\'6&*(){', hex: '214023353475656f2736262a28297b', uint8Array: [33, 64, 35, 53, 52, 117, 101, 111, 39, 54, 38, 42, 40, 41, 123] }, - { unicode: 'привет', ascii: '\\xd0\\xbf\\xd1\\x80\\xd0\\xb8\\xd0\\xb2\\xd0\\xb5\\xd1\\x82', hex: 'd0bfd180d0b8d0b2d0b5d182', uint8Array: [208, 191, 209, 128, 208, 184, 208, 178, 208, 181, 209, 130] }, + { unicode: 'test', ascii: 'test', hex: '74657374', uint8Array: [116, 101, 115, 116], binary: '01110100011001010111001101110100' }, + { unicode: 'test test', ascii: 'test test', hex: '746573742074657374', uint8Array: [116, 101, 115, 116, 32, 116, 101, 115, 116], binary: '011101000110010101110011011101000010000001110100011001010111001101110100' }, + { unicode: '嘿', ascii: '\\xe5\\x98\\xbf', hex: 'e598bf', uint8Array: [229, 152, 191], binary: '111001011001100010111111' }, + { unicode: '\xea12 \x12 p5', ascii: '\\xc3\\xaa12 \\x12 p5', hex: 'c3aa31322012207035', uint8Array: [195, 170, 49, 50, 32, 18, 32, 112, 53], binary: '110000111010101000110001001100100010000000010010001000000111000000110101' }, + { unicode: 'hi \n hi \t', ascii: 'hi \\n hi \\t', hex: '6869200a2068692009', uint8Array: [104, 105, 32, 10, 32, 104, 105, 32, 9], binary: '011010000110100100100000000010100010000001101000011010010010000000001001' }, + { unicode: '!@#54ueo\'6&*(){', ascii: '!@#54ueo\'6&*(){', hex: '214023353475656f2736262a28297b', uint8Array: [33, 64, 35, 53, 52, 117, 101, 111, 39, 54, 38, 42, 40, 41, 123], binary: '001000010100000000100011001101010011010001110101011001010110111100100111001101100010011000101010001010000010100101111011' }, + { unicode: 'привет', ascii: '\\xd0\\xbf\\xd1\\x80\\xd0\\xb8\\xd0\\xb2\\xd0\\xb5\\xd1\\x82', hex: 'd0bfd180d0b8d0b2d0b5d182', uint8Array: [208, 191, 209, 128, 208, 184, 208, 178, 208, 181, 209, 130], binary: '110100001011111111010001100000001101000010111000110100001011001011010000101101011101000110000010' }, ] const getStringToBufferTests = defaultValues.map(({ unicode, uint8Array }) => @@ -26,7 +29,6 @@ const getStringToBufferTests = defaultValues.map(({ unicode, uint8Array }) => describe('stringToBuffer', () => { test.each(getStringToBufferTests)('%j', ({ input, expected }) => { - // @ts-ignore const result = stringToBuffer(input) result.data = Array.from(result.data) expect(result).toEqual(expected) @@ -38,7 +40,6 @@ const getAnyToBufferTests = defaultValues.map(({ uint8Array }) => describe('anyToBuffer', () => { test.each(getAnyToBufferTests)('%j', ({ input, expected }) => { - // @ts-ignore const result = anyToBuffer(input) result.data = Array.from(result.data) expect(result).toEqual(expected) @@ -50,7 +51,6 @@ const getHexToBufferTests = defaultValues.map(({ hex, uint8Array }) => describe('hexToBuffer', () => { test.each(getHexToBufferTests)('%j', ({ input, expected }) => { - // @ts-ignore const result = hexToBuffer(input) result.data = Array.from(result.data) expect(result).toEqual(expected) @@ -62,14 +62,12 @@ const getBufferToStringTests = defaultValues.map(({ unicode, uint8Array }) => describe('bufferToString', () => { test.each(getBufferToStringTests)('%j', ({ input, expected }) => { - // @ts-ignore expect(bufferToString(input)).toEqual(expected) }) }) describe('bufferToUTF8', () => { test.each(getBufferToStringTests)('%j', ({ input, expected }) => { - // @ts-ignore expect(bufferToUTF8(input)).toEqual(expected) }) }) @@ -79,7 +77,6 @@ const getBufferToASCIITests = defaultValues.map(({ ascii, uint8Array }) => describe('bufferToASCII', () => { test.each(getBufferToASCIITests)('%j', ({ input, expected }) => { - // @ts-ignore expect(bufferToASCII(input)).toEqual(expected) }) }) @@ -89,14 +86,12 @@ const getBufferToHexTests = defaultValues.map(({ hex, uint8Array }) => describe('bufferToASCII', () => { test.each(getBufferToHexTests)('%j', ({ input, expected }) => { - // @ts-ignore expect(bufferToHex(input)).toEqual(expected) }) }) describe('UTF8ToBuffer', () => { test.each(getStringToBufferTests)('%j', ({ input, expected }) => { - // @ts-ignore const result = UTF8ToBuffer(input) result.data = Array.from(result.data) expect(result).toEqual(expected) @@ -117,3 +112,32 @@ describe('isEqualBuffers', () => { expect(isEqualBuffers(input1, input2)).toEqual(expected) }) }) + +const getBufferToBinaryTests = defaultValues.map(({ binary, uint8Array }) => + ({ input: anyToBuffer(uint8Array), expected: binary })) + +describe('bufferToBinary', () => { + test.each(getBufferToBinaryTests)('%j', ({ input, expected }) => { + expect(bufferToBinary(input)).toEqual(expected) + }) +}) + +describe('binaryToBuffer', () => { + test.each(getBufferToBinaryTests)('%j', ({ input, expected }) => { + expect(binaryToBuffer(expected)).toEqual(input) + }) +}) + +const javaValues = [ + { uint8Array: [172, 237, 0, 5, 115, 114, 0, 8, 69, 109, 112, 108, 111, 121, 101, 101, 2, 94, 116, 52, 103, 198, 18, 60, 2, 0, 3, 73, 0, 6, 110, 117, 109, 98, 101, 114, 76, 0, 7, 97, 100, 100, 114, 101, 115, 115, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 76, 0, 4, 110, 97, 109, 101, 113, 0, 126, 0, 1, 120, 112, 0, 0, 0, 101, 116, 0, 25, 80, 104, 111, 107, 107, 97, 32, 75, 117, 97, 110, 44, 32, 65, 109, 98, 101, 104, 116, 97, 32, 80, 101, 101, 114, 116, 0, 9, 82, 101, 121, 97, 110, 32, 65, 108, 105], value: { annotations: [], className: 'Employee', fields: [{ number: 101 }, { address: 'Phokka Kuan, Ambehta Peer' }, { name: 'Reyan Ali' }], serialVersionUid: 170701604314812988n } }, + { uint8Array: [172, 237, 0, 5, 115, 114, 0, 32, 115, 101, 114, 105, 97, 108, 105, 122, 97, 116, 105, 111, 110, 68, 101, 109, 111, 46, 65, 110, 110, 111, 116, 97, 116, 105, 111, 110, 84, 101, 115, 116, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 1, 73, 0, 6, 110, 117, 109, 98, 101, 114, 120, 112, 0, 0, 0, 90], value: { annotations: [], className: 'serializationDemo.AnnotationTest', fields: [{ number: 90 }], serialVersionUid: 2n } }, +] + +const getBufferToJavaTests = javaValues.map(({ uint8Array, value }) => + ({ input: anyToBuffer(uint8Array), expected: value })) + +describe('bufferToJava', () => { + test.each(getBufferToJavaTests)('%o', ({ input, expected }) => { + expect(bufferToJava(input)).toEqual(expected) + }) +}) diff --git a/redisinsight/ui/src/utils/tests/formatters/valueFormatters.spec.ts b/redisinsight/ui/src/utils/tests/formatters/valueFormatters.spec.ts index 9c4ad021ef..04bf484500 100644 --- a/redisinsight/ui/src/utils/tests/formatters/valueFormatters.spec.ts +++ b/redisinsight/ui/src/utils/tests/formatters/valueFormatters.spec.ts @@ -1,4 +1,5 @@ import { encode } from '@msgpack/msgpack' +import { serialize } from 'php-serialize' import { KeyValueFormat } from 'uiSrc/constants' import { anyToBuffer, bufferToSerializedFormat, stringToBuffer, stringToSerializedBufferFormat } from 'uiSrc/utils' @@ -41,6 +42,27 @@ describe('bufferToSerializedFormat', () => { }) }) }) + + describe(KeyValueFormat.PHP, () => { + describe('should properly serialize', () => { + const testValues = [[1], '""', 6677, true, { a: { b: [1, 2, '3'] } }].map((v) => ({ + input: stringToBuffer(serialize(v)), + expected: JSON.stringify(v) + })) + + test.each(testValues)('test %j', ({ input, expected }) => { + expect(bufferToSerializedFormat(KeyValueFormat.PHP, input)).toEqual(expected) + }) + }) + + describe('should properly return value with invalid values', () => { + const testValues = ['1-', '[1, 2,]', '{ zx1***.['] + + test.each(testValues)('test json values', (val) => { + expect(bufferToSerializedFormat(KeyValueFormat.PHP, stringToBuffer(val))).toEqual(val) + }) + }) + }) }) describe('stringToSerializedBufferFormat', () => { @@ -82,4 +104,25 @@ describe('stringToSerializedBufferFormat', () => { }) }) }) + + describe(KeyValueFormat.PHP, () => { + describe('should properly unserialize', () => { + const testValues = [[1], '""', 6677, true, { a: { b: [1, 2, '3'] } }].map((v) => ({ + input: JSON.stringify(v), + expected: stringToBuffer(serialize(v)) + })) + + test.each(testValues)('test %j', ({ input, expected }) => { + expect(stringToSerializedBufferFormat(KeyValueFormat.PHP, input)).toEqual(expected) + }) + }) + + describe('should properly return value with invalid values', () => { + const testValues = ['1-', '[1, 2,]', '{ zx1***.['] + + test.each(testValues)('test json values', (val) => { + expect(stringToSerializedBufferFormat(KeyValueFormat.PHP, val)).toEqual(stringToBuffer(val)) + }) + }) + }) }) diff --git a/redisinsight/ui/src/utils/tests/getLetterByIndex.spec.ts b/redisinsight/ui/src/utils/tests/getLetterByIndex.spec.ts new file mode 100644 index 0000000000..e882ab28ab --- /dev/null +++ b/redisinsight/ui/src/utils/tests/getLetterByIndex.spec.ts @@ -0,0 +1,18 @@ +import { getLetterByIndex } from 'uiSrc/utils' + +const getLetterByIndexTests: any[] = [ + [0, 'A'], + [5, 'F'], + [25, 'Z'], + [26, 'AA'], + [52, 'BA'], + [522, 'TC'], + [1024, 'AMK'], +] + +describe('getLetterByIndex', () => { + it.each(getLetterByIndexTests)('for input: %s (index), should be output: %s', + (index, expected) => { + expect(getLetterByIndex(index)).toBe(expected) + }) +}) diff --git a/redisinsight/ui/src/utils/tests/truncateTTL.spec.ts b/redisinsight/ui/src/utils/tests/truncateTTL.spec.ts index d5eac14b71..f934e1ae98 100644 --- a/redisinsight/ui/src/utils/tests/truncateTTL.spec.ts +++ b/redisinsight/ui/src/utils/tests/truncateTTL.spec.ts @@ -1,5 +1,5 @@ import { - truncateTTLToDuration, + truncateNumberToDuration, truncateNumberToFirstUnit, truncateTTLToRange, truncateTTLToSeconds, @@ -89,8 +89,8 @@ describe('Truncate TTL util tests', () => { }) }) - describe('truncateTTLToDuration', () => { - it('truncateTTLToDuration should return appropriate value', () => { + describe('truncateNumberToDuration', () => { + it('truncateNumberToDuration should return appropriate value', () => { const ttl1 = 100 const ttl2 = 1_534 const ttl3 = 54_334 @@ -105,12 +105,12 @@ describe('Truncate TTL util tests', () => { const expectedResponse5 = '3 yr, 6 mo, 19 d, 10 h, 32 min, 10 s' const expectedResponse6 = '67 yr, 2 mo, 6 d, 12 h, 38 min, 20 s' - expect(truncateTTLToDuration(ttl1)).toEqual(expectedResponse1) - expect(truncateTTLToDuration(ttl2)).toEqual(expectedResponse2) - expect(truncateTTLToDuration(ttl3)).toEqual(expectedResponse3) - expect(truncateTTLToDuration(ttl4)).toEqual(expectedResponse4) - expect(truncateTTLToDuration(ttl5)).toEqual(expectedResponse5) - expect(truncateTTLToDuration(ttl6)).toEqual(expectedResponse6) + expect(truncateNumberToDuration(ttl1)).toEqual(expectedResponse1) + expect(truncateNumberToDuration(ttl2)).toEqual(expectedResponse2) + expect(truncateNumberToDuration(ttl3)).toEqual(expectedResponse3) + expect(truncateNumberToDuration(ttl4)).toEqual(expectedResponse4) + expect(truncateNumberToDuration(ttl5)).toEqual(expectedResponse5) + expect(truncateNumberToDuration(ttl6)).toEqual(expectedResponse6) }) }) diff --git a/redisinsight/ui/src/utils/truncateTTL.ts b/redisinsight/ui/src/utils/truncateTTL.ts index f3a749c4c9..adc328208b 100644 --- a/redisinsight/ui/src/utils/truncateTTL.ts +++ b/redisinsight/ui/src/utils/truncateTTL.ts @@ -54,11 +54,11 @@ export const truncateTTLToRange = (ttl: number) => { // 500 => 8min, 20s // 1500 => 25 min // 2500000 => 28d, 22h, 26min -export const truncateTTLToDuration = (ttl: number): string => { +export const truncateNumberToDuration = (number: number): string => { try { const duration = intervalToDuration({ start: 0, - end: ttl * 1_000, + end: number * 1_000, }) const formattedDuration = formatDuration(duration, { @@ -78,5 +78,5 @@ export const truncateTTLToDuration = (ttl: number): string => { export const truncateTTLToSeconds = (ttl: number) => ttl?.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ') ?? '' -export const truncateNumberToFirstUnit = (ttl: number): string => - truncateTTLToDuration(ttl).split(TRUNCATE_DELIMITER)[0] +export const truncateNumberToFirstUnit = (number: number): string => + truncateNumberToDuration(number).split(TRUNCATE_DELIMITER)[0] diff --git a/redisinsight/ui/tsconfig.json b/redisinsight/ui/tsconfig.json new file mode 100644 index 0000000000..4082f16a5d --- /dev/null +++ b/redisinsight/ui/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/redisinsight/yarn.lock b/redisinsight/yarn.lock index 18d5dbfbbf..294fa3fcbd 100644 --- a/redisinsight/yarn.lock +++ b/redisinsight/yarn.lock @@ -2,70 +2,101 @@ # yarn lockfile v1 -abbrev@1: +"@gar/promisify@^1.0.1": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" + integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== + +"@mapbox/node-pre-gyp@^1.0.0": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc" + integrity sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + +"@npmcli/fs@^1.0.0": version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" + integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" -ajv@^6.12.3: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== 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" + mkdirp "^1.0.4" + rimraf "^3.0.2" -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== +agent-base@6, agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" - integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== +agentkeepalive@^4.1.3: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" + integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== dependencies: - safer-buffer "~2.1.0" + clean-stack "^2.0.0" + indent-string "^4.0.0" -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" -aws4@^1.8.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" - integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" balanced-match@^1.0.0: version "1.0.2" @@ -77,13 +108,6 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - bl@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -93,13 +117,6 @@ bl@^4.0.3: inherits "^2.0.4" readable-stream "^3.4.0" -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= - dependencies: - inherits "~2.0.0" - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -116,91 +133,105 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +cacache@^15.2.0: + version "15.3.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + dependencies: + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== -code-point-at@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" - integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +color-support@^1.1.2, color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -console-control-strings@^1.0.0, console-control-strings@~1.1.0: +console-control-strings@^1.0.0, console-control-strings@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= -core-util-is@1.0.2, core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -debug@^3.2.6: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== +debug@4, debug@^4.1.0, debug@^4.3.3: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: - ms "^2.1.1" + ms "2.1.2" -decompress-response@^4.2.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" - integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== dependencies: - mimic-response "^2.0.0" + mimic-response "^3.1.0" deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= -detect-libc@^1.0.2, detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= +depd@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +detect-libc@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= +encoding@^0.1.12: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" + iconv-lite "^0.6.2" end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" @@ -209,104 +240,73 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + expand-template@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -fast-deep-equal@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-minipass@^1.2.5: - version "1.2.7" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" - integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== dependencies: - minipass "^2.6.0" + minipass "^3.0.0" fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fstream@^1.0.0, fstream@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" - integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== dependencies: - aproba "^1.0.3" + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" + has-unicode "^2.0.1" + signal-exit "^3.0.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.5" github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= -glob@^7.0.3, glob@^7.1.3: +glob@^7.1.3: version "7.1.7" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== @@ -318,56 +318,83 @@ glob@^7.0.3, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -graceful-fs@^4.1.2: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== +glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" -has-unicode@^2.0.0: +graceful-fs@^4.2.6: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +has-unicode@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= +http-cache-semantics@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +https-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" + ms "^2.0.0" -iconv-lite@^0.4.4: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== +iconv-lite@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: - safer-buffer ">= 2.1.2 < 3" + safer-buffer ">= 2.1.2 < 3.0.0" ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore-walk@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335" - integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ== - dependencies: - minimatch "^3.0.4" +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== inflight@^1.0.4: version "1.0.6" @@ -377,7 +404,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3: +inherits@2, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -387,133 +414,164 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -is-fullwidth-code-point@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" - integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= - dependencies: - number-is-nan "^1.0.0" - -is-fullwidth-code-point@^2.0.0: +ip@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" + integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= +keytar@^7.9.0: + version "7.9.0" + resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.9.0.tgz#4c6225708f51b50cbf77c5aae81721964c2918cb" + integrity sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ== + dependencies: + node-addon-api "^4.3.0" + prebuild-install "^7.0.1" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-fetch-happen@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.2.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.2" + promise-retry "^2.0.1" + socks-proxy-agent "^6.0.0" + ssri "^8.0.0" + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +minimist@^1.2.0, minimist@^1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" -jsprim@^1.2.2: +minipass-fetch@^1.3.2: version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz#d75e0091daac1b0ffd7e9d41629faff7d0c1f1b6" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" -keytar@^7.7.0: - version "7.7.0" - resolved "https://registry.yarnpkg.com/keytar/-/keytar-7.7.0.tgz#3002b106c01631aa79b1aa9ee0493b94179bbbd2" - integrity sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A== +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== dependencies: - node-addon-api "^3.0.0" - prebuild-install "^6.0.0" + minipass "^3.0.0" -mime-db@1.49.0: - version "1.49.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" - integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.32" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5" - integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A== +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== dependencies: - mime-db "1.49.0" - -mimic-response@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" - integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + minipass "^3.0.0" -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== dependencies: - brace-expansion "^1.1.7" + minipass "^3.0.0" -minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" - integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: + version "3.3.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.4.tgz#ca99f95dd77c43c7a76bf51e6d200025eee0ffae" + integrity sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw== dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" + yallist "^4.0.0" -minizlib@^1.2.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" - integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== +minizlib@^2.0.0, minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== dependencies: - minipass "^2.9.0" + minipass "^3.0.0" + yallist "^4.0.0" mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -ms@^2.1.1: +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.0.0: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -523,118 +581,74 @@ napi-build-utils@^1.0.1: resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== -needle@^2.2.1: - version "2.8.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.8.0.tgz#1c8ef9c1a2c29dcc1e83d73809d7bc681c80a048" - integrity sha512-ZTq6WYkN/3782H1393me3utVYdq2XyqNUFBsprEE3VMAT0+hP/cItpnITpqsY6ep2yeFE4Tqtqwc74VqUlUYtw== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - -node-abi@^2.21.0: - version "2.30.1" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf" - integrity sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w== - dependencies: - semver "^5.4.1" - -node-addon-api@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" - integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== - -node-gyp@3.x: - version "3.8.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" - integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== - dependencies: - fstream "^1.0.0" - glob "^7.0.3" - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "^2.87.0" - rimraf "2" - semver "~5.3.0" - tar "^2.0.0" - which "1" - -node-pre-gyp@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054" - integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - -"nopt@2 || 3": - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= +negotiator@^0.6.2: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-abi@^3.3.0: + version "3.24.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.24.0.tgz#b9d03393a49f2c7e147d0c99f180e680c27c1599" + integrity sha512-YPG3Co0luSu6GwOBsmIdGW6Wx0NyNDLg/hriIyDllVsNwnI6UeqaWShxC3lbH4LtEQUgoLP3XR1ndXiDAWvmRw== + dependencies: + semver "^7.3.5" + +node-addon-api@^4.2.0, node-addon-api@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" + integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== + +node-fetch@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-gyp@8.x: + version "8.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" + integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^9.1.0" + nopt "^5.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== dependencies: abbrev "1" -nopt@^4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" - integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== - dependencies: - abbrev "1" - osenv "^0.1.4" - -npm-bundled@^1.0.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" - integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== - dependencies: - npm-normalize-package-bin "^1.0.1" - -npm-normalize-package-bin@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" - integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== - -npm-packlist@^1.1.6: - version "1.4.8" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" - integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" - npm-normalize-package-bin "^1.0.1" + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.1, npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - -number-is-nan@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4.1.0: +object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -646,62 +660,48 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -os-homedir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" - integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= - -os-tmpdir@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -osenv@0, osenv@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" + aggregate-error "^3.0.0" path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -prebuild-install@^6.0.0: - version "6.1.4" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f" - integrity sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ== +prebuild-install@^7.0.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45" + integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw== dependencies: - detect-libc "^1.0.3" + detect-libc "^2.0.0" expand-template "^2.0.3" github-from-package "0.0.0" minimist "^1.2.3" mkdirp-classic "^0.5.3" napi-build-utils "^1.0.1" - node-abi "^2.21.0" - npmlog "^4.0.1" + node-abi "^3.3.0" pump "^3.0.0" rc "^1.2.7" - simple-get "^3.0.3" + simple-get "^4.0.0" tar-fs "^2.0.0" tunnel-agent "^0.6.0" -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== -psl@^1.1.28: - version "1.8.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" - integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" pump@^3.0.0: version "3.0.0" @@ -711,16 +711,6 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^2.1.0, punycode@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -731,20 +721,7 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -readable-stream@^2.0.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.1.1, readable-stream@^3.4.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -753,70 +730,41 @@ readable-stream@^3.1.1, readable-stream@^3.4.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" -request@^2.87.0: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== -rimraf@2, rimraf@^2.6.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: +safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sax@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -semver@^5.3.0, semver@^5.4.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== +semver@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@~5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" - integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8= +semver@^7.3.5: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" -set-blocking@~2.0.0: +set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= @@ -826,61 +774,73 @@ signal-exit@^3.0.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + simple-concat@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f" integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== -simple-get@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.1.tgz#cc7ba77cfbe761036fbfce3d021af25fc5584d55" - integrity sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA== +simple-get@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== dependencies: - decompress-response "^4.2.0" + decompress-response "^6.0.0" once "^1.3.1" simple-concat "^1.0.0" -sqlite3@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.0.2.tgz#00924adcc001c17686e0a6643b6cbbc2d3965083" - integrity sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA== +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +socks-proxy-agent@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz#2687a31f9d7185e38d530bef1944fe1f1496d6ce" + integrity sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ== dependencies: - node-addon-api "^3.0.0" - node-pre-gyp "^0.11.0" + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks@^2.6.2: + version "2.7.0" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.0.tgz#f9225acdb841e874dca25f870e9130990f3913d0" + integrity sha512-scnOe9y4VuiNUULJN72GrM26BNOjVsfPXI+j+98PkyEfsIXroa5ofyjT+FzGvn/xHs73U2JtoBYAVx9Hl4quSA== + dependencies: + ip "^2.0.0" + smart-buffer "^4.2.0" + +sqlite3@^5.0.11: + version "5.0.11" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.0.11.tgz#102c835d70be66da9d95a383fd6ea084a082ef7f" + integrity sha512-4akFOr7u9lJEeAWLJxmwiV43DJcGV7w3ab7SjQFAFaTVyknY3rZjvXTKIVtWqUoY4xwhjwoHKYs2HDW2SoHVsA== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.0" + node-addon-api "^4.2.0" + tar "^6.1.11" optionalDependencies: - node-gyp "3.x" - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -string-width@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" - integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + node-gyp "8.x" + +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - strip-ansi "^3.0.0" + minipass "^3.1.1" -"string-width@^1.0.2 || 2": - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" string_decoder@^1.1.1: version "1.3.0" @@ -889,26 +849,12 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: - ansi-regex "^3.0.0" + ansi-regex "^5.0.1" strip-json-comments@~2.0.1: version "2.0.1" @@ -936,35 +882,22 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -tar@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" - integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== - dependencies: - block-stream "*" - fstream "^1.0.12" - inherits "2" - -tar@^4: - version "4.4.15" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.15.tgz#3caced4f39ebd46ddda4d6203d48493a919697f8" - integrity sha512-ItbufpujXkry7bHH9NpQyTXPbJ72iTlXgkBAYsAjDXk3Ds8t/3NfO5P4xZGy7u+sYuQUbimgzswX4uQIEeNVOA== +tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: + version "6.1.11" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.8.6" - minizlib "^1.2.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.3" + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== tunnel-agent@^0.6.0: version "0.6.0" @@ -973,57 +906,58 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== dependencies: - punycode "^2.1.0" + imurmurhash "^0.1.4" -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= -uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" + tr46 "~0.0.3" + webidl-conversions "^3.0.0" -which@1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== +which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== +wide-align@^1.1.2, wide-align@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== dependencies: - string-width "^1.0.2 || 2" + string-width "^1.0.2 || 2 || 3 || 4" wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -yallist@^3.0.0, yallist@^3.0.3: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== diff --git a/tests/e2e/common-actions/cli-actions.ts b/tests/e2e/common-actions/cli-actions.ts new file mode 100644 index 0000000000..348a31ad5a --- /dev/null +++ b/tests/e2e/common-actions/cli-actions.ts @@ -0,0 +1,33 @@ +import { t } from 'testcafe'; +import { CliPage } from '../pageObjects'; + +const cliPage = new CliPage(); + +export class CliActions { + + /** + * Check list of commands searched + * @param searchedCommand Searched command in Command Helper + * @param listToCompare The list with commands to compare with opened in Command Helper + */ + async checkSearchedCommandInCommandHelper(searchedCommand: string, listToCompare: string[]): Promise { + await t.typeText(cliPage.cliHelperSearch, searchedCommand, { speed: 0.5 }); + //Verify results in the output + const commandsCount = await cliPage.cliHelperOutputTitles.count; + for (let i = 0; i < commandsCount; i++) { + await t.expect(cliPage.cliHelperOutputTitles.nth(i).textContent).eql(listToCompare[i], 'Results in the output contains searched value'); + } + } + + /** + * Check commands list + * @param listToCompare The list with commands to compare with opened in Command Helper + */ + async checkCommandsInCommandHelper(listToCompare: string[]): Promise { + //Verify results in the output + const commandsCount = await cliPage.cliHelperOutputTitles.count; + for (let i = 0; i < commandsCount; i++) { + await t.expect(cliPage.cliHelperOutputTitles.nth(i).textContent).eql(listToCompare[i], 'Results in the output not contain searched value'); + } + } +} diff --git a/tests/e2e/docker.web.docker-compose.yml b/tests/e2e/docker.web.docker-compose.yml index 0f914bd947..55d743f7fd 100644 --- a/tests/e2e/docker.web.docker-compose.yml +++ b/tests/e2e/docker.web.docker-compose.yml @@ -8,6 +8,7 @@ services: tty: true volumes: - ./results:/usr/src/app/results + - ./report:/usr/src/app/report - ./plugins:/usr/src/app/plugins - .redisinsight-v2:/root/.redisinsight-v2 env_file: diff --git a/tests/e2e/helpers/api/api-database.ts b/tests/e2e/helpers/api/api-database.ts index 21703a2d91..a28f1d523e 100644 --- a/tests/e2e/helpers/api/api-database.ts +++ b/tests/e2e/helpers/api/api-database.ts @@ -1,7 +1,7 @@ import { t } from 'testcafe'; import * as request from 'supertest'; import { asyncFilter, doAsyncStuff } from '../async-helper'; -import { AddNewDatabaseParameters, OSSClusterParameters, databaseParameters, SentinelParameters } from '../../pageObjects/add-redis-database-page'; +import { AddNewDatabaseParameters, OSSClusterParameters, databaseParameters, SentinelParameters, ClusterNodes } from '../../pageObjects/add-redis-database-page'; import { Common } from '../common'; const common = new Common(); @@ -122,16 +122,18 @@ export async function getDatabaseByConnectionType(connectionType?: string): Prom */ export async function deleteAllDatabasesApi(): Promise { const allDatabases = await getAllDatabases(); - const databaseIds = []; - for (let i = 0; i < allDatabases.length; i++) { - const dbData = JSON.parse(JSON.stringify(allDatabases[i])); - databaseIds.push(dbData.id); - } - if (databaseIds.length > 0) { - await request(endpoint).delete('/instance') - .send({ 'ids': databaseIds }) - .set('Accept', 'application/json') - .expect(200); + if (allDatabases.length > 0) { + const databaseIds = []; + for (let i = 0; i < allDatabases.length; i++) { + const dbData = JSON.parse(JSON.stringify(allDatabases[i])); + databaseIds.push(dbData.id); + } + if (databaseIds.length > 0) { + await request(endpoint).delete('/instance') + .send({ 'ids': databaseIds }) + .set('Accept', 'application/json') + .expect(200); + } } } @@ -193,3 +195,18 @@ export async function deleteStandaloneDatabasesApi(databasesParameters: AddNewDa }); } } + +/** + * Get OSS Cluster nodes + * @param databaseParameters The database parameters + */ +export async function getClusterNodesApi(databaseParameters: OSSClusterParameters): Promise { + const databaseId = await getDatabaseByName(databaseParameters.ossClusterDatabaseName); + const response = await request(endpoint) + .get(`/instance/${databaseId}/cluster-details`) + .set('Accept', 'application/json') + .expect(200); + let nodes = await response.body.nodes; + let nodeNames = await nodes.map((node: ClusterNodes) => (node.host + ':' + node.port)); + return nodeNames; +} diff --git a/tests/e2e/helpers/common.ts b/tests/e2e/helpers/common.ts index e32e192771..0bc196c3c3 100644 --- a/tests/e2e/helpers/common.ts +++ b/tests/e2e/helpers/common.ts @@ -1,4 +1,4 @@ -import {RequestMock, t} from 'testcafe'; +import { ClientFunction, RequestMock, t } from 'testcafe'; import { Chance } from 'chance'; import {apiUrl, commonUrl} from './conf'; @@ -35,7 +35,7 @@ export class Common { * @param length The amount of array elements */ async createArrayWithKeyValue(length: number): Promise { - const arr = []; + const arr: string[] = []; for(let i = 1; i <= length * 2; i++) { arr[i] = `${chance.word({ length: 10 })}-key${i}`; arr[i + 1] = `${chance.word({ length: 10 })}-value${i}`; @@ -48,8 +48,8 @@ export class Common { * Create array of keys and values for using in OSS Cluster * @param length The amount of array elements */ - async createArrayWithKeyValueForOSSCluster(length: number): Promise { - const arr = []; + async createArrayWithKeyValueForOSSCluster(length: number): Promise { + const arr: string[] = []; for(let i = 1; i <= length * 2; i++) { arr[i] = `{user1}:${chance.word({ length: 10 })}-key${i}`; arr[i + 1] = `${chance.word({ length: 10 })}-value${i}`; @@ -64,7 +64,7 @@ export class Common { * @param keyName The name of the key */ async createArrayWithKeyValueAndKeyname(length: number, keyName: string): Promise { - const keyNameArray = []; + const keyNameArray: string[] = []; for(let i = 1; i <= length; i++) { const key = `${keyName}${i}`; const value = `value${i}`; @@ -86,7 +86,7 @@ export class Common { * @param length The amount of array elements */ async createArray(length: number): Promise { - const arr = []; + const arr: string[] = []; for(let i = 1; i <= length; i++) { arr[i] = `${i}`; } @@ -123,4 +123,20 @@ export class Common { getEndpoint(): string { return apiUrl; } + + /** + * Reload page + */ + async reloadPage(): Promise { + await t.eval(() => location.reload()); + } + + /** + * Check opened URL + * @param expectedUrl Expected link that is compared with actual + */ + async checkURL(expectedUrl: string): Promise { + const getPageUrl = ClientFunction(() => window.location.href); + await t.expect(getPageUrl()).eql(expectedUrl, 'Opened URL is not correct'); + } } diff --git a/tests/e2e/helpers/conf.ts b/tests/e2e/helpers/conf.ts index d5f725c188..7aabf767d2 100644 --- a/tests/e2e/helpers/conf.ts +++ b/tests/e2e/helpers/conf.ts @@ -44,12 +44,14 @@ export const ossSentinelConfig = { masters: [{ alias: 'primary-group-1', db: '0', - name: 'primary-group-1' + name: 'primary-group-1', + password: 'defaultpass' }, { alias: 'primary-group-2', db: '0', - name: 'primary-group-2' + name: 'primary-group-2', + password: 'defaultpass' }], name: ['primary-group-1', 'primary-group-2'] }; diff --git a/tests/e2e/helpers/constants.ts b/tests/e2e/helpers/constants.ts index f59a0e239f..7168d27fd3 100644 --- a/tests/e2e/helpers/constants.ts +++ b/tests/e2e/helpers/constants.ts @@ -18,7 +18,7 @@ export const COMMANDS_TO_CREATE_KEY = Object.freeze({ [KeyTypesTexts.ZSet]: (key: string, member = 'member', score = 1) => `ZADD ${key} ${score} '${member}'`, [KeyTypesTexts.String]: (key: string, value = 'val') => `SET ${key} '${value}'`, [KeyTypesTexts.ReJSON]: (key: string, json = '"val"') => `JSON.SET ${key} . '${json}'`, - [KeyTypesTexts.Stream]: (key: string, value: string | number = 'value', field: string | number = 1) => `XADD ${key} * '${field}' '${value}'`, + [KeyTypesTexts.Stream]: (key: string, value: string | number = 'value', field: string | number = 'field') => `XADD ${key} * '${field}' '${value}'`, [KeyTypesTexts.Graph]: (key: string) => `GRAPH.QUERY ${key} "CREATE ()"`, [KeyTypesTexts.TimeSeries]: (key: string) => `TS.CREATE ${key}` }); diff --git a/tests/e2e/helpers/database.ts b/tests/e2e/helpers/database.ts index d73d570915..868fb6dd4d 100644 --- a/tests/e2e/helpers/database.ts +++ b/tests/e2e/helpers/database.ts @@ -10,6 +10,7 @@ import { CliPage } from '../pageObjects'; import { addNewStandaloneDatabaseApi, discoverSentinelDatabaseApi } from './api/api-database'; +import { Common } from './common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const addRedisDatabasePage = new AddRedisDatabasePage(); @@ -18,6 +19,7 @@ const autoDiscoverREDatabases = new AutoDiscoverREDatabases(); const browserPage = new BrowserPage(); const userAgreementPage = new UserAgreementPage(); const cliPage = new CliPage(); +const common = new Common(); /** * Add a new database manually using host and port @@ -126,7 +128,7 @@ export async function acceptLicenseTermsAndAddDatabaseApi(databaseParameters: Ad await acceptLicenseTerms(); await addNewStandaloneDatabaseApi(databaseParameters); // Reload Page to see the new added database through api - await t.eval(() => location.reload()); + await common.reloadPage(); //Connect to DB await myRedisDatabasePage.clickOnDBByName(databaseName); } @@ -151,7 +153,7 @@ export async function acceptLicenseTermsAndAddSentinelDatabaseApi(databaseParame await acceptLicenseTerms(); await discoverSentinelDatabaseApi(databaseParameters); // Reload Page to see the database added through api - await t.eval(() => location.reload()); + await common.reloadPage(); //Connect to DB await myRedisDatabasePage.clickOnDBByName(databaseParameters.name[1] ?? ''); } @@ -182,7 +184,7 @@ export async function acceptLicenseTermsAndAddRECloudDatabase(databaseParameters await t.click(addRedisDatabasePage.addRedisDatabaseButton); // Reload page until db appears do { - await t.eval(() => location.reload()); + await common.reloadPage(); } while (!(await dbSelector.exists) && Date.now() - startTime < searchTimeout); await t.expect(myRedisDatabasePage.dbNameList.withExactText(databaseParameters.databaseName ?? '').exists).ok('The existence of the database', { timeout: 5000 }); @@ -193,7 +195,6 @@ export async function acceptLicenseTermsAndAddRECloudDatabase(databaseParameters export async function acceptLicenseTerms(): Promise { await t.maximizeWindow(); await userAgreementPage.acceptLicenseTerms(); - await t.expect(userAgreementPage.userAgreementsPopup.visible).notOk('The user agreements popup is not shown', { timeout: 2000 }); } //Accept License terms and connect to the RedisStack database diff --git a/tests/e2e/helpers/keys.ts b/tests/e2e/helpers/keys.ts index df07e2752a..46151913ff 100644 --- a/tests/e2e/helpers/keys.ts +++ b/tests/e2e/helpers/keys.ts @@ -3,10 +3,9 @@ import { t } from 'testcafe'; import { Chance } from 'chance'; import { COMMANDS_TO_CREATE_KEY } from '../helpers/constants'; import { BrowserPage, CliPage } from '../pageObjects'; -import { KeyData } from '../pageObjects/browser-page'; +import { KeyData, AddKeyArguments } from '../pageObjects/browser-page'; import { KeyTypesTexts } from './constants'; import { Common } from './common'; -import { AddKeyArguments } from '../pageObjects/browser-page'; const common = new Common(); const cliPage = new CliPage(); @@ -53,8 +52,8 @@ export async function addKeysViaCli(keyData: KeyData, keyValue?: string, keyFiel for (const { textType, keyName } of keyData) { if (textType in COMMANDS_TO_CREATE_KEY) { textType === 'Hash' || textType === 'Stream' - ? await t.typeText(cliPage.cliCommandInput, COMMANDS_TO_CREATE_KEY[textType](keyName, keyValue, keyField), { paste: true }) - : await t.typeText(cliPage.cliCommandInput, COMMANDS_TO_CREATE_KEY[textType](keyName, keyValue), { paste: true }); + ? await t.typeText(cliPage.cliCommandInput, COMMANDS_TO_CREATE_KEY[textType](keyName, keyValue, keyField), { paste: true }) + : await t.typeText(cliPage.cliCommandInput, COMMANDS_TO_CREATE_KEY[textType](keyName, keyValue), { paste: true }); await t.pressKey('enter'); } } @@ -84,15 +83,17 @@ export async function populateDBWithHashes(host: string, port: string, keyArgume const dbConf = { host, port: Number(port) }; const client = createClient(dbConf); - await client.on('error', async function (error: string) { + await client.on('error', async function(error: string) { throw new Error(error); }); - await client.on('connect', async function () { + await client.on('connect', async function() { if (keyArguments.keysCount != undefined) { for (let i = 0; i < keyArguments.keysCount; i++) { const keyName = `${keyArguments.keyNameStartWith}${common.generateWord(20)}`; - await client.hset([keyName, 'field1', 'Hello'], async (error: string) => { - if (error) throw error; + await client.hset([keyName, 'field1', 'Hello'], async(error: string) => { + if (error) { + throw error; + } }); } } @@ -111,10 +112,10 @@ export async function populateHashWithFields(host: string, port: string, keyArgu const client = createClient(dbConf); const fields: string[] = []; - await client.on('error', async function (error: string) { + await client.on('error', async function(error: string) { throw new Error(error); }); - await client.on('connect', async function () { + await client.on('connect', async function() { if (keyArguments.fieldsCount != undefined) { for (let i = 0; i < keyArguments.fieldsCount; i++) { const field = `${keyArguments.fieldStartWith}${common.generateWord(10)}`; @@ -122,8 +123,10 @@ export async function populateHashWithFields(host: string, port: string, keyArgu fields.push(field, fieldValue); } } - await client.hset(keyArguments.keyName, fields, async (error: string) => { - if (error) throw error; + await client.hset(keyArguments.keyName, fields, async(error: string) => { + if (error) { + throw error; + } }); await client.quit(); }); @@ -140,18 +143,20 @@ export async function populateListWithElements(host: string, port: string, keyAr const client = createClient(dbConf); const elements: string[] = []; - await client.on('error', async function (error: string) { + await client.on('error', async function(error: string) { throw new Error(error); }); - await client.on('connect', async function () { + await client.on('connect', async function() { if (keyArguments.elementsCount != undefined) { for (let i = 0; i < keyArguments.elementsCount; i++) { const element = `${keyArguments.elementStartWith}${common.generateWord(10)}`; elements.push(element); } } - await client.lpush(keyArguments.keyName, elements, async (error: string) => { - if (error) throw error; + await client.lpush(keyArguments.keyName, elements, async(error: string) => { + if (error) { + throw error; + } }); await client.quit(); }); @@ -168,18 +173,20 @@ export async function populateSetWithMembers(host: string, port: string, keyArgu const client = createClient(dbConf); const members: string[] = []; - await client.on('error', async function (error: string) { + await client.on('error', async function(error: string) { throw new Error(error); }); - await client.on('connect', async function () { + await client.on('connect', async function() { if (keyArguments.membersCount != undefined) { for (let i = 0; i < keyArguments.membersCount; i++) { const member = `${keyArguments.memberStartWith}${common.generateWord(10)}`; members.push(member); } } - await client.sadd(keyArguments.keyName, members, async (error: string) => { - if (error) throw error; + await client.sadd(keyArguments.keyName, members, async(error: string) => { + if (error) { + throw error; + } }); await client.quit(); }); @@ -194,12 +201,14 @@ export async function deleteAllKeysFromDB(host: string, port: string): Promise { - if (error) throw error; + if (error) { + throw error; + } }); await client.quit(); }); diff --git a/tests/e2e/package.json b/tests/e2e/package.json index b2cfe2ecd5..7f5f289cb9 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -12,13 +12,13 @@ "build:web": "yarn --cwd ../../ build:web", "redis:last": "docker run --name redis-last-version -p 7777:6379 -d redislabs/redismod", "start:app": "cross-env SERVER_STATIC_CONTENT=true yarn start:api", - "test:chrome": "testcafe --concurrency 1 chrome tests/ -r html:./report/report.html,spec -e -s takeOnFails=true,path=report/screenshots,pathPattern=${USERAGENT}/${DATE}_${TIME}/${FIXTURE}_${TEST_INDEX}.png", + "test:chrome": "testcafe --cache --concurrency 1 chrome tests/ -r html:./report/report.html,spec -e -s takeOnFails=true,path=report/screenshots,pathPattern=${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST_ID}_${FILE_INDEX}.png", "test:chrome:ci": "ts-node ./web.runner.ts", "test": "yarn test:chrome", "lint": "eslint . --ext .ts,.js,.tsx,.jsx", "test:desktop:ci": "ts-node ./desktop.runner.ts", "test:desktop:ci:win": "ts-node ./desktop.runner.win.ts", - "test:desktop": "testcafe electron tests/ --browser-init-timeout 180000 -e -r html:./report/desktop-report.html,spec -s takeOnFails=true,path=report/screenshots,pathPattern=${USERAGENT}/${DATE}_${TIME}/${FIXTURE}_${TEST_INDEX}.png" + "test:desktop": "testcafe electron tests/ --browser-init-timeout 180000 -e -r html:./report/desktop-report.html,spec -s takeOnFails=true,path=report/screenshots,pathPattern=${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST_ID}_${FILE_INDEX}.png" }, "keywords": [], "author": "", diff --git a/tests/e2e/pageObjects/add-redis-database-page.ts b/tests/e2e/pageObjects/add-redis-database-page.ts index e5a02e0669..e0792aa206 100644 --- a/tests/e2e/pageObjects/add-redis-database-page.ts +++ b/tests/e2e/pageObjects/add-redis-database-page.ts @@ -22,6 +22,12 @@ export class AddRedisDatabasePage { connectToDatabaseButton = Selector('[data-testid=connect-to-db-btn]'); connectToRedisStackButton = Selector('[aria-label="Connect to database"]'); discoverSentinelDatabaseButton = Selector('[data-testid=btn-submit]'); + cloneDatabaseButton = Selector('[data-testid=clone-db-btn]'); + sentinelNavigation = Selector('[data-testid=sentinel-nav-group]'); + cloneSentinelNavigation = Selector('[data-testid=sentinel-nav-group-clone]'); + sentinelDatabaseNavigation = Selector('[data-testid=database-nav-group]'); + cloneSentinelDatabaseNavigation = Selector('[data-testid=database-nav-group-clone]'); + cancelButton = Selector('[data-testid=btn-cancel]'); //TEXT INPUTS (also referred to as 'Text fields') hostInput = Selector('[data-testid=host]'); portInput = Selector('[data-testid=port]'); @@ -34,6 +40,12 @@ export class AddRedisDatabasePage { databaseIndexInput = Selector('[data-testid=db]'); errorMessage = Selector('[data-test-subj=toast-error]'); databaseIndexMessage = Selector('[data-testid=db-index-message]'); + primaryGroupNameInput = Selector('[data-testid=primary-group]'); + masterGroupPassword = Selector('[data-testid=sentinel-master-password]'); + //Links + buildFromSource = Selector('a').withExactText('Build from source'); + buildFromDocker = Selector('a').withExactText('Docker'); + buildFromHomebrew = Selector('a').withExactText('Homebrew'); /** * Adding a new redis database @@ -212,3 +224,13 @@ export type databaseParameters = { connectionType?: string, lastConnection?: string }; + +/** + * Nodes in OSS Cluster parameters + * @param host The host of the node + * @param port The port of the node + */ + export type ClusterNodes = { + host: string, + port: string +}; diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index f7c05522d7..719da0b7d8 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -26,6 +26,7 @@ export class BrowserPage { refreshKeyButton = Selector('[data-testid=refresh-key-btn]'); applyButton = Selector('[data-testid=apply-btn]'); editKeyNameButton = Selector('[data-testid=edit-key-btn]'); + editKeyValueButton = Selector('[data-testid=edit-key-value-btn]'); closeKeyButton = Selector('[data-testid=close-key-btn]'); plusAddKeyButton = Selector('[data-testid=btn-add-key]'); addKeyValueItemsButton = Selector('[data-testid=add-key-value-items-btn]'); @@ -87,6 +88,9 @@ export class BrowserPage { saveButton = Selector('[data-testid=save-btn]'); bulkActionsButton = Selector('[data-testid=btn-bulk-actions]'); editHashButton = Selector('[data-testid^=edit-hash-button-]'); + editZsetButton = Selector('[data-testid^=zset-edit-button-]'); + editListButton = Selector('[data-testid^=edit-list-button-]'); + workbenchLinkButton = Selector('[data-test-subj=workbench-page-btn]'); //CONTAINERS streamGroupsContainer = Selector('[data-testid=stream-groups-container]'); streamConsumersContainer = Selector('[data-testid=stream-consumers-container]'); @@ -96,6 +100,7 @@ export class BrowserPage { streamMessagesContainer = Selector('[data-testid=stream-messages-container]'); //LINKS internalLinkToWorkbench = Selector('[data-testid=internal-workbench-link]'); + userSurveyLink = Selector('[data-testid=user-survey-link]'); //OPTION ELEMENTS stringOption = Selector('#string'); jsonOption = Selector('#ReJSON-RL'); @@ -113,7 +118,7 @@ export class BrowserPage { claimTimeOptionSelect = Selector('[data-testid=time-option-select]'); relativeTimeOption = Selector('#idle'); timestampOption = Selector('#time'); - formatSwitcher = Selector('[data-testid=select-format-key-value]'); + formatSwitcher = Selector('[data-testid=select-format-key-value]', { timeout: 2000 }); formatSwitcherIcon = Selector('img[data-testid^=key-value-formatter-option-selected]'); //TABS streamTabGroups = Selector('[data-testid=stream-tab-Groups]'); @@ -128,7 +133,9 @@ export class BrowserPage { hashFieldValueInput = Selector('[data-testid=field-value]'); hashFieldNameInput = Selector('[data-testid=field-name]'); hashFieldValueEditor = Selector('[data-testid=hash-value-editor]'); + listElementEditor = Selector('[data-testid=hash-value-editor]'); listKeyElementInput = Selector('[data-testid=element]'); + listKeyElementEditorInput = Selector('[data-testid=element-value-editor]'); stringKeyValueInput = Selector('[data-testid=string-value]'); jsonKeyValueInput = Selector('[data-testid=json-value]'); setMemberInput = Selector('[data-testid=member-name]'); @@ -155,6 +162,7 @@ export class BrowserPage { claimIdleTimeInput = Selector('[data-testid=time-count]'); claimRetryCountInput = Selector('[data-testid=retry-count]'); lastIdInput = Selector('[data-testid=last-id-field]'); + inlineItemEditor = Selector('[data-testid=inline-item-editor]'); //TEXT ELEMENTS keySizeDetails = Selector('[data-testid=key-size-text]'); keyLengthDetails = Selector('[data-testid=key-length-text]'); @@ -175,7 +183,6 @@ export class BrowserPage { noResultsFound = Selector('[data-test-subj=no-result-found]'); searchAdvices = Selector('[data-test-subj=search-advices]'); keysNumberOfResults = Selector('[data-testid=keys-number-of-results]'); - keysNumberOfScanned = Selector('[data-testid=keys-number-of-scanned]'); keysTotalNumber = Selector('[data-testid=keys-total]'); overviewTotalKeys = Selector('[data-test-subj=overview-total-keys]'); overviewTotalMemory = Selector('[data-test-subj=overview-total-memory]'); @@ -193,7 +200,6 @@ export class BrowserPage { treeViewDeviceKyesCount = Selector('[data-testid^=count_device] span'); ttlValueInKeysTable = Selector('[data-testid^=ttl-]'); stringKeyValue = Selector('.key-details-body pre'); - keyDetailsValue = Selector('.key-details-body div div div'); keyDetailsBadge = Selector('.key-details-header .euiBadge__text'); treeViewKeysItem = Selector('[data-testid*="keys:keys:"]'); treeViewNotPatternedKeys = Selector('[data-testid*="node-item_keys"]'); @@ -204,6 +210,7 @@ export class BrowserPage { multiSearchArea = Selector(this.cssFilteringLabel); keyDetailsHeader = Selector('[data-testid=key-details-header]'); keyListTable = Selector('[data-testid=keyList-table]'); + keyListMessage = Selector('[data-testid=no-result-found-msg]'); keyDetailsTable = Selector('[data-testid=key-details]'); keyNameFormDetails = Selector('[data-testid=key-name-text]'); keyDetailsTTL = Selector('[data-testid=key-ttl-text]'); @@ -235,7 +242,9 @@ export class BrowserPage { rangeLeftTimestamp = Selector('[data-testid=range-left-timestamp]'); rangeRightTimestamp = Selector('[data-testid=range-right-timestamp]'); jsonValue = Selector('[data-testid=value-as-json]'); - + stringValueAsJson = Selector(this.cssJsonValue); + // POPUPS + changeValueWarning = Selector('[data-testid=approve-popover]'); /** * Common part for Add any new key * @param keyName The name of the key @@ -531,15 +540,31 @@ export class BrowserPage { */ async editStringKeyValue(value: string): Promise { await t - .click(this.keyDetailsValue) + .click(this.stringKeyValueInput) .pressKey('ctrl+a delete') .typeText(this.stringKeyValueInput, value) .click(this.applyButton); } - //Get string key value from details + //Get String key value from details async getStringKeyValue(): Promise { - return this.keyDetailsValue.textContent; + return this.stringKeyValueInput.textContent; + } + + /** + * Edit Zset key score from details + * @param value The value of the key + */ + async editZsetKeyScore(value: string): Promise { + await t + .click(this.editZsetButton) + .typeText(this.inlineItemEditor, value, { replace: true, paste: true }) + .click(this.applyButton); + } + + //Get Zset key score from details + async getZsetKeyScore(): Promise { + return this.zsetScoresList.textContent; } /** @@ -564,10 +589,47 @@ export class BrowserPage { async editHashKeyValue(value: string): Promise { await t .click(this.editHashButton) - .typeText(this.hashFieldValueEditor, value, {replace: true, paste: true}) + .typeText(this.hashFieldValueEditor, value, { replace: true, paste: true }) .click(this.applyButton); } + //Get Hash key value from details + async getHashKeyValue(): Promise { + return this.hashFieldValue.textContent; + } + + /** + * Edit List key value from details + * @param value The value of the key + */ + async editListKeyValue(value: string): Promise { + await t + .click(this.editListButton) + .typeText(this.listKeyElementEditorInput, value, { replace: true, paste: true }) + .click(this.applyButton); + } + + //Get List key value from details + async getListKeyValue(): Promise { + return this.listElementsList.textContent; + } + + /** + * Edit JSON key value from details + * @param value The value of the key + */ + async editJsonKeyValue(value: string): Promise { + await t + .click(this.jsonScalarValue) + .typeText(this.inlineItemEditor, value, { replace: true, paste: true }) + .click(this.applyButton); + } + + //Get JSON key value from details + async getJsonKeyValue(): Promise { + return this.jsonKeyValue.textContent; + } + /** * Search by the value in the key details * @param value The value of the search parameter @@ -703,7 +765,7 @@ export class BrowserPage { * @param jsonKey The json key name * @param jsonKeyValue The value of the json key */ - async addJsonKeyOnTheSameLevel(jsonKey: string, jsonKeyValue: any): Promise { + async addJsonKeyOnTheSameLevel(jsonKey: string, jsonKeyValue: string): Promise { await t.click(this.addJsonObjectButton); await t.typeText(this.jsonKeyInput, jsonKey); await t.typeText(this.jsonValueInput, jsonKeyValue); @@ -715,7 +777,7 @@ export class BrowserPage { * @param jsonKey The json key name * @param jsonKeyValue The value of the json key */ - async addJsonKeyInsideStructure(jsonKey: string, jsonKeyValue: any): Promise { + async addJsonKeyInsideStructure(jsonKey: string, jsonKeyValue: string): Promise { await t.click(this.expandJsonObject); await t.click(this.addJsonFieldButton); await t.typeText(this.jsonKeyInput, jsonKey); @@ -858,6 +920,24 @@ export class BrowserPage { await t.click(this.formatSwitcher); await t.click(option); } + + /** + * Verify that keys can be scanned more and results increased + */ + async verifyScannningMore(): Promise { + for (let i = 10; i < 100; i += 10) { + // Remember results value + const rememberedScanResults = Number((await this.keysNumberOfResults.textContent).replace(/\s/g, '')); + await t.expect(this.progressKeyList.exists).notOk('Progress Bar is still displayed', { timeout: 30000 }); + const scannedValueText = await this.scannedValue.textContent; + const regExp = new RegExp(`${i} 00` + '.'); + await t.expect(scannedValueText).match(regExp, `The database is not automatically scanned by ${i} 000 keys`); + await t.doubleClick(this.scanMoreButton); + await t.expect(this.progressKeyList.exists).ok('Progress Bar is not displayed', { timeout: 30000 }); + const scannedResults = Number((await this.keysNumberOfResults.textContent).replace(/\s/g, '')); + await t.expect(scannedResults).gt(rememberedScanResults, { timeout: 3000 }); + } + } } /** diff --git a/tests/e2e/pageObjects/cli-page.ts b/tests/e2e/pageObjects/cli-page.ts index 86c8190993..620950c09d 100644 --- a/tests/e2e/pageObjects/cli-page.ts +++ b/tests/e2e/pageObjects/cli-page.ts @@ -27,6 +27,7 @@ export class CliPage { commandHelperBadge = Selector('[data-testid=expand-command-helper] span'); cliResizeButton = Selector('[data-test-subj=resize-btn-browser-cli]'); workbenchLink = Selector('[data-test-subj=cli-workbench-page-btn]'); + returnToList = Selector('[data-testid=cli-helper-back-to-list-btn]'); //TEXT INPUTS (also referred to as 'Text fields') cliCommandInput = Selector('[data-testid=cli-command]'); cliArea = Selector('[data-testid=cli'); @@ -58,7 +59,7 @@ export class CliPage { * Select filter group type * @param groupName The group name */ - async selectFilterGroupType(groupName: string): Promise{ + async selectFilterGroupType(groupName: string): Promise { await t.click(this.filterGroupTypeButton); await t.click(this.filterOptionGroupType.withExactText(groupName)); } @@ -69,7 +70,7 @@ export class CliPage { * @param amount The amount of the keys * @param keyName The name of the keys. The default value is keyName */ - async addKeysFromCli(keyCommand: string, amount: number, keyName = 'keyName'): Promise{ + async addKeysFromCli(keyCommand: string, amount: number, keyName = 'keyName'): Promise { //Open CLI await t.click(this.cliExpandButton); //Add keys @@ -83,7 +84,7 @@ export class CliPage { * Send command in Cli * @param command The command to send */ - async sendCommandInCli(command: string): Promise{ + async sendCommandInCli(command: string): Promise { //Open CLI await t.click(this.cliExpandButton); await t.typeText(this.cliCommandInput, command, { paste: true }); @@ -95,7 +96,7 @@ export class CliPage { * Get command result execution * @param command The command for send in CLI */ - async getSuccessCommandResultFromCli(command: string): Promise{ + async getSuccessCommandResultFromCli(command: string): Promise { //Open CLI await t.click(this.cliExpandButton); //Add keys @@ -127,18 +128,4 @@ export class CliPage { await t.click(this.readMoreButton); await t.expect(getPageUrl()).eql(url, 'The opened page'); } - - /** - * Check URL of command opened from command helper - * @param searchedCommand Searched command in Command Helper - * @param listToCompare The list with commands to compare with opened in Command Helper - */ - async checkSearchedCommandInCommandHelper(searchedCommand: string, listToCompare: string[]): Promise { - await t.typeText(this.cliHelperSearch, searchedCommand, { speed: 0.5 }); - //Verify results in the output - const commandsCount = await this.cliHelperOutputTitles.count; - for (let i = 0; i < commandsCount; i++) { - await t.expect(this.cliHelperOutputTitles.nth(i).textContent).eql(listToCompare[i], 'Results in the output contains searched value'); - } - } } diff --git a/tests/e2e/pageObjects/database-overview-page.ts b/tests/e2e/pageObjects/database-overview-page.ts index 8d46fae67a..fd4f5b57b4 100644 --- a/tests/e2e/pageObjects/database-overview-page.ts +++ b/tests/e2e/pageObjects/database-overview-page.ts @@ -1,7 +1,4 @@ -import { Selector, t } from 'testcafe'; -import { BrowserPage } from '../pageObjects'; - -const browserPage = new BrowserPage(); +import { Selector } from 'testcafe'; export class DatabaseOverviewPage { //------------------------------------------------------------------------------------------- diff --git a/tests/e2e/pageObjects/index.ts b/tests/e2e/pageObjects/index.ts index e18a654206..45e05c7360 100644 --- a/tests/e2e/pageObjects/index.ts +++ b/tests/e2e/pageObjects/index.ts @@ -11,6 +11,7 @@ import { DatabaseOverviewPage } from './database-overview-page'; import { HelpCenterPage } from './help-center-page'; import { ShortcutsPage } from './shortcuts-page'; import { MonitorPage } from './monitor-page'; +import { OverviewPage } from './overview-page'; import { PubSubPage } from './pub-sub-page'; import { SlowLogPage } from './slow-log-page'; import { NotificationPage } from './notification-page'; @@ -29,6 +30,7 @@ export { HelpCenterPage, ShortcutsPage, MonitorPage, + OverviewPage, PubSubPage, SlowLogPage, NotificationPage diff --git a/tests/e2e/pageObjects/monitor-page.ts b/tests/e2e/pageObjects/monitor-page.ts index a0de888b9f..97c8a83fa8 100644 --- a/tests/e2e/pageObjects/monitor-page.ts +++ b/tests/e2e/pageObjects/monitor-page.ts @@ -24,7 +24,7 @@ export class MonitorPage { monitorArea = Selector('[data-testid=monitor]'); monitorWarningMessage = Selector('[data-testid=monitor-warning-message]'); monitorCommandLinePart = Selector('[data-testid=monitor] span'); - monitorCommandLineTimestamp = Selector('[data-testid=monitor] span').withText(/[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3}/); + monitorCommandLineTimestamp = Selector('[data-testid=monitor] span').withText(/[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}/); monitorNoPermissionsMessage = Selector('[data-testid=monitor-error-message]'); saveLogToolTip = Selector('[data-testid=save-log-tooltip]'); monitorNotStartedElement = Selector('[data-testid=monitor-not-started]'); diff --git a/tests/e2e/pageObjects/my-redis-databases-page.ts b/tests/e2e/pageObjects/my-redis-databases-page.ts index b67883a388..9b9601d08b 100644 --- a/tests/e2e/pageObjects/my-redis-databases-page.ts +++ b/tests/e2e/pageObjects/my-redis-databases-page.ts @@ -10,6 +10,7 @@ export class MyRedisDatabasePage { //BUTTONS settingsButton = Selector('[data-testid=settings-page-btn]'); workbenchButton = Selector('[data-testid=workbench-page-btn]'); + analysisPageButton = Selector('[data-testid=analytics-page-btn]'); helpCenterButton = Selector('[data-testid=help-menu-button]'); githubButton = Selector('[data-testid=github-repo-icon]'); browserButton = Selector('[data-testid=browser-page-btn]'); @@ -48,7 +49,7 @@ export class MyRedisDatabasePage { //TEXT ELEMENTS moduleTooltip = Selector('.euiToolTipPopover'); moduleQuantifier = Selector('[data-testid=_module]'); - dbNameList = Selector('[data-testid^=instance-name]'); + dbNameList = Selector('[data-testid^=instance-name]', { timeout: 3000 }); tableRowContent = Selector('[data-test-subj=database-alias-column]'); databaseInfoMessage = Selector('[data-test-subj=euiToastHeader]'); hostPort = Selector('[data-testid=host-port]'); @@ -64,7 +65,7 @@ export class MyRedisDatabasePage { await t.click(this.toastCloseButton); } const db = this.dbNameList.withExactText(dbName.trim()); - await t.expect(db.exists).ok('The database exists', {timeout: 10000}); + await t.expect(db.exists).ok(`"${dbName}" database doesn't exist`, {timeout: 10000}); await t.click(db); } @@ -109,9 +110,8 @@ export class MyRedisDatabasePage { * @param databaseName The name of the database to be edited */ async clickOnEditDBByName(databaseName: string): Promise { - const dbNames = this.tableRowContent; + const dbNames = this.dbNameList; const count = await dbNames.count; - for (let i = 0; i < count; i++) { if ((await dbNames.nth(i).innerText || '').includes(databaseName)) { await t.click(this.editDatabaseButton.nth(i)); diff --git a/tests/e2e/pageObjects/notification-page.ts b/tests/e2e/pageObjects/notification-page.ts index cd5c56afca..c8c7dfae2a 100644 --- a/tests/e2e/pageObjects/notification-page.ts +++ b/tests/e2e/pageObjects/notification-page.ts @@ -30,6 +30,7 @@ export class NotificationPage { notificationBody = Selector(this.cssNotificationBody); notificationDate = Selector(this.cssNotificationDate); notificationList = Selector(this.cssNotificationList); + notificationCategory = Selector('[data-testid=notification-category]'); //ICONS notificationBadge = Selector('[data-testid=total-unread-badge]', { timeout: 35000 }); @@ -81,5 +82,8 @@ export type NotificationParameters = { timestamp: number, body: string, type?: string, - isRead?: boolean + isRead?: boolean, + category?: string, + colorCategory?: string, + rbgColor?: string }; diff --git a/tests/e2e/pageObjects/overview-page.ts b/tests/e2e/pageObjects/overview-page.ts new file mode 100644 index 0000000000..25e1a4d2d6 --- /dev/null +++ b/tests/e2e/pageObjects/overview-page.ts @@ -0,0 +1,66 @@ +import { Selector } from 'testcafe'; + +export class OverviewPage { + //CSS Selectors + cssTableRow = 'tr[class=euiTableRow]'; + //------------------------------------------------------------------------------------------- + //DECLARATION OF SELECTORS + //*Declare all elements/components of the relevant page. + //*Target any element/component via data-id, if possible! + //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). + //------------------------------------------------------------------------------------------- + //BUTTONS + overviewTab = Selector('[data-testid=analytics-tab-ClusterDetails]'); + // COMPONENTS + clusterDetailsUptime = Selector('[data-testid=cluster-details-uptime]'); + //TABLE COMPONENTS + tableHeaderCell = Selector('[data-test-subj^=tableHeaderCell]'); + primaryNodesTable = Selector('[data-testid=primary-nodes-table]'); + tableRow = Selector('tr[class=euiTableRow]'); + connectedClientsValue = Selector('[data-testid^=connectedClients-value]'); + totalKeysValue = Selector('[data-testid^=totalKeys-value]'); + networkInputValue = Selector('[data-testid^=networkInKbps-value]'); + networkOutputValue = Selector('[data-testid^=networkOutKbps-value]'); + + /** + * Get Primary nodes count in table + */ + async getPrimaryNodesCount(): Promise { + return await this.primaryNodesTable.find(this.cssTableRow).count; + } + + /** + * Get total value from all rows in column + * @param column The column name + */ + async getTotalValueByColumnName(column: string): Promise { + let totalNumber = 0; + let columnInSelector = ''; + switch (column) { + case 'Commands/s': + columnInSelector = 'opsPerSecond'; + break; + case 'Clients': + columnInSelector = 'connectedClients'; + break; + case 'Total Keys': + columnInSelector = 'totalKeys'; + break; + case 'Network Input': + columnInSelector = 'networkInKbps'; + break; + case 'Network Output': + columnInSelector = 'networkOutKbps'; + break; + case 'Total Memory': + columnInSelector = 'usedMemory'; + break; + default: columnInSelector = ''; + } + const rowSelector = Selector(`[data-testid^=${columnInSelector}-value]`); + for (let i = 0; i < await rowSelector.count; i++) { + totalNumber += Number(await rowSelector.nth(i).textContent); + } + return totalNumber; + } +} diff --git a/tests/e2e/pageObjects/settings-page.ts b/tests/e2e/pageObjects/settings-page.ts index 8af4c36f25..8392d92122 100644 --- a/tests/e2e/pageObjects/settings-page.ts +++ b/tests/e2e/pageObjects/settings-page.ts @@ -12,10 +12,12 @@ export class SettingsPage { accordionAppearance = Selector('[data-test-subj=accordion-appearance]'); accordionPrivacySettings = Selector('[data-test-subj=accordion-privacy-settings]'); accordionAdvancedSettings = Selector('[data-test-subj=accordion-advanced-settings]'); + accordionWorkbenchSettings = Selector('[data-test-subj=accordion-workbench-settings]'); switchAnalyticsOption = Selector('[data-testid=switch-option-analytics]'); switchEulaOption = Selector('[data-testid=switch-option-eula]'); submitConsentsPopupButton = Selector('[data-testid=consents-settings-popup] [data-testid=btn-submit]'); switchNotificationsOption = Selector('[data-testid=switch-option-notifications]'); + switchEditorCleanupOption = Selector('[data-testid=switch-workbench-cleanup]'); //TEXT INPUTS (also referred to as 'Text fields') keysToScanValue = Selector('[data-testid=keys-to-scan-value]'); keysToScanInput = Selector('[data-testid=keys-to-scan-input]'); @@ -65,4 +67,22 @@ export class SettingsPage { async getEulaSwitcherValue(): Promise { return await this.switchEulaOption.getAttribute('aria-checked'); } + + /** + * Get state of Editor Cleanup switcher + */ + async getEditorCleanupSwitcherValue(): Promise { + return await this.switchEditorCleanupOption.getAttribute('aria-checked'); + } + + /** + * Enable Editor Cleanup switcher + * @param state Enabled(true) or disabled(false) + */ + async changeEditorCleanupSwitcher(state: boolean): Promise { + const currentState = await this.getEditorCleanupSwitcherValue(); + if (currentState !== `${state}`) { + await t.click(this.switchEditorCleanupOption); + } + } } diff --git a/tests/e2e/pageObjects/slow-log-page.ts b/tests/e2e/pageObjects/slow-log-page.ts index cea32a9604..68265537e9 100644 --- a/tests/e2e/pageObjects/slow-log-page.ts +++ b/tests/e2e/pageObjects/slow-log-page.ts @@ -10,7 +10,6 @@ export class SlowLogPage { //CSS Selectors cssSelectorDurationValue = '[data-testid=duration-value]'; //BUTTONS - slowLogPageButton = Selector('[data-testid=slowlog-page-btn]'); slowLogSortByTimestamp = Selector('[data-testid=header-sorting-button]'); slowLogNumberOfCommandsDropdown = Selector('[data-testid=count-select]'); slowLogConfigureButton = Selector('[data-testid=configure-btn]'); @@ -32,6 +31,7 @@ export class SlowLogPage { slowLogCommandValue = Selector('[data-testid=command-value]'); slowLogEmptyResult = Selector('[data-testid=empty-slow-log]'); slowLogCommandStatistics = Selector('[data-testid=entries-from-timestamp]'); + configInfo = Selector('[data-testid=config-info]'); // Table slowLogTable = Selector('[data-testid=slowlog-table]'); diff --git a/tests/e2e/pageObjects/user-agreement-page.ts b/tests/e2e/pageObjects/user-agreement-page.ts index d9cbdede9e..02d3140d9b 100644 --- a/tests/e2e/pageObjects/user-agreement-page.ts +++ b/tests/e2e/pageObjects/user-agreement-page.ts @@ -22,6 +22,7 @@ export class UserAgreementPage { await t.click(this.recommendedSwitcher); await t.click(this.switchOptionEula); await t.click(this.submitButton); + await t.expect(this.userAgreementsPopup.visible).notOk('The user agreements popup is not shown', { timeout: 2000 }); } } diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index ec0268fe04..fa93d89114 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -11,7 +11,11 @@ export class WorkbenchPage { cssTableViewTypeOption = '[data-testid=view-type-selected-Plugin-redisearch__redisearch]'; cssMonacoCommandPaletteLine = '[aria-label="Command Palette"]'; cssQueryTextResult = '[data-testid=query-cli-result]'; + cssWorkbenchCommandInHistory = '[data-testid=wb-command]'; + cssWorkbenchCommandSuccessResultInHistory = '[data-testid=cli-output-response-success]'; + cssWorkbenchCommandFailedResultInHistory = '[data-testid=data-testid="cli-output-response-fail"]'; cssQueryTableResult = '[data-testid^=query-table-result-]'; + cssQueryPluginResult = '[data-testid^=query-table-result-]'; queryGraphContainer = '[data-testid=query-graph-container]'; cssQueryCardCommand = '[data-testid=query-card-command]'; cssQueryCardCommandResult = '[data-testid=query-common-result]'; @@ -55,6 +59,9 @@ export class WorkbenchPage { preselectModelBikeSalesButton = Selector('[data-testid="preselect-Model bike sales"]'); showSalesPerRegiomButton = Selector('[data-testid="preselect-Show all sales per region"]'); queryCardNoModuleButton = Selector('[data-testid=query-card-no-module-button] a'); + rawModeBtn = Selector('[data-testid="btn-change-mode"]'); + groupMode = Selector('[data-testid=btn-change-group-mode]'); + copyCommand = Selector('[data-testid=copy-command]'); //ICONS noCommandHistoryIcon = Selector('[data-testid=wb_no-results__icon]'); //LINKS @@ -95,6 +102,9 @@ export class WorkbenchPage { runButtonToolTip = Selector('[data-testid=run-query-tooltip]'); loadedCommand = Selector('[class=euiLoadingContent__singleLine]'); runButtonSpinner = Selector('[data-testid=loading-spinner]'); + workbenchCommandInHistory = Selector(this.cssWorkbenchCommandInHistory); + workbenchCommandSuccessResultInHistory = Selector(this.cssWorkbenchCommandSuccessResultInHistory); + workbenchCommandFailedResultInHistory = Selector(this.cssWorkbenchCommandFailedResultInHistory); //MONACO ELEMENTS monacoCommandDetails = Selector('div.suggest-details-container'); monacoCloseCommandDetails = Selector('span.codicon-close'); @@ -130,7 +140,7 @@ export class WorkbenchPage { //Select Table view option in Workbench results async selectViewTypeTable(): Promise { await t.click(this.selectViewType); - await t.click(this.tableViewTypeOption); + await t.doubleClick(this.tableViewTypeOption); } //Select view option in Workbench results @@ -161,16 +171,17 @@ export class WorkbenchPage { } /** - * Send commands array in Workbench page - * @param command The array of commands to send - * @param result The array of commands to send + * Check the last command and result in workbench + * @param command The command to check + * @param result The result to check + * @param childNum Indicator which command result need to check */ - async checkWorkbenchCommandResult(command: string, result: string): Promise { + async checkWorkbenchCommandResult(command: string, result: string, childNum = 0): Promise { //Compare the command with executed command - const actualCommand = await this.queryCardContainer.nth(0).find(this.cssQueryCardCommand).textContent; + const actualCommand = await this.queryCardContainer.nth(childNum).find(this.cssQueryCardCommand).textContent; await t.expect(actualCommand).eql(command); //Compare the command result with executed command - const actualCommandResult = await this.queryCardContainer.nth(0).find(this.cssQueryCardCommandResult).textContent; + const actualCommandResult = await this.queryCardContainer.nth(childNum).find(this.cssQueryTextResult).textContent; await t.expect(actualCommandResult).eql(result); } } diff --git a/tests/e2e/test-data/formatters-data.ts b/tests/e2e/test-data/formatters-data.ts new file mode 100644 index 0000000000..567f0154d8 --- /dev/null +++ b/tests/e2e/test-data/formatters-data.ts @@ -0,0 +1,76 @@ +/** + * Formatters objects with test data for format convertion + */ +export const formatters = [{ + format: 'JSON', + fromText: '{ "field": "value" }', + fromTextEdit: '{ "field": "value123" }' +}, +{ + format: 'Msgpack', + fromHexText: 'DF00000001A56669656C64A576616C7565', + fromText: '{ "field": "value" }', + fromTextEdit: '{ "field": "value123" }', + formattedText: '{ "field": "value" }' +}, +{ + format: 'Protobuf', + fromHexText: '08d90f10d802', + formattedText: '[ { "1": 2009 }, { "2": 344 } ]' +}, +{ + format: 'PHP serialized', + fromText: 'a:2:{i:0;s:12:"Sample array";i:1;a:2:{i:0;s:5:"Apple";i:1;s:6:"Orange";}}', + fromTextEdit: '[ "Sample array", [ "Apple", "Orange15" ] ]', + formattedText: '[ "Sample array", [ "Apple", "Orange" ] ]' +}, +{ + format: 'Java serialized', + fromHexText: 'aced000573720008456d706c6f796565025e743467c6123c0200034900066e756d6265724c0007616464726573737400124c6a6176612f6c616e672f537472696e673b4c00046e616d6571007e000178700000006574001950686f6b6b61204b75616e2c20416d62656874612050656572740009526579616e20416c69', + formattedText: '{ "fields": [ { "number": 101 }, { "address": "Phokka Kuan, Ambehta Peer" }, { "name": "Reyan Ali" } ], "annotations": [], "className": "Employee", "serialVersionUid": "170701604314812988" }' +}, +{ + format: 'ASCII', + fromText: '山女子水 рус ascii', + fromTextEdit: '山女子水 рус ascii 山女子', + formattedText: '\\xe5\\xb1\\xb1\\xe5\\xa5\\xb3\\xe5\\xad\\x90\\xe6\\xb0\\xb4 \\xd1\\x80\\xd1\\x83\\xd1\\x81 ascii', + formattedTextEdit: '\\xe5\\xb1\\xb1\\xe5\\xa5\\xb3\\xe5\\xad\\x90\\xe6\\xb0\\xb4 \\xd1\\x80\\xd1\\x83\\xd1\\x81 ascii \\xe5\\xb1\\xb1\\xe5\\xa5\\xb3\\xe5\\xad\\x90' +}, +{ + format: 'HEX', + fromText: '山女子水 рус hex', + fromTextEdit: '山女子水 рус hex 山女子', + formattedText: 'e5b1b1e5a5b3e5ad90e6b0b420d180d183d18120686578', + formattedTextEdit: 'e5b1b1e5a5b3e5ad90e6b0b420d180d183d1812068657820e5b1b1e5a5b3e5ad90' +}, +{ + format: 'Binary', + fromText: '水 рус bin', + fromTextEdit: '水山 рус bin 子', + formattedText: '1110011010110000101101000010000011010001100000001101000110000011110100011000000100100000011000100110100101101110', + formattedTextEdit: '111001101011000010110100111001011011000110110001001000001101000110000000110100011000001111010001100000010010000001100010011010010110111000100000111001011010110110010000' +}, +{ + format: 'Pickle', + fromHexText: '286470300a5327617272270a70310a286c70320a49310a6149320a617353276f626a270a70330a286470340a532761270a70350a532762270a70360a7373532748656c6c6f270a70370a5327776f726c64270a70380a732e', + formattedText: '{ "arr": [ 1, 2 ], "obj": { "a": "b" }, "Hello": "world" }' +}]; + +/** + * PHP data for convertion including different php serialized data types + */ +export const phpData = [{ + dataType: 'Object', + from: 'a:6:{i:1;s:30:"PHP code tester Sandbox Online";s:5:"emoji";s:24:"😀 😃 😄 😁 😆";i:2;i:5;i:5;i:89009;s:13:"Random number";i:341;s:11:"PHP Version";s:5:"8.1.9";}', + converted: '{ "1": "PHP code tester Sandbox Online", "2": 5, "5": 89009, "emoji": "😀 😃 😄 😁 😆", "Random number": 341, "PHP Version": "8.1.9" }' +}, +{ + dataType: 'Number', + from: 'i:34567234;', + converted: '34567234' +}, +{ + dataType: 'String', + from: 's:72:"Dumbledore took Harry in his arms and turned toward the Dursleys\' house.";', + converted: '"Dumbledore took Harry in his arms and turned toward the Dursleys\' house."' +}]; diff --git a/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts b/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts index 4bfd32be20..b587e563da 100644 --- a/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts @@ -38,14 +38,21 @@ test('Verify that user can access the bulk actions screen in the Browser', async // Open bulk actions await t.click(browserPage.bulkActionsButton); await t.expect(bulkActionsPage.bulkActionsContainer.visible).ok('Bulk actions screen not opened'); -}); -test('Verify that user can see pattern summary of the keys selected: key type, pattern', async t => { - // Filter by Hash keys - await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); - // Open bulk actions - await t.click(browserPage.bulkActionsButton); + // Verify that user can see pattern summary of the keys selected: key type, pattern await t.expect(bulkActionsPage.infoFilter.innerText).contains('Key type:\nHASH', 'Key type is not correct'); await t.expect(bulkActionsPage.infoSearch.innerText).contains('Pattern: *', 'Key pattern is not correct'); + // Verify that user can hover over info icon in Bulk Delete preview and see info about accuracy of the calculation + const tooltipText = 'Expected amount is estimated based on the number of keys scanned and the scan percentage. The final number may be different.'; + await t.hover(bulkActionsPage.bulkDeleteTooltipIcon); + await t.expect(browserPage.tooltip.textContent).eql(tooltipText, 'Tooltip is not displayed or text is invalid'); + // Verify that user can see warning message clicking on Delete button for Bulk Deletion + const warningTooltipTitle = 'Are you sure you want to perform this action?'; + const warningTooltipMessage = 'All keys with HASH key type and selected pattern will be deleted.'; + await t.click(bulkActionsPage.deleteButton); + await t.expect(bulkActionsPage.bulkActionWarningTooltip.textContent).contains(warningTooltipTitle, 'Warning Tooltip title is not displayed or text is invalid'); + await t.expect(bulkActionsPage.bulkActionWarningTooltip.textContent).contains(warningTooltipMessage, 'Warning Tooltip message is not displayed or text is invalid'); + await t.expect(bulkActionsPage.bulkApplyButton.visible).ok('Confirm deletion button not displayed'); + }); test('Verify that user can see no pattern selected message when no key type and pattern applied for Bulk Delete', async t => { const messageTitle = 'No pattern or key type set'; @@ -56,8 +63,8 @@ test('Verify that user can see no pattern selected message when no key type and await t.expect(bulkActionsPage.bulkActionsPlaceholder.textContent).contains(messageText, 'No pattern message not displayed'); }); test('Verify that user can see summary of scanned level', async t => { - const expectedAmount = 'Expected amount: ~10 002 keys'; - const scannedKeys = 'Scanned 5% (500/10 002) and found 500 keys'; + const expectedAmount = new RegExp('Expected amount: ~' + '10 ' + '.'); + const scannedKeys = new RegExp('Scanned 5% \\(....10 ...\\) and found ... keys'); // Add 10000 Hash keys await populateDBWithHashes(dbParameters.host, dbParameters.port, keyToAddParameters); // Filter by Hash keys @@ -65,30 +72,9 @@ test('Verify that user can see summary of scanned level', async t => { // Open bulk actions await t.click(browserPage.bulkActionsButton); // Verify that prediction of # of keys matching the filter in the entire database displayed - await t.expect(bulkActionsPage.bulkActionsSummary.textContent).contains(expectedAmount, 'Bulk actions summary is not correct'); + await t.expect(bulkActionsPage.bulkActionsSummary.textContent).match(expectedAmount, 'Bulk actions summary is not correct'); // Verify that % of total keys scanned, # of keys scanned / total # of keys in the database, # of keys matching the filter displayed - await t.expect(bulkActionsPage.bulkDeleteSummary.innerText).contains(scannedKeys, 'Bulk delete summary is not correct'); -}); -test('Verify that user can hover over info icon in Bulk Delete preview and see info about accuracy of the calculation', async t => { - const tooltipText = 'Expected amount is estimated based on the number of keys scanned and the scan percentage. The final number may be different.'; - // Filter by Hash keys - await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); - // Open bulk actions - await t.click(browserPage.bulkActionsButton); - await t.hover(bulkActionsPage.bulkDeleteTooltipIcon); - await t.expect(browserPage.tooltip.textContent).eql(tooltipText, 'Tooltip is not displayed or text is invalid'); -}); -test('Verify that user can see warning message clicking on Delete button for Bulk Deletion', async t => { - const warningTooltipTitle = 'Are you sure you want to perform this action?'; - const warningTooltipMessage = 'All keys with HASH key type and selected pattern will be deleted.'; - // Filter by Hash keys - await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); - // Open bulk actions - await t.click(browserPage.bulkActionsButton); - await t.click(bulkActionsPage.deleteButton); - await t.expect(bulkActionsPage.bulkActionWarningTooltip.textContent).contains(warningTooltipTitle, 'Warning Tooltip title is not displayed or text is invalid'); - await t.expect(bulkActionsPage.bulkActionWarningTooltip.textContent).contains(warningTooltipMessage, 'Warning Tooltip message is not displayed or text is invalid'); - await t.expect(bulkActionsPage.bulkApplyButton.visible).ok('Confirm deletion button not displayed'); + await t.expect(bulkActionsPage.bulkDeleteSummary.innerText).match(scannedKeys, 'Bulk delete summary is not correct'); }); test('Verify that user can see blue progress line during the process of bulk deletion', async t => { // Add 500000 Hash keys diff --git a/tests/e2e/tests/critical-path/browser/database-overview.e2e.ts b/tests/e2e/tests/critical-path/browser/database-overview.e2e.ts index f006f6cf4f..177add8479 100644 --- a/tests/e2e/tests/critical-path/browser/database-overview.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/database-overview.e2e.ts @@ -34,7 +34,7 @@ let keys2: string[]; fixture `Database overview` .meta({type: 'critical_path'}) .page(commonUrl) - .beforeEach(async t => { + .beforeEach(async() => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); }) .afterEach(async() => { @@ -48,8 +48,8 @@ test await deleteStandaloneDatabaseApi(ossStandaloneConfig); await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); })('Verify that user can see the list of Modules updated each time when he connects to the database', async t => { - const firstDatabaseModules = []; - const secondDatabaseModules = []; + const firstDatabaseModules: string[] = []; + const secondDatabaseModules: string[] = []; //Remember modules let countOfModules = await browserPage.modulesButton.count; for(let i = 0; i < countOfModules; i++) { @@ -67,7 +67,7 @@ test //Add database with different modules await t.click(myRedisDatabasePage.myRedisDBButton); await addNewStandaloneDatabaseApi(ossStandaloneRedisearch); - await t.eval(() => location.reload()); + await common.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneRedisearch.databaseName); countOfModules = await browserPage.modulesButton.count; for(let i = 0; i < countOfModules; i++) { @@ -123,7 +123,7 @@ test //Add database with more than 1M keys await t.click(myRedisDatabasePage.myRedisDBButton); await addNewStandaloneDatabaseApi(ossStandaloneBigConfig); - await t.eval(() => location.reload()); + await common.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneBigConfig.databaseName); //Wait 5 seconds await t.wait(fiveSecondsTimeout); diff --git a/tests/e2e/tests/critical-path/browser/filtering.e2e.ts b/tests/e2e/tests/critical-path/browser/filtering.e2e.ts index 5f4a52f20b..0bc7dd27d3 100644 --- a/tests/e2e/tests/critical-path/browser/filtering.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/filtering.e2e.ts @@ -1,3 +1,4 @@ +import { Chance } from 'chance'; import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { @@ -7,7 +8,6 @@ import { } from '../../../helpers/conf'; import { keyLength, KeyTypesTexts, rte } from '../../../helpers/constants'; import { addKeysViaCli, deleteKeysViaCli, keyTypes } from '../../../helpers/keys'; -import { Chance } from 'chance'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; @@ -22,76 +22,86 @@ keysData.forEach(key => key.keyName = `${key.keyName}` + '-' + `${common.generat fixture `Filtering per key name in Browser page` .meta({type: 'critical_path', rte: rte.standalone}) .page(commonUrl) - .beforeEach(async () => { + .beforeEach(async() => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); }) - .afterEach(async () => { + .afterEach(async() => { //Delete database await deleteStandaloneDatabaseApi(ossStandaloneConfig); - }) + }); test - .after(async () => { + .after(async() => { //Clear and delete database await browserPage.deleteKeyByName(keyName); await deleteStandaloneDatabaseApi(ossStandaloneConfig); - }) - ('Verify that user can search a key with selected data type is filters', async t => { + })('Verify that user can search a key with selected data type is filters', async t => { keyName = chance.word({ length: 10 }); //Add new key await browserPage.addStringKey(keyName); //Search by key with full name & specified type - await browserPage.selectFilterGroupType(KeyTypesTexts.String) + await browserPage.selectFilterGroupType(KeyTypesTexts.String); await browserPage.searchByKeyName(keyName); //Verify that key was found const isKeyIsDisplayedInTheList = await browserPage.isKeyIsDisplayedInTheList(keyName); await t.expect(isKeyIsDisplayedInTheList).ok('The key was found'); }); test - .after(async () => { + .after(async() => { // Clear keys and database await deleteKeysViaCli(keysData); await deleteStandaloneDatabaseApi(ossStandaloneConfig); - }) - ('Verify that user can filter keys per data type in Browser page', async t => { + })('Verify that user can filter keys per data type in Browser page', async t => { keyName = chance.word({ length: 10 }); //Create new keys await addKeysViaCli(keysData); for (const { textType, keyName } of keysData) { await browserPage.selectFilterGroupType(textType); await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok(`The key of type ${textType} was found`); + const regExp = new RegExp('[1-9]'); + await t.expect(browserPage.keysNumberOfResults.textContent).match(regExp, 'Number of found keys'); } }); test - .before(async () => { + .before(async() => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); }) - .after(async () => { + .after(async() => { //Delete database await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); - }) - ('Verify that user see the key type label when filtering per key types and when removes lable the filter is removed on Browser page', async t => { //Check filtering labes + })('Verify that user see the key type label when filtering per key types and when removes label the filter is removed on Browser page', async t => { //Check filtering labels for (const { textType } of keyTypes) { await browserPage.selectFilterGroupType(textType); //Check key type label - await t.expect((await browserPage.filteringLabel.textContent).toUpperCase).eql(textType.toUpperCase, `The label of type ${textType} is dispalyed`); - await t.expect(browserPage.keysNumberOfResults.visible).ok(`The filter ${textType} is applied`); + await t.expect((await browserPage.filteringLabel.textContent).toUpperCase).eql(textType.toUpperCase, `The label of type ${textType} is displayed`); + if (['STREAM', 'GRAPH', 'TS'].includes(textType)) { + await t.expect(browserPage.keysNumberOfResults.textContent).eql('0', 'Number of found keys'); + } + else { + const regExp = new RegExp('5..'); + await t.expect(browserPage.keysNumberOfResults.textContent).match(regExp, 'Number of found keys'); + } } - //Check removing of the label - await t.click(browserPage.deleteFilterButton); - await t.expect(browserPage.multiSearchArea.find(browserPage.cssFilteringLabel).visible).notOk(`The label of filtering type is removed`); - await t.expect(browserPage.keysSummary.textContent).contains('Total', `The filter is removed`); + //Check removing of the label + await t.click(browserPage.deleteFilterButton); + await t.expect(browserPage.multiSearchArea.find(browserPage.cssFilteringLabel).visible).notOk('The label of filtering type is removed'); + await t.expect(browserPage.keysSummary.textContent).contains('Total', 'The filter is removed'); }); test - ('Verify that user can see filtering per key name starts when he press Enter or clicks the control to filter per key name', async t => { //Check filtering labes + .after(async() => { + //Clear and delete database + await browserPage.deleteKeyByName(keyName); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('Verify that user can see filtering per key name starts when he press Enter or clicks the control to filter per key name', async t => { //Check filtering labes keyName = chance.word({ length: 10 }); - await t.expect(browserPage.keyListTable.textContent).contains('No keys to display.', 'The filtering is not set'); + //Add new key + await browserPage.addStringKey(keyName); //Check the filtering starts by press Enter - await t.typeText(browserPage.filterByPatterSearchInput, keyName); + await t.typeText(browserPage.filterByPatterSearchInput, 'InvalidText'); await t.pressKey('enter'); await t.expect(browserPage.searchAdvices.visible).ok('The filtering is set'); //Check the filtering starts by clicks the control - await t.eval(() => location.reload()); - await t.typeText(browserPage.filterByPatterSearchInput, keyName); + await common.reloadPage(); + await t.typeText(browserPage.filterByPatterSearchInput, 'InvalidText'); await t.click(browserPage.searchButton); await t.expect(browserPage.searchAdvices.visible).ok('The filtering is set'); }); diff --git a/tests/e2e/tests/critical-path/browser/formatters.e2e.ts b/tests/e2e/tests/critical-path/browser/formatters.e2e.ts index a5b0749e34..2c515d0021 100644 --- a/tests/e2e/tests/critical-path/browser/formatters.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/formatters.e2e.ts @@ -6,32 +6,24 @@ import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; +import { formatters, phpData } from '../../../test-data/formatters-data'; const browserPage = new BrowserPage(); const common = new Common(); const keysData = keyTypes.map(object => ({ ...object })).filter((v, i) => i <= 6 && i !== 5); keysData.forEach(key => key.keyName = `${key.keyName}` + '-' + `${common.generateWord(keyLength)}`); -const formatters = [{ - format: 'JSON', - fromText: '{ "field": "value" }' -}, { - format: 'Msgpack', - fromText: '{ "field": "value" }', - toText: 'DF00000001A56669656C64A576616C7565' -}, { - format: 'ASCII', - fromText: '山女子水 рус ascii', - toText: '\\xe5\\xb1\\xb1\\xe5\\xa5\\xb3\\xe5\\xad\\x90\\xe6\\xb0\\xb4 \\xd1\\x80\\xd1\\x83\\xd1\\x81 ascii' -}, { - format: 'HEX', - fromText: '山女子水 рус ascii', - toText: 'e5b1b1e5a5b3e5ad90e6b0b420d180d183d181206173636969' -}]; +const binaryFormattersSet = [formatters[5], formatters[6], formatters[7]]; +const formattersHighlightedSet = [formatters[0], formatters[3]]; +const fromBinaryFormattersSet = [formatters[1], formatters[2], formatters[4], formatters[8]]; +const formattersForEditSet = [formatters[0], formatters[1], formatters[3]]; +const formattersWithTooltipSet = [formatters[0], formatters[1], formatters[2], formatters[3], formatters[4], formatters[8]]; +const notEditableFormattersSet = [formatters[2], formatters[4], formatters[8]]; +const defaultFormatter = 'Unicode'; fixture `Formatters` .meta({ - type: 'regression', + type: 'critical_path', rte: rte.standalone }) .page(commonUrl) @@ -45,126 +37,169 @@ fixture `Formatters` await deleteKeysViaCli(keysData); await deleteStandaloneDatabaseApi(ossStandaloneConfig); }); -test - .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); - // Create new keys - await addKeysViaCli(keysData, formatters[0].fromText, formatters[0].fromText); - })('Verify that user can see highlighted key details in JSON format', async t => { - // Verify for Hash, List, Set, ZSet, String, Stream keys - for (let i = 0; i < keysData.length; i++) { - const valueSelector = Selector(`[data-testid^=${keysData[i].keyName.split('-')[0]}-][data-testid*=${keysData[i].data}]`); - await browserPage.openKeyDetailsByKeyName(keysData[i].keyName); - // Verify that json value not formatted with default formatter - await t.expect(valueSelector.find(browserPage.cssJsonValue).exists).notOk(`${keysData[i].textType} Value is formatted to JSON`); - await browserPage.selectFormatter('JSON'); - // Verify that json value is formatted and highlighted - await t.expect(valueSelector.find(browserPage.cssJsonValue).exists).ok(`${keysData[i].textType} Value is not formatted to JSON`); - // Verify that Hash field is formatted to json and highlighted - if (keysData[i].keyName === 'hash') { - await t.expect(browserPage.hashField.find(browserPage.cssJsonValue).exists).ok('Hash field is not formatted to JSON'); - } - // Verify that Stream field is formatted to json and highlighted - if (keysData[i].keyName === 'stream') { - await t.expect(Selector(browserPage.cssJsonValue).count).eql(2, 'Hash field is not formatted to JSON'); +formattersHighlightedSet.forEach(formatter => { + test + .before(async() => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + // Create new keys + await addKeysViaCli(keysData, formatter.fromText, formatter.fromText); + })(`Verify that user can see highlighted key details in ${formatter.format} format`, async t => { + // Verify for JSON and PHP serialized + // Verify for Hash, List, Set, ZSet, String, Stream keys + for (const key of keysData) { + const valueSelector = Selector(`[data-testid^=${key.keyName.split('-')[0]}-][data-testid*=${key.data}]`); + await browserPage.openKeyDetailsByKeyName(key.keyName); + // Verify that value not formatted with default formatter + await browserPage.selectFormatter(defaultFormatter); + await t.expect(valueSelector.find(browserPage.cssJsonValue).exists).notOk(`${key.textType} Value is formatted to ${formatter.format}`); + await browserPage.selectFormatter(formatter.format); + // Verify that value is formatted and highlighted + await t.expect(valueSelector.find(browserPage.cssJsonValue).exists).ok(`${key.textType} Value is not formatted to ${formatter.format}`); + // Verify that Hash field is formatted and highlighted for JSON and PHP serialized + if (key.keyName === 'hash') { + await t.expect(browserPage.hashField.find(browserPage.cssJsonValue).exists).ok(`Hash field is not formatted to ${formatter.format}`); + } + // Verify that Stream field is formatted and highlighted for JSON and PHP serialized + if (key.keyName === 'stream') { + await t.expect(Selector(browserPage.cssJsonValue).count).eql(2, `Hash field is not formatted to ${formatter.format}`); + } } - } + }); +}); +fromBinaryFormattersSet.forEach(formatter => { + test(`Verify that user can see highlighted key details in ${formatter.format} format`, async t => { + // Verify for Msgpack, Protobuf, Java serialized, Pickle formats + // Open Hash key details + await browserPage.openKeyDetailsByKeyName(keysData[0].keyName); + // Add valid value in HEX format for convertion + await browserPage.selectFormatter('HEX'); + await browserPage.editHashKeyValue(formatter.fromHexText ?? ''); + await browserPage.selectFormatter(formatter.format); + // Verify that value is formatted and highlighted + await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.formattedText ?? '', `Value is not saved as ${formatter.format}`); + await t.expect(browserPage.hashFieldValue.find(browserPage.cssJsonValue).exists).ok(`Value is not formatted to ${formatter.format}`); + + }); +}); +formattersForEditSet.forEach(formatter => { + test(`Verify that user can edit the values in the key regardless if they are valid in ${formatter.format} format or not`, async t => { + // Verify for JSON, Msgpack, PHP serialized formatters + const invalidText = 'invalid text'; + // Open key details and select formatter + await browserPage.openKeyDetails(keysData[0].keyName); + await browserPage.selectFormatter(formatter.format); + await browserPage.editHashKeyValue(invalidText); + await t.click(browserPage.saveButton); + // Verify that invalid value can be saved + await t.expect(browserPage.hashFieldValue.textContent).contains(invalidText, `Invalid ${formatter.format} value is not saved`); + // Add valid value which can be converted + await browserPage.editHashKeyValue(formatter.fromText ?? ''); + // Verify that valid value can be saved on edit + formatter.format === 'PHP serialized' + ? await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.formattedText ?? '', `Valid ${formatter.format} value is not saved`) + : await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.fromText ?? '', `Valid ${formatter.format} value is not saved`); + await t.expect(browserPage.hashFieldValue.find(browserPage.cssJsonValue).exists).ok(`Value is not formatted to ${formatter.format}`); + await browserPage.editHashKeyValue(formatter.fromTextEdit ?? ''); + // Verify that valid value can be edited to another valid value + await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.fromTextEdit ?? '', `Valid ${formatter.format} value is not saved`); + await t.expect(browserPage.hashFieldValue.find(browserPage.cssJsonValue).exists).ok(`Value is not formatted to ${formatter.format}`); }); -test('Verify that user can edit the values in the key regardless if they are valid in JSON/Msgpack format or not', async t => { - // Verify for JSON and Msgpack formatters - const invalidText = 'invalid text'; - for (const formatter of formatters) { - if (formatter.format === 'JSON' || formatter.format === 'Msgpack') { +}); +formattersWithTooltipSet.forEach(formatter => { + test(`Verify that user can see tooltip with convertion failed message on hover when data is not valid ${formatter.format}`, async t => { + // Verify for JSON, Msgpack, Protobuf, PHP serialized, Java serialized object, Pickle formatters + const failedMessage = `Failed to convert to ${formatter.format}`; + for (let i = 0; i < keysData.length; i++) { + const valueSelector = Selector(`[data-testid^=${keysData[i].keyName.split('-')[0]}-][data-testid*=${keysData[i].data}]`); // Open key details and select formatter - await browserPage.openKeyDetails(keysData[0].keyName); + await browserPage.openKeyDetailsByKeyName(keysData[i].keyName); await browserPage.selectFormatter(formatter.format); - await browserPage.editHashKeyValue(invalidText); - // Verify that invalid value can be saved - await t.expect(browserPage.hashFieldValue.textContent).contains(invalidText, `Invalid ${formatter.format} value is not saved`); - await browserPage.editHashKeyValue(formatter.fromText); - // Verify that valid value can be saved - await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.fromText, `Valid ${formatter.format} value is not saved`); - await t.expect(browserPage.hashFieldValue.find(browserPage.cssJsonValue).exists).ok(`Value is not formatted to ${formatter.format}`); + // Verify that not valid value is not formatted + await t.expect(valueSelector.find(browserPage.cssJsonValue).exists).notOk(`${keysData[i].textType} Value is formatted to ${formatter.format}`); + await t.hover(valueSelector, { offsetX: 5 }); + // Verify that tooltip with convertion failed message displayed + await t.expect(browserPage.tooltip.textContent).contains(failedMessage, `"${failedMessage}" is not displayed in tooltip`); } - } + }); }); -test('Verify that user can see tooltip with convertion failed message on hover when data is not valid JSON/Msgpack', async t => { - // Verify for JSON and Msgpack formatters - for (const formatter of formatters) { - if (formatter.format === 'JSON' || formatter.format === 'Msgpack') { - const failedMessage = `Failed to convert to ${formatter.format}`; +binaryFormattersSet.forEach(formatter => { + test + .before(async() => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + // Create new keys + await addKeysViaCli(keysData, formatter.fromText); + })(`Verify that user can see key details converted to ${formatter.format} format`, async t => { + // Verify for ASCII, HEX, Binary formatters + // Verify for Hash, List, Set, ZSet, String, Stream keys for (let i = 0; i < keysData.length; i++) { const valueSelector = Selector(`[data-testid^=${keysData[i].keyName.split('-')[0]}-][data-testid*=${keysData[i].data}]`); - // Open key details and select formatter await browserPage.openKeyDetailsByKeyName(keysData[i].keyName); + // Verify that value not formatted with default formatter + await browserPage.selectFormatter(defaultFormatter); + await t.expect(valueSelector.innerText).contains(formatter.fromText ?? '', `Value is formatted as ${formatter.format} in Unicode`); await browserPage.selectFormatter(formatter.format); - // Verify that not valid value is not formatted - await t.expect(valueSelector.find(browserPage.cssJsonValue).exists).notOk(`${keysData[i].textType} Value is formatted to ${formatter.format}`); - await t.hover(valueSelector, { offsetX: 5 }); - // Verify that tooltip with convertion failed message displayed - await t.expect(browserPage.tooltip.textContent).contains(failedMessage, `"${failedMessage}" is not displayed in tooltip`); + // Verify that value is formatted + await t.expect(valueSelector.innerText).contains(formatter.formattedText ?? '', `Value is not formatted to ${formatter.format}`); + // Verify that Hash field is formatted to ASCII/HEX/Binary + if (keysData[i].keyName === 'hash') { + await t.expect(browserPage.hashField.innerText).contains(formatter.formattedText ?? '', `Hash field is not formatted to ${formatter.format}`); + } } - } + }); + test(`Verify that user can edit value for Hash field in ${formatter.format} and convert then to another format`, async t => { + // Verify for ASCII, HEX, Binary formatters + // Open key details and select formatter + await browserPage.openKeyDetails(keysData[0].keyName); + await browserPage.selectFormatter(formatter.format); + // Add value in selected format + await browserPage.editHashKeyValue(formatter.formattedText ?? ''); + // Verify that value saved in selected format + await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.formattedText ?? '', `${formatter.format} value is not saved`); + await browserPage.selectFormatter('Unicode'); + // Verify that value converted to Unicode + await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.fromText ?? '', `${formatter.format} value is not converted to Unicode`); + await browserPage.selectFormatter(formatter.format); + await browserPage.editHashKeyValue(formatter.formattedTextEdit ?? ''); + // Verify that valid converted value can be edited to another + await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.formattedTextEdit ?? '', `${formatter.format} value is not saved`); + await browserPage.selectFormatter('Unicode'); + // Verify that value converted to Unicode + await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.fromTextEdit ?? '', `${formatter.format} value is not converted to Unicode`); + }); +}); +test('Verify that user can format different data types of PHP serialized', async t => { + // Open Hash key details + await browserPage.openKeyDetailsByKeyName(keysData[0].keyName); + for (const type of phpData) { + //Add fields to the hash key + await browserPage.selectFormatter('Unicode'); + await browserPage.addFieldToHash(type.dataType, type.from); + //Search the added field + await browserPage.searchByTheValueInKeyDetails(type.dataType); + await browserPage.selectFormatter('PHP serialized'); + // Verify that PHP serialized value is formatted and highlighted + await t.expect(browserPage.hashFieldValue.innerText).contains(type.converted, `Value is not saved as PHP ${type.dataType}`); + await t.expect(browserPage.hashFieldValue.find(browserPage.cssJsonValue).exists).ok(`Value is not formatted to PHP ${type.dataType}`); } }); -test - .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); - // Create new keys - await addKeysViaCli(keysData, formatters[1].fromText); - })('Verify that user can see highlighted key details in Msgpack format', async t => { - // Open HaSH key details - await browserPage.openKeyDetailsByKeyName(keysData[0].keyName); - // Verify that msgpack value not formatted with default formatter - await t.expect(browserPage.hashFieldValue.find(browserPage.cssJsonValue).exists).notOk('Value is formatted to Msgpack'); - // Add valid msgpack in HEX format - await browserPage.selectFormatter('HEX'); - await browserPage.editHashKeyValue(formatters[1].toText!); - await browserPage.selectFormatter('Msgpack'); - // Verify that msgpack value is formatted and highlighted - await t.expect(browserPage.hashFieldValue.innerText).contains(formatters[1].fromText!, 'Value is not saved as msgpack'); - await t.expect(browserPage.hashFieldValue.find(browserPage.cssJsonValue).exists).ok('Value is not formatted to Msgpack'); - }); -test - .before(async() => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); - // Create new keys - await addKeysViaCli(keysData, formatters[2].fromText); - })('Verify that user can see key details converted to ASCII/HEX format', async t => { - // Verify for ASCII and HEX formatters - for (const formatter of formatters) { - if (formatter.format === 'ASCII' || formatter.format === 'HEX') { - // Verify for Hash, List, Set, ZSet, String, Stream keys - for (let i = 0; i < keysData.length; i++) { - const valueSelector = Selector(`[data-testid^=${keysData[i].keyName.split('-')[0]}-][data-testid*=${keysData[i].data}]`); - await browserPage.openKeyDetailsByKeyName(keysData[i].keyName); - // Verify that value not formatted with default formatter - await t.expect(valueSelector.innerText).contains(formatter.fromText!, `Value is formatted as ${formatter.format} in Unicode`); - await browserPage.selectFormatter(formatter.format); - // Verify that value is formatted - await t.expect(valueSelector.innerText).contains(formatter.toText!, `Value is formatted to ${formatter.format}`); - // Verify that Hash field is formatted to ASCII/HEX - if (keysData[i].keyName === 'hash') { - await t.expect(browserPage.hashField.innerText).contains(formatter.toText!, `Hash field is not formatted to ${formatter.format}`); - } - } +notEditableFormattersSet.forEach(formatter => { + test(`Verify that user see edit icon disabled for all keys when ${formatter.format} selected`, async t => { + // Verify for Protobuf, Java serialized, Pickle + // Verify for Hash, List, ZSet, String keys + for (const key of keysData) { + if (key.keyName === 'hash' || key.keyName === 'list' || key.keyName === 'zset' || key.keyName === 'string') { + const editBtn = (key.keyName === 'string') + ? browserPage.editKeyValueButton + : Selector(`[data-testid^=edit-][data-testid*=${key.keyName.split('-')[0]}]`); + await browserPage.openKeyDetailsByKeyName(key.keyName); + await browserPage.selectFormatter(formatter.format); + // Verify that edit button disabled + await t.expect(editBtn.hasAttribute('disabled')).ok(`Key ${key.textType} is enabled for ${formatter.format} formatter`); + // Hover on disabled button + await t.hover(editBtn); + // Verify tooltip content + await t.expect(browserPage.tooltip.textContent).contains('Cannot edit the value in this format', 'Tooltip has wrong text'); } } }); -test('Verify that user can edit value for Hash field in ASCII/HEX and convert then to another format', async t => { - // Verify for ASCII and HEX formaterrs - for (const formatter of formatters) { - if (formatter.format === 'ASCII' || formatter.format === 'HEX') { - // Open key details and select formatter - await browserPage.openKeyDetails(keysData[0].keyName); - await browserPage.selectFormatter(formatter.format); - // Add value in selected format - await browserPage.editHashKeyValue(formatter.toText!); - // Verify that value saved in selected format - await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.toText!, `${formatter.format} value is not saved`); - await browserPage.selectFormatter('Unicode'); - // Verify that value converted to Unicode - await t.expect(browserPage.hashFieldValue.innerText).contains(formatter.fromText!, `${formatter.format} value is not converted to Unicode`); - } - } }); diff --git a/tests/e2e/tests/critical-path/browser/json-key.e2e.ts b/tests/e2e/tests/critical-path/browser/json-key.e2e.ts index 8886702970..f3becda23a 100644 --- a/tests/e2e/tests/critical-path/browser/json-key.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/json-key.e2e.ts @@ -23,8 +23,7 @@ fixture `JSON Key verification` await browserPage.deleteKeyByName(keyName); await deleteStandaloneDatabaseApi(ossStandaloneConfig); }) -//skipped due the issue https://redislabs.atlassian.net/browse/RI-2866 -test.skip +test .meta({ rte: rte.standalone }) ('Verify that user can not add invalid JSON structure inside of created JSON', async t => { keyName = chance.word({ length: 10 }); diff --git a/tests/e2e/tests/critical-path/browser/scan-keys.e2e.ts b/tests/e2e/tests/critical-path/browser/scan-keys.e2e.ts index 572df95d2c..2a63281546 100644 --- a/tests/e2e/tests/critical-path/browser/scan-keys.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/scan-keys.e2e.ts @@ -63,12 +63,12 @@ test await t.click(cliPage.cliCollapseButton); //Search keys await browserPage.searchByKeyName(searchPattern); - const keysNumberOfScanned = await browserPage.keysNumberOfScanned.textContent; + const keysNumberOfScanned = await browserPage.scannedValue.textContent; //Verify that number of scanned is 1000 await t.expect(keysNumberOfScanned).contains('1 000', 'Number of scanned is 1000'); //Scan more await t.click(browserPage.scanMoreButton); - const keysNumberOfScannedScanMore = await browserPage.keysNumberOfScanned.textContent; + const keysNumberOfScannedScanMore = await browserPage.scannedValue.textContent; //Verify that number of results is 2000 await t.expect(keysNumberOfScannedScanMore).contains('2 000', 'Number of scanned is 2000'); }); diff --git a/tests/e2e/tests/critical-path/browser/stream-key.e2e.ts b/tests/e2e/tests/critical-path/browser/stream-key.e2e.ts index b470e49610..f98b7b4399 100644 --- a/tests/e2e/tests/critical-path/browser/stream-key.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/stream-key.e2e.ts @@ -76,12 +76,12 @@ test('Verify that user can add new Stream Entry for Stream data type key which h // Create new field and value and check that new column is added await browserPage.addEntryToStream(newField, chance.word({ length: 20 })); await t.expect(browserPage.streamEntryIDDateValue.count).eql(2, 'Two Entries ID'); - await t.expect(browserPage.streamFields.count).eql(6, 'Two fields in table'); + await t.expect(browserPage.streamFields.count).eql(7, 'Two fields in table'); await t.expect(browserPage.streamEntryFields.count).eql(4, 'Four values in table'); // Create value to existed filed and check that new column was not added await browserPage.addEntryToStream(newField, chance.word({ length: 20 })); await t.expect(browserPage.streamEntryIDDateValue.count).eql(3, 'Three Entries ID'); - await t.expect(browserPage.streamFields.count).eql(7, 'Still two fields in table'); + await t.expect(browserPage.streamFields.count).eql(8, 'Still two fields in table'); await t.expect(browserPage.streamEntryFields.count).eql(6, 'Six values in table'); }); test('Verify that during new entry adding to existing Stream, user can clear the value and the row itself', async t => { diff --git a/tests/e2e/tests/critical-path/cli/cli-command-helper.e2e.ts b/tests/e2e/tests/critical-path/cli/cli-command-helper.e2e.ts index 20273e2ddd..a1bf9bbd18 100644 --- a/tests/e2e/tests/critical-path/cli/cli-command-helper.e2e.ts +++ b/tests/e2e/tests/critical-path/cli/cli-command-helper.e2e.ts @@ -3,8 +3,10 @@ import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; import { CliPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { CliActions } from '../../../common-actions/cli-actions'; const cliPage = new CliPage(); +const cliActions = new CliActions(); const defaultHelperText = 'Enter any command in CLI or use search to see detailed information.'; const COMMAND_APPEND = 'APPEND'; @@ -116,14 +118,14 @@ test //Select group from list and remember commands await cliPage.selectFilterGroupType(COMMAND_GROUP_TIMESERIES); const commandsFilterCount = await cliPage.cliHelperOutputTitles.count; - const timeSeriesCommands = []; + const timeSeriesCommands: string[] = []; for(let i = 0; i < commandsFilterCount; i++) { timeSeriesCommands.push(await cliPage.cliHelperOutputTitles.nth(i).textContent); } //Unselect group from list await cliPage.selectFilterGroupType(COMMAND_GROUP_TIMESERIES); //Search per part of command and check all opened commands - await cliPage.checkSearchedCommandInCommandHelper(commandForSearch, timeSeriesCommands); + await cliActions.checkSearchedCommandInCommandHelper(commandForSearch, timeSeriesCommands); //Check the first command documentation url await cliPage.checkURLCommand(timeSeriesCommands[0], `https://redis.io/commands/${timeSeriesCommands[0].toLowerCase()}/`); await t.switchToParentWindow(); @@ -137,14 +139,14 @@ test //Select group from list and remember commands await cliPage.selectFilterGroupType(COMMAND_GROUP_GRAPH); const commandsFilterCount = await cliPage.cliHelperOutputTitles.count; - const graphCommands = []; + const graphCommands: string[] = []; for(let i = 0; i < commandsFilterCount; i++) { graphCommands.push(await cliPage.cliHelperOutputTitles.nth(i).textContent); } //Unselect group from list await cliPage.selectFilterGroupType(COMMAND_GROUP_GRAPH); //Search per part of command and check all opened commands - await cliPage.checkSearchedCommandInCommandHelper(commandForSearch, graphCommands); + await cliActions.checkSearchedCommandInCommandHelper(commandForSearch, graphCommands); //Check the first command documentation url await cliPage.checkURLCommand(graphCommands[0], externalPageLink); await t.switchToParentWindow(); diff --git a/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts b/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts new file mode 100644 index 0000000000..7420be5142 --- /dev/null +++ b/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts @@ -0,0 +1,114 @@ +import { rte } from '../../../helpers/constants'; +import { AddRedisDatabasePage, MyRedisDatabasePage } from '../../../pageObjects'; +import { commonUrl, ossClusterConfig, ossSentinelConfig, ossStandaloneConfig } from '../../../helpers/conf'; +import { acceptLicenseTerms } from '../../../helpers/database'; +import { + addNewOSSClusterDatabaseApi, + addNewStandaloneDatabaseApi, + deleteAllSentinelDatabasesApi, + deleteOSSClusterDatabaseApi, + deleteStandaloneDatabaseApi, + discoverSentinelDatabaseApi +} from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; + +const addRedisDatabasePage = new AddRedisDatabasePage(); +const myRedisDatabasePage = new MyRedisDatabasePage(); +const common = new Common(); +const newOssDatabaseAlias = 'cloned oss cluster'; + +fixture `Clone databases` + .meta({ type: 'critical_path' }) + .page(commonUrl); +test + .before(async () => { + await acceptLicenseTerms(); + await addNewStandaloneDatabaseApi(ossStandaloneConfig); + await common.reloadPage(); + }) + .after(async () => { + // Delete databases + const dbNumber = await myRedisDatabasePage.dbNameList.withExactText(ossStandaloneConfig.databaseName).count; + for (let i = 0; i < dbNumber; i++) { + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + } + }) + .meta({ rte: rte.standalone })('Verify that user can clone Standalone db', async t => { + await myRedisDatabasePage.clickOnEditDBByName(ossStandaloneConfig.databaseName); + // Verify that user can cancel the Clone by clicking the “Cancel” or the “x” button + await t.click(addRedisDatabasePage.cloneDatabaseButton); + await t.click(addRedisDatabasePage.cancelButton); + await t.expect(myRedisDatabasePage.editAliasButton.withText('Clone ').visible).notOk('Clone panel is still displayed', { timeout: 2000 }); + await myRedisDatabasePage.clickOnEditDBByName(ossStandaloneConfig.databaseName); + await t.click(addRedisDatabasePage.cloneDatabaseButton); + // Verify that user see the “Add Database Manually” form pre-populated with all the connection data when cloning DB + await t + // Verify that name in the header has the prefix “Clone” + .expect(myRedisDatabasePage.editAliasButton.withText('Clone ').visible).ok('Clone panel is not displayed') + .expect(addRedisDatabasePage.hostInput.getAttribute('value')).eql(ossStandaloneConfig.host, 'Wrong host value') + .expect(addRedisDatabasePage.portInput.getAttribute('value')).eql(ossStandaloneConfig.port, 'Wrong port value') + .expect(addRedisDatabasePage.databaseAliasInput.getAttribute('value')).eql(ossStandaloneConfig.databaseName, 'Wrong host value'); + // Verify that user can confirm the creation of the database by clicking “Clone Database” + await t.click(addRedisDatabasePage.addRedisDatabaseButton); + await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossStandaloneConfig.databaseName).count).eql(2, 'DB was not cloned'); + }); +test + .before(async () => { + await acceptLicenseTerms(); + await addNewOSSClusterDatabaseApi(ossClusterConfig); + await common.reloadPage(); + }) + .after(async() => { + // Delete database + await deleteOSSClusterDatabaseApi(ossClusterConfig); + await myRedisDatabasePage.deleteDatabaseByName(newOssDatabaseAlias); + }) + .meta({ rte: rte.ossCluster })('Verify that user can clone OSS Cluster', async t => { + await myRedisDatabasePage.clickOnEditDBByName(ossClusterConfig.ossClusterDatabaseName); + await t.click(addRedisDatabasePage.cloneDatabaseButton); + await t + .expect(myRedisDatabasePage.editAliasButton.withText('Clone ').visible).ok('Clone panel is not displayed') + .expect(addRedisDatabasePage.portInput.getAttribute('value')).eql(ossClusterConfig.ossClusterPort, 'Wrong port value') + .expect(addRedisDatabasePage.databaseAliasInput.getAttribute('value')).eql(ossClusterConfig.ossClusterDatabaseName, 'Wrong host value'); + // Edit Database alias before cloning + await t.typeText(addRedisDatabasePage.databaseAliasInput, newOssDatabaseAlias, { replace: true }); + await t.click(addRedisDatabasePage.addRedisDatabaseButton); + await t.expect(myRedisDatabasePage.dbNameList.withExactText(newOssDatabaseAlias).visible).ok('DB was not closed'); + await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossClusterConfig.ossClusterDatabaseName).visible).ok('Original DB is not displayed'); + }); +test + .before(async () => { + await acceptLicenseTerms(); + // Add Sentinel databases + await discoverSentinelDatabaseApi(ossSentinelConfig); + await common.reloadPage(); + }) + .after(async () => { + // Delete all primary groups + const sentinelCopy = ossSentinelConfig; + sentinelCopy.masters.push(ossSentinelConfig.masters[1]); + sentinelCopy.name.push(ossSentinelConfig.name[1]); + await deleteAllSentinelDatabasesApi(sentinelCopy); + await common.reloadPage(); + }) + .meta({ rte: rte.sentinel })('Verify that user can clone Sentinel', async t => { + await myRedisDatabasePage.clickOnEditDBByName(ossSentinelConfig.name[1]); + await t.click(addRedisDatabasePage.cloneDatabaseButton); + // Verify that for Sentinel Host and Port fields are replaced with editable Primary Group Name field + await t + .expect(myRedisDatabasePage.editAliasButton.withText('Clone ').visible).ok('Clone panel is not displayed') + .expect(addRedisDatabasePage.databaseAliasInput.getAttribute('value')).eql(ossSentinelConfig.name[1], 'Invalid primary group alias value') + .expect(addRedisDatabasePage.primaryGroupNameInput.getAttribute('value')).eql(ossSentinelConfig.name[1], 'Invalid primary group name value'); + // Validate Databases section + await t + .click(addRedisDatabasePage.cloneSentinelDatabaseNavigation) + .expect(addRedisDatabasePage.masterGroupPassword.getAttribute('value')).eql(ossSentinelConfig.masters[1].password, 'Invalid sentinel database password'); + // Validate Sentinel section + await t + .click(addRedisDatabasePage.cloneSentinelNavigation) + .expect(addRedisDatabasePage.portInput.getAttribute('value')).eql(ossSentinelConfig.sentinelPort, 'Invalid sentinel port') + .expect(addRedisDatabasePage.passwordInput.getAttribute('value')).eql(ossSentinelConfig.sentinelPassword, 'Invalid sentinel password'); + // Clone Sentinel Primary Group + await t.click(addRedisDatabasePage.addRedisDatabaseButton); + await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossSentinelConfig.masters[1].name).count).gt(1, 'Primary Group was not cloned'); + }); diff --git a/tests/e2e/tests/critical-path/database/modules.e2e.ts b/tests/e2e/tests/critical-path/database/modules.e2e.ts index a76183d39a..ea1d28c5cd 100644 --- a/tests/e2e/tests/critical-path/database/modules.e2e.ts +++ b/tests/e2e/tests/critical-path/database/modules.e2e.ts @@ -4,9 +4,11 @@ import { acceptLicenseTerms } from '../../../helpers/database'; import { MyRedisDatabasePage, DatabaseOverviewPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../helpers/conf'; import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const databaseOverviewPage = new DatabaseOverviewPage(); +const common = new Common(); const moduleNameList = ['RediSearch', 'RedisJSON', 'RedisGraph', 'RedisTimeSeries', 'RedisBloom', 'RedisGears', 'RedisAI']; const moduleList = [myRedisDatabasePage.moduleSearchIcon, myRedisDatabasePage.moduleJSONIcon, myRedisDatabasePage.moduleGraphIcon, myRedisDatabasePage.moduleTimeseriesIcon, myRedisDatabasePage.moduleBloomIcon, myRedisDatabasePage.moduleGearsIcon, myRedisDatabasePage.moduleAIIcon]; @@ -14,11 +16,11 @@ const moduleList = [myRedisDatabasePage.moduleSearchIcon, myRedisDatabasePage.mo fixture `Database modules` .meta({ type: 'critical_path' }) .page(commonUrl) - .beforeEach(async t => { + .beforeEach(async () => { await acceptLicenseTerms(); await addNewStandaloneDatabaseApi(ossStandaloneRedisearch); // Reload Page - await t.eval(() => location.reload()); + await common.reloadPage(); }) .afterEach(async() => { //Delete database diff --git a/tests/e2e/tests/critical-path/monitor/monitor.e2e.ts b/tests/e2e/tests/critical-path/monitor/monitor.e2e.ts index 192894c83b..07d4d3fd0e 100644 --- a/tests/e2e/tests/critical-path/monitor/monitor.e2e.ts +++ b/tests/e2e/tests/critical-path/monitor/monitor.e2e.ts @@ -25,16 +25,12 @@ const keyName = `${chance.word({ length: 20 })}-key`; const keyValue = `${chance.word({ length: 10 })}-value`; fixture `Monitor` - .meta({ type: 'critical_path' }) + .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async() => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); - }) - .afterEach(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test - .meta({ rte: rte.standalone }) .after(async() => { await browserPage.deleteKeyByName(keyName); await deleteStandaloneDatabaseApi(ossStandaloneConfig); @@ -56,8 +52,12 @@ test await monitorPage.checkCommandInMonitorResults(command, [keyName, keyValue]); }); test - .meta({ rte: rte.standalone })('Verify that user can see the list of all commands from all clients ran for this Redis database in the list of results in Monitor', async t => { - //Define commands in different clients + .after(async t => { + await t.click(myRedisDatabasePage.browserButton); + await browserPage.deleteKeyByName(keyName); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('Verify that user can see the list of all commands from all clients ran for this Redis database in the list of results in Monitor', async t => { + //Define commands in different clients const cli_command = 'command'; const workbench_command = 'hello'; const common_command = 'info'; diff --git a/tests/e2e/tests/critical-path/monitor/save-commands.e2e.ts b/tests/e2e/tests/critical-path/monitor/save-commands.e2e.ts index 2faa6a7774..893c91d048 100644 --- a/tests/e2e/tests/critical-path/monitor/save-commands.e2e.ts +++ b/tests/e2e/tests/critical-path/monitor/save-commands.e2e.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import * as os from 'os'; +import { join as joinPath } from 'path'; import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; import { MonitorPage, CliPage } from '../../../pageObjects'; import { @@ -12,13 +13,33 @@ import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; const monitorPage = new MonitorPage(); const cliPage = new CliPage(); const tempDir = os.tmpdir(); -const downloadsDir = `C:*****\\Downloads`; +let downloadedFilePath = ''; + +async function getFileDownloadPath(): Promise { + return joinPath(os.homedir(), 'Downloads'); +} + +async function findByFileStarts(dir: string): Promise { + if (fs.existsSync(dir)) { + const matchedFiles: string[] = []; + const files = fs.readdirSync(dir); + for (const file of files) { + if (file.startsWith('test_standalone')) { + matchedFiles.push(file); + } + } + return matchedFiles.length; + } else { + return 0; + } +} fixture `Save commands` .meta({ type: 'critical_path' }) .page(commonUrl) .beforeEach(async() => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + downloadedFilePath = await getFileDownloadPath(); }) .afterEach(async() => { //Delete database @@ -34,7 +55,7 @@ test //Check the toggle and Tooltip for Save log await t.expect(monitorPage.saveLogSwitchButton.visible).ok('The toggle that allows to save Profiler log is displayed'); await t.hover(monitorPage.saveLogSwitchButton); - for(const message of toolTip){ + for (const message of toolTip) { await t.expect(monitorPage.saveLogToolTip.textContent).contains(message, 'The toolTip for save log in Profiler is displayed'); } //Check toggle state @@ -88,25 +109,25 @@ test await t.expect(monitorPage.resetProfilerButton.visible).ok('The Reset Profiler button visibility'); await t.expect(monitorPage.downloadLogButton.visible).ok('The Download button visibility'); }); -//skipped due the error in path -test.skip +test .meta({ rte: rte.standalone })('Verify that when user see the toggle is OFF - Profiler logs are not being saved', async t => { //Remember the number of files in Temp - const numberOfDownloadFiles = fs.readdirSync(downloadsDir).length; + const numberOfDownloadFiles = await findByFileStarts(downloadedFilePath); //Start Monitor without Save logs await monitorPage.startMonitor(); + await t.wait(3000); //Check the download files - await t.expect(numberOfDownloadFiles).eql(fs.readdirSync(downloadsDir).length, 'The Profiler logs are not being saved'); + await t.expect(await findByFileStarts(downloadedFilePath)).eql(numberOfDownloadFiles, 'The Profiler logs are saved'); }); -//skipped due the error in path +// Skipped due to testCafe issue https://github.com/DevExpress/testcafe/issues/5574 test.skip .meta({ rte: rte.standalone })('Verify that when user see the toggle is ON - Profiler logs are being saved', async t => { //Remember the number of files in Temp - const numberOfDownloadFiles = fs.readdirSync(downloadsDir).length; + const numberOfDownloadFiles = await findByFileStarts(downloadedFilePath); //Start Monitor with Save logs await monitorPage.startMonitorWithSaveLog(); //Download logs and check result await monitorPage.stopMonitor(); await t.click(monitorPage.downloadLogButton); - await t.expect(numberOfDownloadFiles).gt(fs.readdirSync(downloadsDir).length, 'The Profiler logs are being saved'); + await t.expect(await findByFileStarts(downloadedFilePath)).gt(numberOfDownloadFiles, 'The Profiler logs not saved', { timeout: 5000 }); }); diff --git a/tests/e2e/tests/critical-path/notifications/notification-center.e2e.ts b/tests/e2e/tests/critical-path/notifications/notification-center.e2e.ts index 00d1d9bb3b..4f88727b44 100644 --- a/tests/e2e/tests/critical-path/notifications/notification-center.e2e.ts +++ b/tests/e2e/tests/critical-path/notifications/notification-center.e2e.ts @@ -4,20 +4,25 @@ import { commonUrl } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; import { NotificationPage, MyRedisDatabasePage } from '../../../pageObjects'; import { NotificationParameters } from '../../../pageObjects/notification-page'; +import { Common } from '../../../helpers/common'; const description = require('./notifications.json'); const jsonNotifications: NotificationParameters[] = description.notifications; const notificationPage = new NotificationPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); +const common = new Common(); + +// Sort all notifications in json file +const sortedNotifications = jsonNotifications.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1); fixture `Notifications` .meta({ rte: rte.none, type: 'critical_path' }) .page(commonUrl) - .beforeEach(async t => { + .beforeEach(async () => { await acceptLicenseTerms(); await notificationPage.changeNotificationsSwitcher(true); await deleteAllNotificationsFromDB(); - await t.eval(() => location.reload()); + await common.reloadPage(); }); test('Verify that when manager publishes new notification, it appears in the app', async t => { // Get number of notifications in the badge @@ -29,11 +34,17 @@ test('Verify that when manager publishes new notification, it appears in the app await t.expect(notificationPage.notificationTitle.visible).ok('Title in popup is displayed'); await t.expect(notificationPage.notificationBody.visible).ok('Body in popup is displayed'); await t.expect(notificationPage.notificationDate.visible).ok('Date in popup is displayed'); + // Verify that user can see notification with category badge and category color in a single notification + await t.expect(notificationPage.notificationCategory.visible).ok('Category is not displayed in popup'); + if (sortedNotifications[0].category !== undefined) { + await t.expect(notificationPage.notificationCategory.innerText).eql(sortedNotifications[0].category ?? '', 'Text for category is not correct'); + await t.expect(notificationPage.notificationCategory.withExactText(sortedNotifications[0].category ?? '').withAttribute('style', `background-color: rgb${sortedNotifications[0].rbgColor}; color: rgb(0, 0, 0);`).exists).ok('Category color'); + } // Verify that user can click on close button and received notification will be closed await t.click(notificationPage.closeNotificationPopup); await t.expect(notificationPage.notificationPopup.visible).notOk('Notification popup is not displayed'); // Verify that when user closes new notification popup, number of unread messages decreased in badge - if (notificationPage.notificationBadge.exists) { + if (await notificationPage.notificationBadge.exists) { const newMessagesAfterClosing = await notificationPage.getUnreadNotificationNumber(); await t.expect(newMessagesBeforeClosing).eql(newMessagesAfterClosing + 1, 'Reduced number of unread messages'); } @@ -67,6 +78,11 @@ test('Verify that user can open notification center by clicking on icon and see await t.expect(notificationPage.notificationTitle.withExactText(jsonNotifications[i].title).exists).ok('Displayed title'); await t.expect(notificationPage.notificationBody.withExactText(jsonNotifications[i].body).exists).ok('Displayed body'); await t.expect(notificationPage.notificationDate.withExactText(await notificationPage.convertEpochDateToMessageDate(jsonNotifications[i])).exists).ok('Displayed date'); + // Verify that user can see notification with category badge and category color in the notification center + if (jsonNotifications[i].category !== undefined) { + await t.expect(notificationPage.notificationCategory.withExactText(jsonNotifications[i].category ?? '').exists).ok(`${jsonNotifications[i].category} category name not displayed`); + await t.expect(notificationPage.notificationCategory.withExactText(jsonNotifications[i].category ?? '').withAttribute('style', `background-color: rgb${jsonNotifications[i].rbgColor}; color: rgb(0, 0, 0);`).exists).ok('Category color'); + } } // Verify that as soon as user closes notification center, unread messages become read await t.click(myRedisDatabasePage.myRedisDBButton); // Close notification center @@ -78,8 +94,6 @@ test('Verify that user can open notification center by clicking on icon and see test('Verify that all messages in notification center are sorted by timestamp from newest to oldest', async t => { // Wait for new notifications await t.expect(notificationPage.notificationBadge.exists).ok('New notifications appear', { timeout: 35000 }); - // Sort all notifications in json file - const sortedNotifications = jsonNotifications.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1); await t.click(notificationPage.notificationCenterButton); for (let i = 0; i < sortedNotifications.length; i++) { // Get data one by one from notification center @@ -97,7 +111,7 @@ test await acceptLicenseTerms(); await notificationPage.changeNotificationsSwitcher(false); await deleteAllNotificationsFromDB(); - await t.eval(() => location.reload()); + await common.reloadPage(); await t.expect(notificationPage.notificationBadge.exists).notOk('No badge'); })('Verify that new popup message is not displayed when notifications are turned off', async t => { // Verify that user can see notification badge increased when new messages is sent and notifications are turned off diff --git a/tests/e2e/tests/critical-path/notifications/notifications.json b/tests/e2e/tests/critical-path/notifications/notifications.json index c7cd850e63..a265fe474c 100644 --- a/tests/e2e/tests/critical-path/notifications/notifications.json +++ b/tests/e2e/tests/critical-path/notifications/notifications.json @@ -3,12 +3,17 @@ { "title": "Lorem ipsum dolor sit amet", "body": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque id augue ligula. Nulla facilisi. In hac habitasse platea dictumst. Maecenas gravida interdum velit ut consectetur. Vestibulum urna tellus, bibendum vitae massa ut, aliquet fringilla quam. Quisque commodo, neque eu condimentum dictum, turpis purus efficitur quam, in faucibus augue augue sed felis. Donec et faucibus nisl, sit amet tincidunt neque. In a enim nec odio condimentum sodales. Integer sed ligula eget neque venenatis commodo ut quis mauris. Phasellus urna augue, auctor ac nisl vel, fermentum pellentesque libero. Suspendisse tellus mauris, ultricies sed rhoncus vitae, suscipit quis odio. Nullam pellentesque enim eros, et lacinia risus placerat in. Aliquam et lorem ante. Proin tempor volutpat augue, in tincidunt magna viverra id.", - "timestamp": 1659438264 + "timestamp": 1659438264, + "category": "Habitasse platea dictumst", + "categoryColor": "#a00a6b", + "rbgColor": "(160, 10, 107)" }, { "title": "Sed a elit quis sem fringilla imperdiet cras malesuada justo sed urna fringilla lobortis", "body": "Aenean rhoncus massa ac vehicula scelerisque. Ut in lorem id eros commodo ultrices. Nunc consequat non augue vitae pharetra. Vivamus ut urna vel lacus accumsan pharetra. Aliquam rhoncus nibh ut elementum laoreet. Cras ut risus tortor. Aenean lacinia blandit felis, et tempor tellus vulputate ut. Maecenas in nibh a eros posuere cursus in ac ligula. Suspendisse iaculis congue risus, eget tristique quam porta id. In consequat, ligula in interdum laoreet, lacus leo tempus turpis, vitae mollis tellus lectus a felis. Sed accumsan ornare lorem, vel facilisis purus commodo eget. Vestibulum mattis blandit orci et molestie. Ut molestie ante eget eros gravida dictum. Maecenas eleifend eget dui sed imperdiet. In dapibus lectus et venenatis malesuada. Vivamus ut nunc neque.", - "timestamp": 1658935023 + "timestamp": 1658935023, + "category": "Maecenas nec ultrices", + "rbgColor": "(102, 102, 102)" }, { "title": "In vel ultricies justo sed at mi id nisl lacinia vehicula", @@ -18,12 +23,18 @@ { "title": "In a enim nec odio condimentum sodales. Integer sed ligula eget neque venenatis commodo ut quis mauris", "body": "In vel ultricies justo. Sed at mi id nisl lacinia vehicula ut eget orci. In hac habitasse platea dictumst. Ut vel elementum justo. Nulla finibus convallis felis. Aenean dictum interdum lorem, non placerat risus egestas vel. Nullam sit amet dui a mauris eleifend pharetra mattis vitae nisl. Sed semper justo id arcu suscipit, et pretium felis ornare. Nulla in auctor eros, vel pellentesque dui. Phasellus ut laoreet ipsum, ac aliquet erat", - "timestamp": 1557538664 + "timestamp": 1557538664, + "category": "Condimentum", + "categoryColor": "#008556", + "rbgColor": "(0, 133, 86)" }, { "title": "In vel ultricies justo sed at mi id nisl lacinia vehicula", "body": "In vel ultricies justo. Sed at mi id nisl lacinia vehicula ut eget orci. In hac habitasse platea dictumst. Ut vel elementum justo. Nulla finibus convallis felis. Aenean dictum interdum lorem, non placerat risus egestas vel. Nullam sit amet dui a mauris eleifend pharetra mattis vitae nisl. Sed semper justo id arcu suscipit, et pretium felis ornare. Nulla in auctor eros, vel pellentesque dui. Phasellus ut laoreet ipsum, ac aliquet erat", - "timestamp": 1669449262 + "timestamp": 1669449262, + "category": "Pellentesque", + "categoryColor": "#364cff", + "rbgColor": "(54, 76, 255)" } ] } diff --git a/tests/e2e/tests/critical-path/overview/overview.e2e.ts b/tests/e2e/tests/critical-path/overview/overview.e2e.ts new file mode 100644 index 0000000000..0458e52336 --- /dev/null +++ b/tests/e2e/tests/critical-path/overview/overview.e2e.ts @@ -0,0 +1,86 @@ +import { Selector } from 'testcafe'; +import { MyRedisDatabasePage, CliPage, OverviewPage, WorkbenchPage } from '../../../pageObjects'; +import { rte } from '../../../helpers/constants'; +import { acceptLicenseTermsAndAddOSSClusterDatabase } from '../../../helpers/database'; +import { commonUrl, ossClusterConfig } from '../../../helpers/conf'; +import { deleteOSSClusterDatabaseApi, getClusterNodesApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; + +const overviewPage = new OverviewPage(); +const myRedisDatabasePage = new MyRedisDatabasePage(); +const common = new Common(); +const cliPage = new CliPage(); +const workbenchPage = new WorkbenchPage(); + +const headerColumns = { + 'Type': 'OSS Cluster', + 'Version': '7.0.0', + 'User': 'Default' +}; +const keyName = common.generateWord(10); +const commandToAddKey = `set ${keyName} test`; + +fixture `Overview` + .meta({ type: 'critical_path', rte: rte.ossCluster }) + .page(commonUrl) + .beforeEach(async t => { + await acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig, ossClusterConfig.ossClusterDatabaseName); + // Go to Analysis Tools page + await t.click(myRedisDatabasePage.analysisPageButton); + }) + .afterEach(async() => { + await deleteOSSClusterDatabaseApi(ossClusterConfig); + }); +test('Overview tab header for OSS Cluster', async t => { + const uptime = /[1-9][0-9]\s|[0-9]\smin|[1-9][0-9]\smin|[0-9]\sh/; + // Verify that user see "Overview" tab by default for OSS Cluster + await t.expect(overviewPage.overviewTab.withAttribute('aria-selected', 'true').exists).ok('The Overview tab not opened'); + // Verify that user see "Overview" header with OSS Cluster info + for (const key in headerColumns) { + const columnSelector = Selector(`[data-testid=cluster-details-item-${key}]`); + await t.expect(columnSelector.textContent).contains(`${headerColumns[key]}`, `Cluster detail ${key} is incorrect`); + } + // Verify that Uptime is displayed as time in seconds or minutes from start + await t.expect(overviewPage.clusterDetailsUptime.textContent).match(uptime, 'Uptime value is not correct'); +}); +test + .after(async() => { + //Clear database and delete + await cliPage.sendCommandInCli(`DEL ${keyName}`); + await cliPage.sendCommandInCli('FT.DROPINDEX idx:schools DD'); + await deleteOSSClusterDatabaseApi(ossClusterConfig); + })('Primary node statistics table displaying', async t => { + // Remember initial table values + const initialValues: number[] = []; + const nodes = (await getClusterNodesApi(ossClusterConfig)).sort(); + const columns = ['Commands/s', 'Clients', 'Total Keys', 'Network Input', 'Network Output', 'Total Memory']; + for (const column in columns) { + initialValues.push(await overviewPage.getTotalValueByColumnName(column)); + } + const nodesNumberInHeader = parseInt((await overviewPage.tableHeaderCell.nth(0).textContent).match(/\d+/)![0]); + + // Add key from CLI + await t.click(cliPage.cliExpandButton); + await t.typeText(cliPage.cliCommandInput, commandToAddKey); + await t.pressKey('enter'); + await t.click(cliPage.cliCollapseButton); + // Verify nodes in header column equal to rows + await t.expect(await overviewPage.getPrimaryNodesCount()).eql(nodesNumberInHeader, 'Primary nodes in table are not displayed'); + // Verify that all nodes from BE response are displayed in table + for (const node of nodes) { + await t.expect(overviewPage.tableRow.nth(nodes.indexOf(node)).textContent).contains(node, `Node ${node} is not displayed in table`); + } + // Go to Workbench page + await t.click(myRedisDatabasePage.workbenchButton); + //Run Create hash index command to load network and memory + await t.click(workbenchPage.documentButtonInQuickGuides); + await t.click(workbenchPage.internalLinkWorkingWithHashes); + await t.click(workbenchPage.preselectCreateHashIndex); + await t.click(workbenchPage.submitCommandButton); + // Go to Analysis Tools page + await t.click(myRedisDatabasePage.analysisPageButton); + // Verify that values in table are dynamic + for (const column in columns) { + await t.expect(await overviewPage.getTotalValueByColumnName(column)).notEql(initialValues[columns.indexOf(column)], `${column} not dynamic`); + } + }); diff --git a/tests/e2e/tests/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts b/tests/e2e/tests/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts index b0d120326c..3784b1c14b 100644 --- a/tests/e2e/tests/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts +++ b/tests/e2e/tests/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts @@ -4,10 +4,12 @@ import { commonUrl, ossStandaloneConfig, ossStandaloneV5Config } from '../../../ import { env, rte } from '../../../helpers/constants'; import { verifyMessageDisplayingInPubSub } from '../../../helpers/pub-sub'; import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const pubSubPage = new PubSubPage(); const cliPage = new CliPage(); +const common = new Common(); fixture `Subscribe/Unsubscribe from a channel` .meta({ env: env.web, rte: rte.standalone, type: 'critical_path' }) @@ -65,7 +67,7 @@ test await acceptLicenseTerms(); await addNewStandaloneDatabaseApi(ossStandaloneV5Config); await addNewStandaloneDatabaseApi(ossStandaloneConfig); - await t.eval(() => location.reload()); + await common.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); //Go to PubSub page await t.click(myRedisDatabasePage.pubSubButton); diff --git a/tests/e2e/tests/critical-path/settings/settings.e2e.ts b/tests/e2e/tests/critical-path/settings/settings.e2e.ts index 0caa377c2f..9fe4aaad6a 100644 --- a/tests/e2e/tests/critical-path/settings/settings.e2e.ts +++ b/tests/e2e/tests/critical-path/settings/settings.e2e.ts @@ -2,9 +2,11 @@ import { MyRedisDatabasePage, SettingsPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { acceptLicenseTerms } from '../../../helpers/database'; import { commonUrl } from '../../../helpers/conf'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const settingsPage = new SettingsPage(); +const common = new Common(); const explicitErrorHandler = (): void => { window.addEventListener('error', e => { @@ -30,7 +32,7 @@ test await t.click(settingsPage.accordionAdvancedSettings); await settingsPage.changeKeysToScanValue('1500'); // Reload Page - await t.eval(() => location.reload()); + await common.reloadPage(); // Check that value was set await t.click(settingsPage.accordionAdvancedSettings); await t.expect(settingsPage.keysToScanValue.textContent).eql('1500', 'Keys to Scan has proper value'); @@ -49,7 +51,7 @@ test for (const value of equalValues) { await t.click(settingsPage.switchAnalyticsOption); // Reload Page - await t.eval(() => location.reload()); + await common.reloadPage(); await t.click(settingsPage.accordionPrivacySettings); await t.expect(await settingsPage.getAnalyticsSwitcherValue()).eql(value, 'Analytics was switched properly'); } diff --git a/tests/e2e/tests/critical-path/slow-log/slow-log.e2e.ts b/tests/e2e/tests/critical-path/slow-log/slow-log.e2e.ts index 3fea91e10b..eaf2458dd3 100644 --- a/tests/e2e/tests/critical-path/slow-log/slow-log.e2e.ts +++ b/tests/e2e/tests/critical-path/slow-log/slow-log.e2e.ts @@ -1,4 +1,4 @@ -import { SlowLogPage, MyRedisDatabasePage, BrowserPage, CliPage } from '../../../pageObjects'; +import { SlowLogPage, MyRedisDatabasePage, BrowserPage, CliPage, OverviewPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; import { commonUrl, ossStandaloneBigConfig } from '../../../helpers/conf'; @@ -8,6 +8,7 @@ const slowLogPage = new SlowLogPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); const cliPage = new CliPage(); +const overviewPage = new OverviewPage(); const slowerThanParameter = 1; let maxCommandLength = 50; let command = `slowlog get ${maxCommandLength}`; @@ -17,13 +18,15 @@ fixture `Slow Log` .page(commonUrl) .beforeEach(async t => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); - await t.click(slowLogPage.slowLogPageButton); + await t.click(myRedisDatabasePage.analysisPageButton); }) .afterEach(async() => { await slowLogPage.resetToDefaultConfig(); await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); }); test('Verify that user can open new Slow Log page using new icon on left app panel', async t => { + // Verify that user see "Slow Log" page by default for non OSS Cluster + await t.expect(overviewPage.overviewTab.withAttribute('aria-selected', 'true').exists).notOk('The Overview tab is displayed for non OSS Cluster db'); // Verify that user can configure slowlog-max-len for Slow Log and see whole set of commands according to the setting await slowLogPage.changeSlowerThanParameter(slowerThanParameter); await cliPage.sendCommandInCli(command); @@ -49,7 +52,7 @@ test('Verify that user can see "No Slow Logs found" message when slowlog-max-len // Go to Browser page to scan keys and turn back await t.click(myRedisDatabasePage.browserButton); await t.click(browserPage.refreshKeysButton); - await t.click(slowLogPage.slowLogPageButton); + await t.click(myRedisDatabasePage.analysisPageButton); // Compare number of logged commands with maxLength await t.expect(slowLogPage.slowLogCommandStatistics.withText(`${maxCommandLength} entries`).exists).ok('Number of displayed commands is less than '); }); @@ -73,7 +76,7 @@ test('Verify that users can specify number of commands that they want to display // Go to Browser page to scan keys and turn back await t.click(myRedisDatabasePage.browserButton); await t.click(browserPage.refreshKeysButton); - await t.click(slowLogPage.slowLogPageButton); + await t.click(myRedisDatabasePage.analysisPageButton); for (let i = 0; i < numberOfCommandsArray.length; i++) { await slowLogPage.changeDisplayUpToParameter(numberOfCommandsArray[i]); if (i === numberOfCommandsArray.length - 1) { @@ -117,8 +120,7 @@ test('Verify that user can reset settings to default on Slow Log page', async t await slowLogPage.changeMaxLengthParameter(maxCommandLength); // Reset settings to default await slowLogPage.resetToDefaultConfig(); - // Open Slow Log configuration and check default settings - await t.expect(slowLogPage.slowLogSlowerThanConfig.withAttribute('value', '10000').exists).ok('Default Slower Than'); - await t.expect(slowLogPage.slowLogMaxLengthConfig.withAttribute('value', '128').exists).ok('Default Max Length'); - await t.expect(slowLogPage.slowLogConfigureUnitButton.withExactText('µs').exists).ok('Default Slower Than'); + // Compare configuration after re-setting + const configText = await slowLogPage.configInfo.textContent; + await t.expect(configText.replace(/\u00a0/g, ' ')).contains('Execution time: 10 000 µs, Max length: 128', 'Not reset configuration'); }); diff --git a/tests/e2e/tests/critical-path/tree-view/tree-view.e2e.ts b/tests/e2e/tests/critical-path/tree-view/tree-view.e2e.ts index b1f2ed494a..65ae5e5d48 100644 --- a/tests/e2e/tests/critical-path/tree-view/tree-view.e2e.ts +++ b/tests/e2e/tests/critical-path/tree-view/tree-view.e2e.ts @@ -7,9 +7,11 @@ import { } from '../../../helpers/conf'; import { rte, KeyTypesTexts } from '../../../helpers/constants'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); const chance = new Chance(); +const common = new Common(); const keyNameFilter = `keyName${chance.word({ length: 10 })}`; @@ -32,7 +34,7 @@ test test .meta({ rte: rte.standalone })('Verify that user can see that "Tree view" mode is enabled state is saved when refreshes the page', async t => { await t.click(browserPage.treeViewButton); - await t.eval(() => location.reload()); + await common.reloadPage(); //Verify that "Tree view" mode enabled state is saved await t.expect(browserPage.treeViewArea.visible).ok('The tree view is displayed'); }); @@ -40,15 +42,7 @@ test .meta({ rte: rte.standalone })('Verify that user can scan DB by 10K in tree view', async t => { await t.click(browserPage.treeViewButton); //Verify that user can use the "Scan More" button to search per another 10000 keys - for (let i = 10; i < 100; i += 10) { - // scannedValue = scannedValue + 10; - await t.expect(browserPage.progressKeyList.exists).notOk('Progress Bar is not displayed', { timeout: 30000 }); - const scannedValueText = await browserPage.scannedValue.textContent; - const regExp = new RegExp(`${i} 00` + '.'); - await t.expect(scannedValueText).match(regExp, `The database is automatically scanned by ${i} 000 keys`); - await t.doubleClick(browserPage.scanMoreButton); - await t.expect(browserPage.progressKeyList.exists).ok('Progress Bar is displayed', { timeout: 30000 }); - } + await browserPage.verifyScannningMore(); }); test .after(async() => { diff --git a/tests/e2e/tests/critical-path/workbench/command-results.e2e.ts b/tests/e2e/tests/critical-path/workbench/command-results.e2e.ts index 7aa654e79f..627e9f5ec8 100644 --- a/tests/e2e/tests/critical-path/workbench/command-results.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/command-results.e2e.ts @@ -14,7 +14,7 @@ const commandForSend2 = 'FT._LIST'; let indexName = chance.word({ length: 5 }); fixture `Command results at Workbench` - .meta({type: 'critical_path'}) + .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); @@ -27,8 +27,7 @@ fixture `Command results at Workbench` await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); await deleteStandaloneDatabaseApi(ossStandaloneConfig); }); -test - .meta({ rte: rte.standalone })('Verify that user can see re-run icon near the already executed command and re-execute the command by clicking on the icon in Workbench page', async t => { +test('Verify that user can see re-run icon near the already executed command and re-execute the command by clicking on the icon in Workbench page', async t => { //Send commands await workbenchPage.sendCommandInWorkbench(commandForSend1); await workbenchPage.sendCommandInWorkbench(commandForSend2); @@ -40,8 +39,7 @@ test //Verify that command is re-executed await t.expect(workbenchPage.queryCardCommand.textContent).eql(commandForSend1, 'The command is re-executed'); }); -test - .meta({ rte: rte.standalone })('Verify that user can see expanded result after command re-run at the top of results table in Workbench', async t => { +test('Verify that user can see expanded result after command re-run at the top of results table in Workbench', async t => { //Send commands await workbenchPage.sendCommandInWorkbench(commandForSend1); await workbenchPage.sendCommandInWorkbench(commandForSend2); @@ -53,8 +51,7 @@ test //Verify that re-executed command is at the top of results await t.expect(workbenchPage.queryCardCommand.nth(0).textContent).eql(commandForSend1, 'The re-executed command is at the top of results table'); }); -test - .meta({ rte: rte.standalone })('Verify that user can delete command with result from table with results in Workbench', async t => { +test('Verify that user can delete command with result from table with results in Workbench', async t => { //Send command await workbenchPage.sendCommandInWorkbench(commandForSend1); //Delete the command from results @@ -63,8 +60,7 @@ test //Verify that deleted command is not in results await t.expect(workbenchPage.queryCardCommand.withExactText(commandForSend1).exists).notOk(`Command ${commandForSend1} is deleted from table with results`); }); -test - .meta({ rte: rte.standalone })('Verify that user can see the results found in the table view by default for FT.INFO, FT.SEARCH and FT.AGGREGATE', async t => { +test('Verify that user can see the results found in the table view by default for FT.INFO, FT.SEARCH and FT.AGGREGATE', async t => { const commands = [ 'FT.INFO', 'FT.SEARCH', @@ -77,7 +73,7 @@ test } }); test - .meta({ env: env.desktop, rte: rte.standalone })('Verify that user can switches between views and see results according to the view rules in Workbench in results', async t => { + .meta({ env: env.desktop })('Verify that user can switches between views and see results according to the view rules in Workbench in results', async t => { indexName = chance.word({ length: 5 }); const commands = [ 'hset doc:10 title "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud" url "redis.io" author "Test" rate "undefined" review "0" comment "Test comment"', @@ -85,7 +81,7 @@ test `FT.SEARCH ${indexName} * limit 0 10000` ]; //Send commands and check table view is default for Search command - for(const command of commands) { + for (const command of commands) { await workbenchPage.sendCommandInWorkbench(command); } await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssTableViewTypeOption).visible).ok('The table view is selected by default for command FT.SEARCH'); @@ -96,9 +92,8 @@ test await workbenchPage.selectViewTypeText(); await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).visible).ok('The result is displayed in Text view'); }); -//skipped due the inaccessibility of the iframe -test.skip - .meta({ env: env.desktop, rte: rte.standalone })('Verify that user can switches between Table and Text for Client List and see results corresponding to their views', async t => { +// Skipped due to issue https://redislabs.atlassian.net/browse/RI-3524 +test.skip('Verify that user can switches between Table and Text for Client List and see results corresponding to their views', async t => { const command = 'CLIENT LIST'; //Send command and check table view is default await workbenchPage.sendCommandInWorkbench(command); @@ -113,8 +108,7 @@ test .after(async() => { //Drop database await deleteStandaloneDatabaseApi(ossStandaloneConfig); - }) - .meta({ rte: rte.standalone })('Verify that user can populate commands in Editor from history by clicking keyboard “up” button', async t => { + })('Verify that user can populate commands in Editor from history by clicking keyboard “up” button', async t => { const commands = [ 'FT.INFO', 'RANDOMKEY', @@ -124,9 +118,13 @@ test for(const command of commands) { await workbenchPage.sendCommandInWorkbench(command); } + // Clear input + await t + .click(workbenchPage.queryInput) + .pressKey('ctrl+a') + .pressKey('delete'); //Verify the quick access to command history by up button - await t.click(workbenchPage.queryInput); - for(const command of commands.reverse()) { + for (const command of commands.reverse()) { await t.pressKey('up'); const script = await workbenchPage.scriptsLines.textContent; await t.expect(script.replace(/\s/g, ' ')).contains(command, 'Result of Manual command is displayed'); diff --git a/tests/e2e/tests/critical-path/workbench/default-scripts-area.e2e.ts b/tests/e2e/tests/critical-path/workbench/default-scripts-area.e2e.ts index 42e4c064f1..8be3417ee9 100644 --- a/tests/e2e/tests/critical-path/workbench/default-scripts-area.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/default-scripts-area.e2e.ts @@ -2,7 +2,7 @@ import { Chance } from 'chance'; import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage } from '../../../pageObjects'; import { rte, env } from '../../../helpers/constants'; -import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; +import { commonUrl, ossStandaloneRedisearch } from '../../../helpers/conf'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); @@ -16,7 +16,7 @@ fixture `Default scripts area at Workbench` .meta({type: 'critical_path'}) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); //Go to Workbench page await t.click(myRedisDatabasePage.workbenchButton); }) @@ -24,7 +24,7 @@ fixture `Default scripts area at Workbench` //Drop index, documents and database await t.switchToMainWindow(); await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); }); test .meta({ env: env.desktop, rte: rte.standalone })('Verify that user can edit and run automatically added "FT._LIST" and "FT.INFO {index}" scripts in Workbench and see the results', async t => { diff --git a/tests/e2e/tests/regression/browser/context.e2e.ts b/tests/e2e/tests/regression/browser/context.e2e.ts index 3fed48d064..f6e37b389c 100644 --- a/tests/e2e/tests/regression/browser/context.e2e.ts +++ b/tests/e2e/tests/regression/browser/context.e2e.ts @@ -8,11 +8,13 @@ import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { Chance } from 'chance'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); const cliPage = new CliPage(); const chance = new Chance(); +const common = new Common(); let keyName = chance.word({ length: 10 }); @@ -63,7 +65,7 @@ test await t.expect(await browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('The key details is selected'); //Navigate to Workbench and reload the window await t.click(myRedisDatabasePage.workbenchButton); - await t.eval(() => location.reload()); + await common.reloadPage(); //Return back to Browser and check context is not saved await t.click(myRedisDatabasePage.browserButton); await t.expect(await browserPage.filterByPatterSearchInput.withAttribute('value', keyName).exists).notOk('Filter per key name is not applied'); diff --git a/tests/e2e/tests/regression/browser/database-overview-keys.e2e.ts b/tests/e2e/tests/regression/browser/database-overview-keys.e2e.ts index 5749a87303..a8c20737fd 100644 --- a/tests/e2e/tests/regression/browser/database-overview-keys.e2e.ts +++ b/tests/e2e/tests/regression/browser/database-overview-keys.e2e.ts @@ -1,6 +1,6 @@ import { Chance } from 'chance'; import { t } from 'testcafe'; -import { acceptLicenseTerms, acceptLicenseTermsAndAddDatabase, deleteDatabase } from '../../../helpers/database'; +import { acceptLicenseTermsAndAddDatabase, acceptLicenseTermsAndAddRECloudDatabase, deleteDatabase } from '../../../helpers/database'; import { MyRedisDatabasePage, CliPage, @@ -32,7 +32,7 @@ const verifyTooltipContainsText = async(text: string, contains: boolean): Promis }; fixture `Database overview` - .meta({ rte: rte.standalone, type: 'regression' }) + .meta({ type: 'regression' }) .page(commonUrl) .beforeEach(async t => { //Create databases and keys @@ -54,33 +54,31 @@ fixture `Database overview` await browserPage.deleteKeyByName(keyName); await deleteStandaloneDatabaseApi(ossStandaloneConfig); }); -test('Verify that user can see total and current logical database number of keys (if there are any keys in other logical DBs)', async t => { - //Wait for Total Keys number refreshed - await t.expect(browserPage.overviewTotalKeys.withText(`${keysAmount + 1}`).exists).ok('Total keys are not changed', { timeout: 10000 }); - await t.hover(workbenchPage.overviewTotalKeys); - //Verify that user can see total number of keys and number of keys in current logical database - await t.expect(browserPage.tooltip.visible).ok('Total keys tooltip not displayed'); - await verifyTooltipContainsText(`${keysAmount + 1}Total Keys`, true); - await verifyTooltipContainsText(`db1:${keysAmount}Keys`, true); -}); -test('Verify that user can see total number of keys and not it current logical database (if there are no any keys in other logical DBs)', async t => { - //Open Database - await t.click(myRedisDatabasePage.myRedisDBButton); - await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); - await t.hover(workbenchPage.overviewTotalKeys); - //Verify that user can see only total number of keys - await t.expect(browserPage.tooltip.visible).ok('Total keys tooltip not displayed'); - await verifyTooltipContainsText(`${keysAmount + 1}Total Keys`, true); - await verifyTooltipContainsText('db1', false); -}); test - .before(async t => { - await acceptLicenseTerms(); - await addRedisDatabasePage.addRedisDataBase(cloudDatabaseConfig); - //Click for saving - await t.click(addRedisDatabasePage.addRedisDatabaseButton); - await t.expect(myRedisDatabasePage.dbNameList.withExactText(cloudDatabaseConfig.databaseName).exists).ok('The existence of the database', { timeout: 5000 }); - await myRedisDatabasePage.clickOnDBByName(cloudDatabaseConfig.databaseName); + .meta({ rte: rte.standalone })('Verify that user can see total and current logical database number of keys (if there are any keys in other logical DBs)', async t => { + //Wait for Total Keys number refreshed + await t.expect(browserPage.overviewTotalKeys.withText(`${keysAmount + 1}`).exists).ok('Total keys are not changed', { timeout: 10000 }); + await t.hover(workbenchPage.overviewTotalKeys); + //Verify that user can see total number of keys and number of keys in current logical database + await t.expect(browserPage.tooltip.visible).ok('Total keys tooltip not displayed'); + await verifyTooltipContainsText(`${keysAmount + 1}Total Keys`, true); + await verifyTooltipContainsText(`db1:${keysAmount}Keys`, true); + }); +test + .meta({ rte: rte.standalone })('Verify that user can see total number of keys and not it current logical database (if there are no any keys in other logical DBs)', async t => { + //Open Database + await t.click(myRedisDatabasePage.myRedisDBButton); + await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); + await t.hover(workbenchPage.overviewTotalKeys); + //Verify that user can see only total number of keys + await t.expect(browserPage.tooltip.visible).ok('Total keys tooltip not displayed'); + await verifyTooltipContainsText(`${keysAmount + 1}Total Keys`, true); + await verifyTooltipContainsText('db1', false); + }); +test + .meta({ rte: rte.reCloud }) + .before(async() => { + await acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); }) .after(async() => { //Delete database diff --git a/tests/e2e/tests/regression/browser/filtering.e2e.ts b/tests/e2e/tests/regression/browser/filtering.e2e.ts index 6959e1bbaa..770d88b0de 100644 --- a/tests/e2e/tests/regression/browser/filtering.e2e.ts +++ b/tests/e2e/tests/regression/browser/filtering.e2e.ts @@ -147,14 +147,12 @@ test await t.expect(browserPage.filterByPatterSearchInput.getAttribute('value')).eql('', 'All characters from filter input are removed'); await t.expect(browserPage.clearFilterButton.visible).notOk('The clear control is disappeared'); }); -test - .after(async() => { - //Delete database - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - })('Verify that when user clicks on “clear” control and filter per key name is applied filter is reset and rescan initiated', async t => { +test('Verify that when user clicks on “clear” control and filter per key name is applied filter is reset and rescan initiated', async t => { keyName = `KeyForSearch${chance.word({ length: 50 })}`; + //Add keys + await browserPage.addStringKey(keyName); //Search for not existed key name - await browserPage.searchByKeyName(keyName); + await browserPage.searchByKeyName(keyName2); await t.expect(browserPage.keyListTable.textContent).contains('No results found.', 'Key is not found'); //Verify the clear control await t.click(browserPage.clearFilterButton); @@ -206,33 +204,33 @@ test }) .after(async() => { // Delete database - await browserPage.deleteKeyByName(keyName); await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user can filter per key name using patterns in DB with 10-50 millions of keys', async t => { - // Create new key - keyName = `KeyForSearch-${chance.word({ length: 10 })}`; - await browserPage.addSetKey(keyName); - // Search by key name + keyName = 'device*'; await browserPage.selectFilterGroupType(KeyTypesTexts.Set); - // Verify that required key is displayed - await browserPage.searchByKeyNameWithScanMore('KeyForSearch*', keyName); - // Verify that required key is displayed in tree view + await browserPage.searchByKeyName(keyName); + for (let i = 0; i < 10; i++) { + // Verify that keys are filtered + await t.expect(browserPage.keyNameInTheList.nth(i).textContent).contains('device', 'Keys filtered incorrectly by key name') + .expect(browserPage.keyNameInTheList.nth(i).textContent).contains('set', 'Keys filtered incorrectly by key type'); + } await t.click(browserPage.treeViewButton); - await browserPage.searchByKeyNameWithScanMore('KeyForSearch*', keyName); + //Verify that user can use the "Scan More" button to search per another 10000 keys + await browserPage.verifyScannningMore(); }); test - .before(async () => { + .before(async() => { // Add Big standalone DB await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); }) - .after(async () => { + .after(async() => { // Delete database await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user can filter per key type in DB with 10-50 millions of keys', async t => { for (let i = 0; i < keyTypes.length - 2; i++) { await browserPage.selectFilterGroupType(keyTypes[i].textType); const filteredTypeKeys = keyTypes[i].keyName === 'json' - ? Selector(`[data-testid^=badge-ReJSON]`) + ? Selector('[data-testid^=badge-ReJSON]') : Selector(`[data-testid^=badge-${keyTypes[i].keyName}]`); // Verify that all results have the same type as in filter await t.expect(await browserPage.filteringLabel.count).eql(await filteredTypeKeys.count, `The keys of type ${keyTypes[i].textType} not filtered correctly`); diff --git a/tests/e2e/tests/regression/browser/format-switcher.e2e.ts b/tests/e2e/tests/regression/browser/format-switcher.e2e.ts index c721a6f7ce..79fde67352 100644 --- a/tests/e2e/tests/regression/browser/format-switcher.e2e.ts +++ b/tests/e2e/tests/regression/browser/format-switcher.e2e.ts @@ -1,17 +1,21 @@ import { keyLength, rte } from '../../../helpers/constants'; import { addKeysViaCli, deleteKeysViaCli, keyTypes } from '../../../helpers/keys'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; -import { BrowserPage } from '../../../pageObjects'; +import { acceptLicenseTerms, acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { addNewStandaloneDatabasesApi, deleteStandaloneDatabaseApi, deleteStandaloneDatabasesApi } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); const common = new Common(); +const myRedisDatabasePage = new MyRedisDatabasePage(); const keysData = keyTypes.map(object => ({ ...object })); keysData.forEach(key => key.keyName = `${key.keyName}` + '-' + `${common.generateWord(keyLength)}`); -const defaultValue = 'Unicode'; +const databasesForAdding = [ + { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testDB1' }, + { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testDB2' } +]; fixture `Format switcher functionality` .meta({ @@ -29,26 +33,41 @@ fixture `Format switcher functionality` await deleteKeysViaCli(keysData); await deleteStandaloneDatabaseApi(ossStandaloneConfig); }); -test('Verify that user can see switcher changed to default for key when reopening key details', async t => { - // Open key details and select JSON formatter - await browserPage.openKeyDetails(keysData[0].keyName); - await browserPage.selectFormatter('JSON'); - // Reopen key details - await t.click(browserPage.closeKeyButton); - await browserPage.openKeyDetailsByKeyName(keysData[0].keyName); - // Verify that formatter changed to default 'Unicode' - await t.expect(browserPage.formatSwitcher.withExactText(defaultValue).visible).ok('Formatter value is not default'); -}); -test('Verify that user can see switcher changed to default for key when switching between keys when details tab opened', async t => { - // Open key details and select HEX formatter - await t.click(browserPage.searchButton); - await browserPage.openKeyDetailsByKeyName(keysData[1].keyName); - await browserPage.selectFormatter('HEX'); - // Open another key details - await browserPage.openKeyDetailsByKeyName(keysData[3].keyName); - // Verify that formatter changed to default 'Unicode' - await t.expect(browserPage.formatSwitcher.withExactText(defaultValue).visible).ok('Formatter value is not default'); -}); +test + .before(async() => { + // Add new databases using API + await acceptLicenseTerms(); + await addNewStandaloneDatabasesApi(databasesForAdding); + // Reload Page + await common.reloadPage(); + await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName); + // Create new keys + await addKeysViaCli(keysData); + }) + .after(async() => { + // Clear keys and database + await deleteKeysViaCli(keysData); + await deleteStandaloneDatabasesApi(databasesForAdding); + })('Formatters saved selection', async t => { + // Open key details and select JSON formatter + await browserPage.openKeyDetails(keysData[0].keyName); + await browserPage.selectFormatter('JSON'); + // Reopen key details + await t.click(browserPage.closeKeyButton); + await browserPage.openKeyDetailsByKeyName(keysData[0].keyName); + // Verify that formatters selection is saved when user switches between keys + await t.expect(browserPage.formatSwitcher.withExactText('JSON').visible).ok('Formatter value is not saved'); + // Verify that formatters selection is saved when user reloads the page + await common.reloadPage(); + await browserPage.openKeyDetailsByKeyName(keysData[1].keyName); + await t.expect(browserPage.formatSwitcher.withExactText('JSON').visible).ok('Formatter value is not saved'); + // Go to another database + await t.click(myRedisDatabasePage.myRedisDBButton); + await myRedisDatabasePage.clickOnDBByName(databasesForAdding[1].databaseName); + await browserPage.openKeyDetailsByKeyName(keysData[2].keyName); + // Verify that formatters selection is saved when user switches between databases + await t.expect(browserPage.formatSwitcher.withExactText('JSON').visible).ok('Formatter value is not saved'); + }); test('Verify that user don`t see format switcher for JSON, GRAPH, TS keys', async t => { // Create array with JSON, GRAPH, TS keys const keysWithoutSwitcher = [keysData[5], keysData[7], keysData[8]]; diff --git a/tests/e2e/tests/regression/browser/formatter-warning.e2e.ts b/tests/e2e/tests/regression/browser/formatter-warning.e2e.ts new file mode 100644 index 0000000000..333cb017af --- /dev/null +++ b/tests/e2e/tests/regression/browser/formatter-warning.e2e.ts @@ -0,0 +1,61 @@ +import { Chance } from 'chance'; +import { rte } from '../../../helpers/constants'; +import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { BrowserPage, CliPage } from '../../../pageObjects'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; +import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; + +const browserPage = new BrowserPage(); +const cliPage = new CliPage(); +const chance = new Chance(); + +const jsonInvalidStructure = '"{\"test\": 123"'; +const title = 'Value will be saved as Unicode'; +const reason = 'as it is not valid in the selected format.'; +let keyName = chance.word({ length: 10 }); + +fixture `Warning for invalid formatter value` + .meta({ + type: 'regression', + rte: rte.standalone + }) + .page(commonUrl) + .beforeEach(async() => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + // await cliPage.sendCommandInCli(jsonCommand); + }) + .afterEach(async() => { + // Clear keys and database + await cliPage.sendCommandInCli(`del ${keyName}`); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + }); +test('Verify that user can see warning message when editing value', async t => { + // Open key details + await browserPage.addStringKey(keyName, '{"test": 123}'); + await browserPage.selectFormatter('JSON'); + await browserPage.editStringKeyValue(jsonInvalidStructure); + await t + // Verify that user sees warning message when value in selected format is not correct + .expect(browserPage.changeValueWarning.visible).ok('Warning is not displayed') + // Verify that tooltip has text "Value will be saved as Unicode as it is not valid in the selected format." + .expect(browserPage.changeValueWarning.find('h4').withExactText(title).visible).ok('Title is not correct') + .expect(browserPage.changeValueWarning.find('div').withExactText(reason).visible).ok('Reason is not correct'); + await t.click(browserPage.saveButton); + // Verify that when user click on save button, value is saved in Unicode format + await t.expect(browserPage.stringValueAsJson.exists).notOk('Value is not converted to Unicode'); + // Verify that user doesn't see warning message if saving value is correct in selected format + await browserPage.editStringKeyValue('{"test": 123}'); + await t + .expect(browserPage.changeValueWarning.visible).notOk('Warning is not displayed') + .expect(browserPage.stringValueAsJson.exists).ok('Value is not converted to JSON object'); +}); +test('Verify that user can remove invalid format value warning the message by clicking on ESC button', async t => { + keyName = chance.word({ length: 10 }); + const keyValue = 'a:1:{s:8:"glossary";a:2:{s:5:"title";s:7:"example";s:8:"GlossDiv";a:2:{s:5:"title";s:1:"S";s:9:"GlossList";a:1:{s:10:"GlossEntry";a:3:{s:2:"ID";s:4:"SGML";s:8:"GlossDef";a:2:{s:4:"para";s:8:"language";s:12:"GlossSeeAlso";a:1:{i:0;s:3:"XML";}}s:8:"GlossSee";s:6:"markup";}}}}}'; + await browserPage.addHashKey(keyName, '5000', 'PHP Serialized', keyValue); + await browserPage.selectFormatter('PHP serialized'); + await browserPage.editHashKeyValue(jsonInvalidStructure); + await t.expect(browserPage.changeValueWarning.visible).ok('Warning is not displayed'); + await t.pressKey('esc'); + await t.expect(browserPage.changeValueWarning.visible).notOk('Warning is still displayed'); +}); diff --git a/tests/e2e/tests/regression/browser/full-screen.e2e.ts b/tests/e2e/tests/regression/browser/full-screen.e2e.ts index f4deb5e061..5056dbef05 100644 --- a/tests/e2e/tests/regression/browser/full-screen.e2e.ts +++ b/tests/e2e/tests/regression/browser/full-screen.e2e.ts @@ -40,7 +40,7 @@ test const widthAfterFullScreen = await browserPage.keyDetailsTable.clientWidth; await t.expect(widthAfterFullScreen).gt(widthBeforeFullScreen, 'Width after switching to full screen'); await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('Key Details Table'); - await t.expect(browserPage.keyDetailsValue.withExactText(keyValue).exists).ok('Key Value in Details'); + await t.expect(browserPage.stringKeyValueInput.withExactText(keyValue).exists).ok('Key Value in Details'); // Verify that user can exit full screen in key details and two tables with keys and key details are displayed await t.click(browserPage.fullScreenModeButton); const widthAfterExitFullScreen = await browserPage.keyDetailsTable.clientWidth; diff --git a/tests/e2e/tests/regression/browser/handle-dbsize-permissions.e2e.ts b/tests/e2e/tests/regression/browser/handle-dbsize-permissions.e2e.ts index 711c15d84d..d29f6c411d 100644 --- a/tests/e2e/tests/regression/browser/handle-dbsize-permissions.e2e.ts +++ b/tests/e2e/tests/regression/browser/handle-dbsize-permissions.e2e.ts @@ -9,6 +9,7 @@ import { ossStandaloneNoPermissionsConfig } from '../../../helpers/conf'; import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; const browserPage = new BrowserPage(); const chance = new Chance(); @@ -16,6 +17,7 @@ const cliPage = new CliPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const databaseOverviewPage = new DatabaseOverviewPage(); const bulkActionsPage = new BulkActionsPage(); +const common = new Common(); const createUserCommand = 'acl setuser noperm nopass on +@all ~* -dbsize'; const keyName = chance.word({ length: 20 }); const createKeyCommand = `set ${keyName} ${chance.word({ length: 20 })}`; @@ -30,7 +32,7 @@ fixture `Handle user permissions` // await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); await t.click(myRedisDatabasePage.myRedisDBButton); await addNewStandaloneDatabaseApi(ossStandaloneNoPermissionsConfig); - await t.eval(() => location.reload()); + await common.reloadPage(); }) .afterEach(async() => { // Delete database @@ -51,7 +53,7 @@ test('Verify that user without dbsize permissions can connect to DB', async t => await cliPage.sendCommandInCli(createKeyCommand); await browserPage.searchByKeyName(keyName); await t.expect(browserPage.keysNumberOfResults.textContent).eql('1', 'Found keys number'); - await t.expect(browserPage.keysNumberOfScanned.textContent).contains('18 000', 'Number of scanned'); + await t.expect(browserPage.scannedValue.textContent).contains('18 000', 'Number of scanned'); await t.expect(browserPage.keysTotalNumber.textContent).contains('18 000', 'Number of total keys'); // Check bulk delete await cliPage.sendCommandInCli(createKeyCommand); diff --git a/tests/e2e/tests/regression/browser/keys-all-databases.e2e.ts b/tests/e2e/tests/regression/browser/keys-all-databases.e2e.ts index 99aacdcd03..679f7107a6 100644 --- a/tests/e2e/tests/regression/browser/keys-all-databases.e2e.ts +++ b/tests/e2e/tests/regression/browser/keys-all-databases.e2e.ts @@ -22,7 +22,7 @@ const browserPage = new BrowserPage(); const common = new Common(); let keyName = common.generateWord(10); -const verifyKeysAdded = async() => { +const verifyKeysAdded = async(): Promise => { keyName = common.generateWord(10); //add Hash key await browserPage.addHashKey(keyName); @@ -51,6 +51,7 @@ test await verifyKeysAdded(); }); test + .meta({ rte: rte.reCloud }) .before(async() => { await acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); }) diff --git a/tests/e2e/tests/regression/browser/large-key-details-values.e2e.ts b/tests/e2e/tests/regression/browser/large-key-details-values.e2e.ts index e3d2140c32..09c11994a4 100644 --- a/tests/e2e/tests/regression/browser/large-key-details-values.e2e.ts +++ b/tests/e2e/tests/regression/browser/large-key-details-values.e2e.ts @@ -45,7 +45,7 @@ test('Verify that user can click on a row to expand it if any of its cells conta await t.expect(entryFieldSmall.clientHeight).lt(startSmallCellHeight + 5, 'Row is expanded', { timeout: 5000 }); // Verify that user can expand/collapse for stream data type await t.click(entryFieldLong); - await t.expect(entryFieldLong.clientHeight).gt(startLongCellHeight + 150, 'Row is not expanded', { timeout: 5000 }); + await t.expect(entryFieldLong.clientHeight).gt(startLongCellHeight + 130, 'Row is not expanded', { timeout: 5000 }); // Verify that user can collapse the row by clicking anywhere on the expanded row await t.click(entryFieldLong); await t.expect(entryFieldLong.clientHeight).eql(startLongCellHeight, 'Row is not collapsed', { timeout: 5000 }); @@ -58,7 +58,7 @@ test('Verify that user can expand/collapse for hash data type', async t => { const startCellHeight = await fieldValueCell.clientHeight; // Verify that user can expand a row of hash data type await t.click(fieldValueCell); - await t.expect(fieldValueCell.clientHeight).gt(startCellHeight + 150, 'Row is not expanded', { timeout: 5000 }); + await t.expect(fieldValueCell.clientHeight).gt(startCellHeight + 130, 'Row is not expanded', { timeout: 5000 }); // Verify that user can collapse a row of hash data type await t.click(fieldValueCell); await t.expect(fieldValueCell.clientHeight).eql(startCellHeight, 'Row is not collapsed', { timeout: 5000 }); @@ -71,7 +71,7 @@ test('Verify that user can expand/collapse for set data type', async t => { const startLongCellHeight = await memberValueCell.clientHeight; // Verify that user can expand a row of set data type await t.click(memberValueCell); - await t.expect(memberValueCell.clientHeight).gt(startLongCellHeight + 150, 'Row is not expanded', { timeout: 5000 }); + await t.expect(memberValueCell.clientHeight).gt(startLongCellHeight + 130, 'Row is not expanded', { timeout: 5000 }); // Verify that user can collapse a row of set data type await t.click(memberValueCell); await t.expect(memberValueCell.clientHeight).eql(startLongCellHeight, 'Row is not collapsed', { timeout: 5000 }); @@ -84,7 +84,7 @@ test('Verify that user can expand/collapse for sorted set data type', async t => const startLongCellHeight = await memberValueCell.clientHeight; // Verify that user can expand a row of sorted set data type await t.click(memberValueCell); - await t.expect(memberValueCell.clientHeight).gt(startLongCellHeight + 150, 'Row is not expanded', { timeout: 5000 }); + await t.expect(memberValueCell.clientHeight).gt(startLongCellHeight + 130, 'Row is not expanded', { timeout: 5000 }); // Verify that user can collapse a row of sorted set data type await t.click(memberValueCell); await t.expect(memberValueCell.clientHeight).eql(startLongCellHeight, 'Row is not collapsed', { timeout: 5000 }); @@ -97,7 +97,7 @@ test('Verify that user can expand/collapse for list data type', async t => { const startLongCellHeight = await elementValueCell.clientHeight; // Verify that user can expand a row of list data type await t.click(elementValueCell); - await t.expect(elementValueCell.clientHeight).gt(startLongCellHeight + 150, 'Row is not expanded', { timeout: 5000 }); + await t.expect(elementValueCell.clientHeight).gt(startLongCellHeight + 130, 'Row is not expanded', { timeout: 5000 }); // Verify that user can collapse a row of list data type await t.click(elementValueCell); await t.expect(elementValueCell.clientHeight).eql(startLongCellHeight, 'Row is not collapsed', { timeout: 5000 }); diff --git a/tests/e2e/tests/regression/browser/survey-link.e2e.ts b/tests/e2e/tests/regression/browser/survey-link.e2e.ts new file mode 100644 index 0000000000..cec4a20e7e --- /dev/null +++ b/tests/e2e/tests/regression/browser/survey-link.e2e.ts @@ -0,0 +1,46 @@ +import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { env, rte } from '../../../helpers/constants'; +import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; +import { Common } from '../../../helpers/common'; +import { deleteAllDatabasesApi } from '../../../helpers/api/api-database'; + +const browserPage = new BrowserPage(); +const myRedisDatabasePage = new MyRedisDatabasePage(); +const common = new Common(); +const externalPageLink = 'https://www.surveymonkey.com/r/redisinsight'; + +fixture `User Survey` + .meta({ + type: 'regression', + rte: rte.standalone, + env: env.web + }) + .page(commonUrl) + .beforeEach(async() => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + }); +test('Verify that user can use survey link', async t => { + // Verify that user can see survey link on any page inside of DB + // Browser page + await t.click(browserPage.userSurveyLink); + // Verify that when users click on RI survey, they are redirected to https://www.surveymonkey.com/r/redisinsight + await common.checkURL(externalPageLink); + await t.switchToParentWindow(); + // Workbench page + await t.click(myRedisDatabasePage.workbenchButton); + await t.expect(browserPage.userSurveyLink.visible).ok('Survey Link is not displayed'); + // Slow Log page + await t.click(myRedisDatabasePage.analysisPageButton); + await t.expect(browserPage.userSurveyLink.visible).ok('Survey Link is not displayed'); + // PubSub page + await t.click(myRedisDatabasePage.pubSubButton); + await t.expect(browserPage.userSurveyLink.visible).ok('Survey Link is not displayed'); + // Verify that user cannot see survey link for list of databases page + await t.click(myRedisDatabasePage.myRedisDBButton); + await t.expect(browserPage.userSurveyLink.visible).notOk('Survey Link is visible'); + // Verify that user cannot see survey link for welcome page + await deleteAllDatabasesApi(); + await common.reloadPage(); + await t.expect(browserPage.userSurveyLink.visible).notOk('Survey Link is visible'); +}); diff --git a/tests/e2e/tests/regression/cli/cli-command-helper.e2e.ts b/tests/e2e/tests/regression/cli/cli-command-helper.e2e.ts index 14c9918be7..f18d3764c6 100644 --- a/tests/e2e/tests/regression/cli/cli-command-helper.e2e.ts +++ b/tests/e2e/tests/regression/cli/cli-command-helper.e2e.ts @@ -1,4 +1,3 @@ -import { ClientFunction } from 'testcafe'; import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; import { Common } from '../../../helpers/common'; import { CliPage } from '../../../pageObjects'; @@ -8,9 +7,11 @@ import { } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { CliActions } from '../../../common-actions/cli-actions'; const cliPage = new CliPage(); const common = new Common(); +const cliActions = new CliActions(); let filteringGroup = ''; let filteringGroups: string[] = []; let commandToCheck = ''; @@ -20,8 +21,6 @@ let commandsArgumentsToCheck: string[] = []; let externalPageLink = ''; let externalPageLinks: string[] = []; -const getPageUrl = ClientFunction(() => window.location.href); - fixture `CLI Command helper` .meta({ type: 'regression' }) .page(commonUrl) @@ -102,7 +101,7 @@ test //Click on Read More link for selected command await t.click(cliPage.readMoreButton); //Check new opened window page with the correct URL - await t.expect(getPageUrl()).eql(externalPageLink, 'The opened page'); + await common.checkURL(externalPageLink); await t.switchToParentWindow(); }); test @@ -121,7 +120,7 @@ test //Click on Read More link for selected command await t.click(cliPage.readMoreButton); //Check new opened window page with the correct URL - await t.expect(getPageUrl()).eql(externalPageLink, 'The opened page'); + await common.checkURL(externalPageLink); await t.switchToParentWindow(); }); test @@ -140,7 +139,8 @@ test //Click on Read More link for selected command await t.click(cliPage.readMoreButton); //Check new opened window page with the correct URL - await t.expect(getPageUrl()).eql(externalPageLink, 'The opened page'); + await common.checkURL(externalPageLink); + // await t.expect(getPageUrl()).eql(externalPageLink, 'The opened page'); await t.switchToParentWindow(); }); test @@ -177,7 +177,7 @@ test //Click on Read More link for selected command await t.click(cliPage.readMoreButton); //Check new opened window page with the correct URL - await t.expect(getPageUrl()).eql(externalPageLinks[i], 'The opened page'); + await common.checkURL(externalPageLinks[i]); //Close the window with external link to switch to the application window await t.closeWindow(); i++; @@ -200,7 +200,7 @@ test //Verify that user can use Read More link for Gears group in Command Helper (RedisGears module) await t.click(cliPage.readMoreButton); //Check new opened window page with the correct URL - await t.expect(getPageUrl()).eql(externalPageLink, 'The opened page'); + await common.checkURL(externalPageLink); //Close the window with external link to switch to the application window await t.closeWindow(); }); @@ -241,9 +241,33 @@ test //Verify that user can use Read More link for Bloom, Cuckoo, CMS, TDigest, TopK groups in Command Helper (RedisBloom module). await t.click(cliPage.readMoreButton); //Check new opened window page with the correct URL - await t.expect(getPageUrl()).eql(externalPageLinks[i], 'The opened page'); + await common.checkURL(externalPageLinks[i]); //Close the window with external link to switch to the application window await t.closeWindow(); i++; } }); +test + .meta({ rte: rte.standalone })('Verify that user can go back to list of commands for group in Command Helper', async t => { + filteringGroup = 'Search'; + commandToCheck = 'FT.EXPLAIN'; + const commandForSearch = 'EXPLAIN'; + //Open Command Helper + await t.click(cliPage.expandCommandHelperButton); + //Select one command from the list + await t.typeText(cliPage.cliHelperSearch, commandForSearch); + await cliPage.selectFilterGroupType(filteringGroup); + // Remember found commands + const commandsFilterCount = await cliPage.cliHelperOutputTitles.count; + const filteredCommands: string[] = []; + for (let i = 0; i < commandsFilterCount; i++) { + filteredCommands.push(await cliPage.cliHelperOutputTitles.nth(i).textContent); + } + // Select command + await t.click(cliPage.cliHelperOutputTitles.withExactText(commandToCheck)); + // Click return button + await t.click(cliPage.returnToList); + // Check that user returned to list with filter and search applied + await cliActions.checkCommandsInCommandHelper(filteredCommands); + await t.expect(cliPage.returnToList.exists).notOk('Return to list button still displayed'); + }); diff --git a/tests/e2e/tests/regression/database/database-list-search.e2e.ts b/tests/e2e/tests/regression/database/database-list-search.e2e.ts index f1bf34c216..1ecade23cf 100644 --- a/tests/e2e/tests/regression/database/database-list-search.e2e.ts +++ b/tests/e2e/tests/regression/database/database-list-search.e2e.ts @@ -1,4 +1,3 @@ -import { t } from 'testcafe'; import { acceptLicenseTerms } from '../../../helpers/database'; import { addNewStandaloneDatabasesApi, @@ -11,8 +10,10 @@ import { import { MyRedisDatabasePage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig, ossStandaloneV5Config, ossSentinelConfig, ossClusterConfig } from '../../../helpers/conf'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); +const common = new Common(); const databasesForSearch = [ { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testSearch' }, { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testSecondSearch' }, @@ -36,7 +37,7 @@ fixture `Database list search` await addNewOSSClusterDatabaseApi(ossClusterConfig); await discoverSentinelDatabaseApi(ossSentinelConfig); // Reload Page - await t.eval(() => location.reload()); + await common.reloadPage(); }) .afterEach(async() => { //Clear and delete databases @@ -93,7 +94,7 @@ test('Verify that user can search DB by Last Connection on the List of databases await t.expect(myRedisDatabasePage.dbNameList.withExactText(databasesForSearch[1].databaseName).exists).ok('The database with Last Connection not found', { timeout: 10000 }); //Verify that database added > 1min ago found on the list search by Last Connection do { - await t.eval(() => location.reload()); + await common.reloadPage(); await t.typeText(myRedisDatabasePage.searchInput, searchedDBSecond, { replace: true }); } while (!(await dbSelector.exists) && Date.now() - startTime < searchTimeout); diff --git a/tests/e2e/tests/regression/database/database-sorting.e2e.ts b/tests/e2e/tests/regression/database/database-sorting.e2e.ts index 6d4312e9a6..b079dc3330 100644 --- a/tests/e2e/tests/regression/database/database-sorting.e2e.ts +++ b/tests/e2e/tests/regression/database/database-sorting.e2e.ts @@ -1,4 +1,3 @@ -import { t } from 'testcafe'; import { acceptLicenseTerms } from '../../../helpers/database'; import { discoverSentinelDatabaseApi, @@ -14,9 +13,11 @@ import { ossSentinelConfig, ossClusterConfig } from '../../../helpers/conf'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const browserPage = new BrowserPage(); +const common = new Common(); const databases = [ { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: ossStandaloneConfig.databaseName }, { host: ossClusterConfig.ossClusterHost, port: ossClusterConfig.ossClusterPort, databaseName: ossClusterConfig.ossClusterDatabaseName }, @@ -27,7 +28,7 @@ const oldDBName = ossStandaloneConfig.databaseName; const newDBName = '! Edited Standalone DB name'; const sortList = async(): Promise => { const sortedByName = databases.sort((a, b) => a.databaseName > b.databaseName ? 1 : -1); - const sortedDatabaseNames = []; + const sortedDatabaseNames: string[] = []; for (let i = 0; i < sortedByName.length; i++) { sortedDatabaseNames.push(sortedByName[i].databaseName); } @@ -46,7 +47,7 @@ fixture `Remember database sorting` await addNewOSSClusterDatabaseApi(ossClusterConfig); await discoverSentinelDatabaseApi(ossSentinelConfig, 1); // Reload Page - await t.eval(() => location.reload()); + await common.reloadPage(); }) .afterEach(async() => { // Clear and delete databases @@ -72,7 +73,7 @@ test('Verify that sorting on the list of databases saved when database opened', const sortedDatabaseHost = [ossClusterConfig.ossClusterDatabaseName, ossSentinelConfig.name[0], ossStandaloneConfig.databaseName]; await myRedisDatabasePage.compareDatabases(actualDatabaseList, sortedDatabaseHost); // Verify that sorting on the list of databases saved when databases list refreshed - await t.eval(() => location.reload()); + await common.reloadPage(); actualDatabaseList = await myRedisDatabasePage.getAllDatabases(); await myRedisDatabasePage.compareDatabases(actualDatabaseList, sortedDatabaseHost); }); diff --git a/tests/e2e/tests/regression/database/github.e2e.ts b/tests/e2e/tests/regression/database/github.e2e.ts index 46695c0ebc..4c686ba89f 100644 --- a/tests/e2e/tests/regression/database/github.e2e.ts +++ b/tests/e2e/tests/regression/database/github.e2e.ts @@ -4,18 +4,20 @@ import { acceptLicenseTerms } from '../../../helpers/database'; import {MyRedisDatabasePage} from '../../../pageObjects'; import {commonUrl, ossStandaloneConfig} from '../../../helpers/conf'; import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); +const common = new Common(); const getPageUrl = ClientFunction(() => window.location.href); fixture `Github functionality` .meta({ type: 'regression' }) .page(commonUrl) - .beforeEach(async t => { + .beforeEach(async () => { await acceptLicenseTerms(); await addNewStandaloneDatabaseApi(ossStandaloneConfig); // Reload Page - await t.eval(() => location.reload()); + await common.reloadPage(); }) .afterEach(async() => { //Delete database diff --git a/tests/e2e/tests/regression/database/overview.e2e.ts b/tests/e2e/tests/regression/database/overview.e2e.ts index cb1b3f86c0..9ead2b58fa 100644 --- a/tests/e2e/tests/regression/database/overview.e2e.ts +++ b/tests/e2e/tests/regression/database/overview.e2e.ts @@ -1,24 +1,16 @@ -import { t } from 'testcafe'; import { rte } from '../../../helpers/constants'; -import {acceptLicenseTerms, deleteDatabase} from '../../../helpers/database'; -import { BrowserPage, AddRedisDatabasePage, MyRedisDatabasePage, DatabaseOverviewPage } from '../../../pageObjects'; +import { acceptLicenseTermsAndAddRECloudDatabase, deleteDatabase } from '../../../helpers/database'; +import { BrowserPage, DatabaseOverviewPage } from '../../../pageObjects'; import { commonUrl, cloudDatabaseConfig } from '../../../helpers/conf'; const browserPage = new BrowserPage(); -const addRedisDatabasePage = new AddRedisDatabasePage(); -const myRedisDatabasePage = new MyRedisDatabasePage(); const databaseOverviewPage = new DatabaseOverviewPage(); fixture `Overview` .meta({ type: 'regression' }) .page(commonUrl) .beforeEach(async() => { - await acceptLicenseTerms(); - await addRedisDatabasePage.addRedisDataBase(cloudDatabaseConfig); - //Click for saving - await t.click(addRedisDatabasePage.addRedisDatabaseButton); - await t.expect(myRedisDatabasePage.dbNameList.withExactText(cloudDatabaseConfig.databaseName).exists).ok('The existence of the database', { timeout: 5000 }); - await myRedisDatabasePage.clickOnDBByName(cloudDatabaseConfig.databaseName); + await acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); }) .afterEach(async() => { //Delete database diff --git a/tests/e2e/tests/regression/database/redisstack.e2e.ts b/tests/e2e/tests/regression/database/redisstack.e2e.ts index 6a31bb46ff..b32a1ca105 100644 --- a/tests/e2e/tests/regression/database/redisstack.e2e.ts +++ b/tests/e2e/tests/regression/database/redisstack.e2e.ts @@ -3,20 +3,22 @@ import { acceptLicenseTerms } from '../../../helpers/database'; import { MyRedisDatabasePage, DatabaseOverviewPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const databaseOverviewPage = new DatabaseOverviewPage(); +const common = new Common(); const moduleNameList = ['RediSearch', 'RedisGraph', 'RedisBloom', 'RedisJSON', 'RedisTimeSeries']; fixture `Redis Stack` .meta({type: 'regression'}) .page(commonUrl) - .beforeEach(async t => { + .beforeEach(async () => { // Add new databases using API await acceptLicenseTerms(); await addNewStandaloneDatabaseApi(ossStandaloneConfig); // Reload Page - await t.eval(() => location.reload()); + await common.reloadPage(); }) .afterEach(async() => { //Delete database diff --git a/tests/e2e/tests/regression/monitor/monitor.e2e.ts b/tests/e2e/tests/regression/monitor/monitor.e2e.ts index 3d9c52f3a3..138a0fd479 100644 --- a/tests/e2e/tests/regression/monitor/monitor.e2e.ts +++ b/tests/e2e/tests/regression/monitor/monitor.e2e.ts @@ -15,6 +15,7 @@ import { } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const monitorPage = new MonitorPage(); @@ -22,6 +23,7 @@ const settingsPage = new SettingsPage(); const browserPage = new BrowserPage(); const cliPage = new CliPage(); const chance = new Chance(); +const common = new Common(); fixture `Monitor` .meta({ type: 'regression' }) @@ -66,7 +68,7 @@ test //Run monitor await monitorPage.startMonitor(); //Refresh the page - await t.eval(() => location.reload()); + await common.reloadPage(); //Check that monitor is closed await t.expect(monitorPage.monitorArea.exists).notOk('Monitor area'); //Check that monitor area doesn't have any saved results @@ -129,7 +131,7 @@ test await deleteStandaloneDatabaseApi(ossStandaloneConfig); await t.click(myRedisDatabasePage.myRedisDBButton); await addNewStandaloneDatabaseApi(ossStandaloneNoPermissionsConfig); - await t.eval(() => location.reload()); + await common.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneNoPermissionsConfig.databaseName); }) .after(async() => { diff --git a/tests/e2e/tests/regression/pub-sub/pub-sub-oss-cluster-7.ts b/tests/e2e/tests/regression/pub-sub/pub-sub-oss-cluster-7.ts index 997f0c4378..8f68a6f08d 100644 --- a/tests/e2e/tests/regression/pub-sub/pub-sub-oss-cluster-7.ts +++ b/tests/e2e/tests/regression/pub-sub/pub-sub-oss-cluster-7.ts @@ -9,10 +9,12 @@ import { addNewOSSClusterDatabaseApi, addNewStandaloneDatabaseApi, deleteOSSClusterDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const pubSubPage = new PubSubPage(); const cliPage = new CliPage(); +const common = new Common(); fixture `PubSub OSS Cluster 7 tests` .meta({ env: env.web, type: 'regression' }) @@ -22,7 +24,7 @@ test .before(async t => { await acceptLicenseTerms(); await addNewOSSClusterDatabaseApi(ossClusterConfig); - await t.eval(() => location.reload()); + await common.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossClusterConfig.ossClusterDatabaseName); await t.click(myRedisDatabasePage.pubSubButton); }) @@ -44,7 +46,7 @@ test .before(async t => { await acceptLicenseTerms(); await addNewStandaloneDatabaseApi(ossStandaloneConfig); - await t.eval(() => location.reload()); + await common.reloadPage(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await t.click(myRedisDatabasePage.pubSubButton); }) diff --git a/tests/e2e/tests/regression/tree-view/tree-view.e2e.ts b/tests/e2e/tests/regression/tree-view/tree-view.e2e.ts index 16dc304cd5..ef2efa0138 100644 --- a/tests/e2e/tests/regression/tree-view/tree-view.e2e.ts +++ b/tests/e2e/tests/regression/tree-view/tree-view.e2e.ts @@ -1,17 +1,18 @@ import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; -import { BrowserPage } from '../../../pageObjects'; +import { BrowserPage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../../helpers/conf'; -import { rte } from '../../../helpers/constants'; +import { KeyTypesTexts, rte } from '../../../helpers/constants'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); +const workbenchPage = new WorkbenchPage(); fixture `Tree view verifications` - .meta({type: 'regression'}) + .meta({type: 'regression', rte: rte.standalone}) .page(commonUrl) .beforeEach(async() => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); @@ -21,7 +22,6 @@ fixture `Tree view verifications` await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); }); test - .meta({ rte: rte.standalone }) .before(async() => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); }) @@ -29,13 +29,16 @@ test //Delete database await deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify that user can see message "No keys to display." when there are no keys in the database', async t => { + const message = 'No keys to display.Use Workbench Guides and Tutorials to quickly load the data.' //Verify the message await t.click(browserPage.treeViewButton); - await t.expect(browserPage.keyListTable.textContent).contains('No keys to display.', 'The message is displayed'); + await t.expect(browserPage.keyListMessage.textContent).contains(message, 'The message is displayed'); + // Verify that workbench opened by clicking on "Use Workbench Guides and Tutorials" link + await t.click(browserPage.workbenchLinkButton); + await t.expect(workbenchPage.expandArea.visible).ok('Workbench page is not opened'); }); -//skipped due the issue -test.skip - .meta({ rte: rte.standalone })('Verify that user can see the total number of keys, the number of keys scanned, the “Scan more” control displayed at the top of Tree view and Browser view', async t => { +test('Verify that user can see the total number of keys, the number of keys scanned, the “Scan more” control displayed at the top of Tree view and Browser view', async t => { + await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); //Verify the controls on the Browser view await t.expect(browserPage.totalKeysNumber.visible).ok('The total number of keys is displayed on the Browser view'); await t.expect(browserPage.scannedValue.visible).ok('The number of keys scanned is displayed on the Browser view'); @@ -46,8 +49,7 @@ test.skip await t.expect(browserPage.scannedValue.visible).ok('The number of keys scanned is displayed on the Tree view'); await t.expect(browserPage.scanMoreButton.visible).ok('The scan more button is displayed on the Tree view'); }); -test - .meta({ rte: rte.standalone })('Verify that when user deletes the key he can see the key is removed from the folder, the number of keys is reduced, the percentage is recalculated', async t => { +test('Verify that when user deletes the key he can see the key is removed from the folder, the number of keys is reduced, the percentage is recalculated', async t => { //Open the first key in the tree view and remove await t.click(browserPage.treeViewButton); await t.expect(browserPage.treeViewDeviceFolder.visible).ok('The key folder is displayed', { timeout: 30000 }); @@ -61,8 +63,7 @@ test await t.expect(browserPage.treeViewDeviceFolder.nth(2).textContent).notEql(keyFolder, 'The key folder is removed from the tree view'); await t.expect(browserPage.treeViewDeviceKyesCount.textContent).notEql(numberOfKeys, 'The number of keys is recalculated'); }); -test - .meta({ rte: rte.standalone })('Verify that user can see that “:” (colon) used as a default separator for namespaces and see the number of keys found per each namespace', async t => { +test('Verify that user can see that “:” (colon) used as a default separator for namespaces and see the number of keys found per each namespace', async t => { await t.click(browserPage.treeViewButton); //Verify the default separator await t.expect(browserPage.treeViewSeparator.textContent).eql(':', 'The “:” (colon) used as a default separator for namespaces'); diff --git a/tests/e2e/tests/regression/workbench/command-results.e2e.ts b/tests/e2e/tests/regression/workbench/command-results.e2e.ts index 4613c8dee2..26f492af6c 100644 --- a/tests/e2e/tests/regression/workbench/command-results.e2e.ts +++ b/tests/e2e/tests/regression/workbench/command-results.e2e.ts @@ -1,83 +1,81 @@ +import { Chance } from 'chance'; import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; import { WorkbenchPage } from '../../../pageObjects/workbench-page'; import { MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, - ossStandaloneConfig + ossStandaloneRedisearch } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; -import { Chance } from 'chance'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const chance = new Chance(); -let indexName = chance.word({ length: 5 }); -let commandsForIndex = [ +const indexName = chance.word({ length: 5 }); +const commandsForIndex = [ `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA price NUMERIC SORTABLE`, 'HMSET product:1 price 20', 'HMSET product:2 price 100' ]; -//skipped due the inaccessibility of the iframe +// skip due to errors of FT.SEARCH issue https://redislabs.atlassian.net/browse/RI-3501 fixture.skip `Command results at Workbench` - .meta({type: 'regression'}) + .meta({type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); //Add index and data await t.click(myRedisDatabasePage.workbenchButton); await workbenchPage.sendCommandsArrayInWorkbench(commandsForIndex); }) - .afterEach(async() => { + .afterEach(async t => { //Drop index and database + await t.switchToMainWindow(); await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - }) + await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + }); test - .meta({ env: env.web, rte: rte.standalone }) - ('Verify that user can switches between Table and Text for FT.INFO and see results corresponding to their views', async t => { - indexName = chance.word({ length: 5 }); + .meta({ env: env.web })('Verify that user can switches between Table and Text for FT.INFO and see results corresponding to their views', async t => { + // indexName = chance.word({ length: 5 }); const infoCommand = `FT.INFO ${indexName}`; //Send FT.INFO and switch to Text view await workbenchPage.sendCommandInWorkbench(infoCommand); await workbenchPage.selectViewTypeText(); - await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).exists).ok(`The text view is switched for command FT.INFO`); + await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).exists).ok('The text view is not switched for command FT.INFO'); //Switch to Table view and check result await workbenchPage.selectViewTypeTable(); - await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTableResult).exists).ok(`The table view is switched for command FT.INFO`); + await t.switchToIframe(workbenchPage.iframe); + await t.expect(await workbenchPage.queryTableResult.exists).ok('The table view is not switched for command FT.INFO'); }); test - .meta({ env: env.web, rte: rte.standalone }) - ('Verify that user can switches between Table and Text for FT.SEARCH and see results corresponding to their views', async t => { - indexName = chance.word({ length: 5 }); + .meta({ env: env.web })('Verify that user can switches between Table and Text for FT.SEARCH and see results corresponding to their views', async t => { const searchCommand = `FT.SEARCH ${indexName} *`; //Send FT.SEARCH and switch to Text view await workbenchPage.sendCommandInWorkbench(searchCommand); await workbenchPage.selectViewTypeText(); - await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).visible).ok(`The text view is switched for command FT.SEARCH`); + await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).visible).ok('The text view is not switched for command FT.SEARCH'); //Switch to Table view and check result await workbenchPage.selectViewTypeTable(); - await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTableResult).exists).ok(`The table view is switched for command FT.SEARCH`); + await t.switchToIframe(workbenchPage.iframe); + await t.expect(await workbenchPage.queryTableResult.exists).ok('The table view is not switched for command FT.SEARCH'); }); test - .meta({ env: env.web, rte: rte.standalone }) - ('Verify that user can switches between Table and Text for FT.AGGREGATE and see results corresponding to their views', async t => { - indexName = chance.word({ length: 5 }); + .meta({ env: env.web })('Verify that user can switches between Table and Text for FT.AGGREGATE and see results corresponding to their views', async t => { const aggregateCommand = `FT.Aggregate ${indexName} * GROUPBY 0 REDUCE MAX 1 @price AS max_price`; //Send FT.AGGREGATE and switch to Text view await workbenchPage.sendCommandInWorkbench(aggregateCommand); await workbenchPage.selectViewTypeText(); - await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).visible).ok(`The text view is switched for command FT.AGGREGATE`); + await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTextResult).visible).ok('The text view is not switched for command FT.AGGREGATE'); //Switch to Table view and check result await workbenchPage.selectViewTypeTable(); - await t.expect(await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssQueryTableResult).exists).ok(`The table view is switched for command FT.AGGREGATE`); + await t.switchToIframe(workbenchPage.iframe); + await t.expect(await workbenchPage.queryTableResult.exists).ok('The table view is not switched for command FT.AGGREGATE'); }); -test - .meta({ env: env.web, rte: rte.standalone }) - ('Verify that user can switches between views and see results according to this view in full mode in Workbench', async t => { - indexName = chance.word({ length: 5 }); +// Skipped due to issue https://redislabs.atlassian.net/browse/RI-3524 +test.skip + .meta({ env: env.web })('Verify that user can switches between views and see results according to this view in full mode in Workbench', async t => { const command = 'CLIENT LIST'; //Send command and check table view is default in full mode await workbenchPage.sendCommandInWorkbench(command); diff --git a/tests/e2e/tests/regression/workbench/context.e2e.ts b/tests/e2e/tests/regression/workbench/context.e2e.ts index 360be3db6f..24b8e5d8d1 100644 --- a/tests/e2e/tests/regression/workbench/context.e2e.ts +++ b/tests/e2e/tests/regression/workbench/context.e2e.ts @@ -3,10 +3,12 @@ import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; import { MyRedisDatabasePage, CliPage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const cliPage = new CliPage(); +const common = new Common(); const speed = 0.4; @@ -63,7 +65,7 @@ test await t.expect(await cliPage.cliCollapseButton.exists).ok('CLI is still expanded'); await t.expect(await workbenchPage.queryInputScriptArea.textContent).eql(command, 'Input in Editor is saved'); //Reload the window and chek context - await t.eval(() => location.reload()); + await common.reloadPage(); await t.expect(await cliPage.cliCollapseButton.exists).notOk('CLI is collapsed'); await t.expect(await workbenchPage.queryInputScriptArea.textContent).eql('', 'Input in Editor is removed'); }); diff --git a/tests/e2e/tests/regression/workbench/default-scripts-area.e2e.ts b/tests/e2e/tests/regression/workbench/default-scripts-area.e2e.ts index 58dd83583d..7c88505393 100644 --- a/tests/e2e/tests/regression/workbench/default-scripts-area.e2e.ts +++ b/tests/e2e/tests/regression/workbench/default-scripts-area.e2e.ts @@ -81,6 +81,7 @@ test.skip .meta({ rte: rte.standalone }) ('Verify that user can see saved scroll position in Enablement area when he leaves Workbench page and goes back again', async t => { //Open Working with Hashes section + await t.click(workbenchPage.documentButtonInQuickGuides); await t.click(workbenchPage.internalLinkWorkingWithHashes); //Evaluate the last button in Enablement Area const buttonsQuantity = await workbenchPage.preselectButtons.count; @@ -94,13 +95,13 @@ test.skip //Go back to Workbench page await t.click(myRedisDatabasePage.workbenchButton); //Check that scroll position is saved - await t.expect(workbenchPage.scrolledEnablementArea.scrollTop).eql(scrollPosition, 'The scroll position status'); + await t.expect(await workbenchPage.scrolledEnablementArea.scrollTop).eql(scrollPosition, 'The scroll position status'); //Go to list of DBs page await t.click(myRedisDatabasePage.myRedisDBButton); //Go back to active DB again await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); //Check that scroll position is saved - await t.expect(workbenchPage.scrolledEnablementArea.scrollTop).eql(scrollPosition, 'Scroll position is correct'); + await t.expect(await workbenchPage.scrolledEnablementArea.scrollTop).eql(scrollPosition, 'Scroll position is correct'); }); test .meta({ rte: rte.standalone }) diff --git a/tests/e2e/tests/regression/workbench/editor-cleanup.e2e.ts b/tests/e2e/tests/regression/workbench/editor-cleanup.e2e.ts new file mode 100644 index 0000000000..24e0ff310c --- /dev/null +++ b/tests/e2e/tests/regression/workbench/editor-cleanup.e2e.ts @@ -0,0 +1,84 @@ +import { acceptLicenseTerms, acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { WorkbenchPage, MyRedisDatabasePage, SettingsPage } from '../../../pageObjects'; +import { rte } from '../../../helpers/constants'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; +import { addNewStandaloneDatabasesApi, deleteStandaloneDatabaseApi, deleteStandaloneDatabasesApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; + +const myRedisDatabasePage = new MyRedisDatabasePage(); +const workbenchPage = new WorkbenchPage(); +const common = new Common(); +const settingsPage = new SettingsPage(); + +const commandToSend = 'info server'; +const databasesForAdding = [ + { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testDB1' }, + { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testDB2' } +]; + +fixture `Workbench Editor Cleanup` + .meta({ type: 'critical_path', rte: rte.standalone }) + .page(commonUrl) + .beforeEach(async() => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + }) + .afterEach(async() => { + // Clear and delete database + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + }); +test('Disabled Editor Cleanup toggle behavior', async t => { + // Go to Settings page + await t.click(myRedisDatabasePage.settingsButton); + await t.click(settingsPage.accordionWorkbenchSettings); + // Disable Editor Cleanup + await t.click(settingsPage.switchEditorCleanupOption); + // Verify that user can see text "Clear the Editor after running commands" for Editor Cleanup In Settings + await t.expect(settingsPage.switchEditorCleanupOption.sibling(0).withExactText('Clear the Editor after running commands').visible).ok('Cleanup text is not correct'); + // Go to Workbench page + await t.click(myRedisDatabasePage.workbenchButton); + // Send commands + await workbenchPage.sendCommandInWorkbench(commandToSend); + await workbenchPage.sendCommandInWorkbench(commandToSend); + // Verify that Editor input is not affected after user running command + await t.expect((await workbenchPage.queryInputScriptArea.textContent).replace(/\s/g, ' ')).eql(commandToSend, 'Input in Editor is saved'); +}); +test('Enabled Editor Cleanup toggle behavior', async t => { + // Go to Workbench page + await t.click(myRedisDatabasePage.workbenchButton); + // Send commands + await workbenchPage.sendCommandInWorkbench(commandToSend); + await workbenchPage.sendCommandInWorkbench(commandToSend); + // Verify that Editor input is cleared after running command + await t.expect(await workbenchPage.queryInputScriptArea.textContent).eql('', 'Input in Editor is saved'); +}); +test + .before(async() => { + // Add new databases using API + await acceptLicenseTerms(); + await addNewStandaloneDatabasesApi(databasesForAdding); + // Reload Page + await common.reloadPage(); + await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName); + }) + .after(async() => { + // Clear and delete database + await deleteStandaloneDatabasesApi(databasesForAdding); + })('Editor Cleanup settings', async t => { + // Go to Settings page + await t.click(myRedisDatabasePage.settingsButton); + await t.click(settingsPage.accordionWorkbenchSettings); + // Disable Editor Cleanup + await settingsPage.changeEditorCleanupSwitcher(false); + await common.reloadPage(); + await t.click(settingsPage.accordionWorkbenchSettings); + // Verify that Editor Cleanup setting is saved when refreshing the page + await t.expect(await settingsPage.getEditorCleanupSwitcherValue()).eql('false', 'Editor Cleanup switcher changed'); + // Go to another database + await t.click(myRedisDatabasePage.myRedisDBButton); + await myRedisDatabasePage.clickOnDBByName(databasesForAdding[1].databaseName); + // Go to Settings page + await t.click(myRedisDatabasePage.settingsButton); + await t.click(settingsPage.accordionWorkbenchSettings); + // Verify that Editor Cleanup setting is saved when switching between databases + await t.expect(await settingsPage.getEditorCleanupSwitcherValue()).eql('false', 'Editor Cleanup switcher changed'); + }); diff --git a/tests/e2e/tests/regression/workbench/group-mode.e2e.ts b/tests/e2e/tests/regression/workbench/group-mode.e2e.ts new file mode 100644 index 0000000000..eb568edbc3 --- /dev/null +++ b/tests/e2e/tests/regression/workbench/group-mode.e2e.ts @@ -0,0 +1,68 @@ +import { Selector } from 'testcafe'; +import { rte } from '../../../helpers/constants'; +import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; +import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; +import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; + +const myRedisDatabasePage = new MyRedisDatabasePage(); +const workbenchPage = new WorkbenchPage(); +const counter = 7; +const command = 'info'; +const commands = ['set key test', 'get key', 'del key']; +const commandsResult = ['OK', 'test', '1']; +const commandsNumber = commands.length; +const commandsString = commands.join('\n'); + +fixture `Workbench Group Mode` + .meta({ rte: rte.standalone, type: 'regression' }) + .page(commonUrl) + .beforeEach(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + //Go to Workbench page + await t.click(myRedisDatabasePage.workbenchButton); + }) + .afterEach(async() => { + //Delete database + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + }); +test('Verify that user can run the commands from the Editor in the group mode', async t => { + await t.click(workbenchPage.groupMode); + // Verify that user can run a command with quantifier and see results in group(10 info) + await workbenchPage.sendCommandInWorkbench(`${counter} ${command}`); + // Verify that user can see number of total commands in group, success commands, number of failed commands in header summary in Workbench + await t.expect(workbenchPage.queryCardCommand.textContent).eql(`${counter} Command(s) - ${counter} success, 0 error(s)`, 'Not valid summary'); + // Verify that user can see full list of commands with results run in group + await t.expect(workbenchPage.queryTextResult.find(workbenchPage.cssWorkbenchCommandInHistory).withText(`> ${command}`).count).eql(counter, 'Number of commands is not correct'); + await t.expect(workbenchPage.queryTextResult.find(workbenchPage.cssWorkbenchCommandSuccessResultInHistory).count).eql(counter, 'Number of command result is not correct'); + // Verify that if the only one command is executed in group, the result will be displayed as for group mode + await workbenchPage.sendCommandInWorkbench(`${command}`); + await t.expect(workbenchPage.queryCardCommand.textContent).eql('1 Command(s) - 1 success, 0 error(s)', 'Not valid summary for 1 command'); + // Turn off group mode + await t.click(workbenchPage.groupMode); + await workbenchPage.sendCommandInWorkbench(commandsString); + await t.expect(workbenchPage.queryCardCommand.textContent).notEql(`${commandsNumber} Command(s) - ${commandsNumber} success, 0 error(s)`, 'Commands are sent in groups'); + for (let i = 0; i++; i < commandsNumber) { + await workbenchPage.checkWorkbenchCommandResult(command[i], commandsResult[i], i); + } +}); +// Skip due to testcafe doesn't work with clipboard buffer. Need to add client function to check this test +test.skip('Verify that when user clicks on copy icon for group result, all commands are copied', async t => { + await t.click(workbenchPage.groupMode); + await workbenchPage.sendCommandInWorkbench(`${commandsString}`); // 3 commands are sent in group mode + // Copy commands from group result + await t.click(workbenchPage.copyCommand); + await t.rightClick(workbenchPage.queryInputScriptArea); + await t.click(Selector('span').withAttribute('aria-label', 'Paste')); + await t.pressKey('ctrl+enter'); + await t.expect(workbenchPage.queryCardCommand.textContent).eql(`${commandsNumber} Command(s) - ${commandsNumber} success, 0 error(s)`, 'Not valid summary'); +}); +test('Verify that user can see group results in full mode', async t => { + await t.click(workbenchPage.groupMode); + await workbenchPage.sendCommandInWorkbench(`${commandsString}`); // 3 commands are sent in group mode + // Open full mode + await t.click(workbenchPage.fullScreenButton); + await t.expect(workbenchPage.queryCardCommand.textContent).eql(`${commandsNumber} Command(s) - ${commandsNumber} success, 0 error(s)`, 'Not valid summary'); + await t.expect(workbenchPage.queryTextResult.find(workbenchPage.cssWorkbenchCommandInHistory).withText('> ').count).eql(commandsNumber, 'Number of commands is not correct'); + await t.expect(workbenchPage.queryTextResult.find(workbenchPage.cssWorkbenchCommandSuccessResultInHistory).count).eql(commandsNumber, 'Number of command result is not correct'); +}); diff --git a/tests/e2e/tests/regression/workbench/history-of-results.e2e.ts b/tests/e2e/tests/regression/workbench/history-of-results.e2e.ts index d5c2fff61f..26c4b99cf7 100644 --- a/tests/e2e/tests/regression/workbench/history-of-results.e2e.ts +++ b/tests/e2e/tests/regression/workbench/history-of-results.e2e.ts @@ -5,11 +5,13 @@ import { MyRedisDatabasePage, WorkbenchPage, CliPage } from '../../../pageObject import { rte } from '../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const chance = new Chance(); const cliPage = new CliPage(); +const common = new Common(); const oneMinuteTimeout = 60000; let keyName = chance.word({ length: 10 }); @@ -36,7 +38,7 @@ test const dateTime = await workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssCommandExecutionDateTime).textContent; //Wait fo 1 minute, refresh page and check results await t.wait(oneMinuteTimeout); - await t.eval(() => location.reload()); + await common.reloadPage(); await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssCommandExecutionDateTime).textContent).eql(dateTime, 'The original date and time of command execution is saved after the page update'); }); //skipped due the long time execution and hangs of test @@ -53,7 +55,7 @@ test.skip await workbenchPage.sendCommandInWorkbench(`${commandToSend} "${commandText}"`); await workbenchPage.sendCommandInWorkbench(commandToGet); //Refresh the page and check result - await t.eval(() => location.reload()); + await common.reloadPage(); await t.click(workbenchPage.queryCardContainer.withText(commandToGet)); await t.expect(workbenchPage.queryTextResult.textContent).eql('"Results have been deleted since they exceed 1 MB. Re-run the command to see new results."', 'The messageis displayed'); }); diff --git a/tests/e2e/tests/regression/workbench/raw-mode.e2e.ts b/tests/e2e/tests/regression/workbench/raw-mode.e2e.ts new file mode 100644 index 0000000000..2f428e9866 --- /dev/null +++ b/tests/e2e/tests/regression/workbench/raw-mode.e2e.ts @@ -0,0 +1,117 @@ +import { acceptLicenseTerms, acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../pageObjects'; +import { env, rte } from '../../../helpers/constants'; +import { commonUrl, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../helpers/conf'; +import { addNewStandaloneDatabasesApi, deleteStandaloneDatabaseApi, deleteStandaloneDatabasesApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; + +const myRedisDatabasePage = new MyRedisDatabasePage(); +const workbenchPage = new WorkbenchPage(); +const common = new Common(); +const browserPage = new BrowserPage(); + +let keyName = common.generateWord(10); +const indexName = common.generateWord(5); +const keyValue = '\\xe5\\xb1\\xb1\\xe5\\xa5\\xb3\\xe9\\xa6\\xac / \\xe9\\xa9\\xac\\xe7\\x9b\\xae abc 123'; +const unicodeValue = '山女馬 / 马目 abc 123'; +const rawModeIcon = '-r'; +const commandsForSend = [ + `set ${keyName} "${keyValue}"`, + `get ${keyName}` +]; +const databasesForAdding = [ + { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testDB1' }, + { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testDB2' } +]; + +fixture `Workbench Raw mode` + .meta({ type: 'critical_path', rte: rte.standalone }) + .page(commonUrl) + .beforeEach(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + // Go to Workbench page + await t.click(myRedisDatabasePage.workbenchButton); + }) + .afterEach(async t => { + // Clear and delete database + await t.click(myRedisDatabasePage.browserButton); + await browserPage.deleteKeyByName(keyName); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + }); +test('Use raw mode for Workbech result', async t => { + // Send commands + await workbenchPage.sendCommandsArrayInWorkbench(commandsForSend); + // Display result in Ascii when raw mode is off + await t.expect(workbenchPage.queryTextResult.textContent).contains(`"${keyValue}"`, 'The result is not correct'); + // Verify that user can't see Raw marker in Workbench command history + await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssCommandExecutionDateTime).textContent) + .notContains(rawModeIcon, 'Raw mode icon is displayed in command history'); + //Send command in raw mode + await t.click(workbenchPage.rawModeBtn); + await workbenchPage.sendCommandInWorkbench(commandsForSend[1]); + // Verify that user can see command result execution in raw mode + await workbenchPage.checkWorkbenchCommandResult(commandsForSend[1], `"${unicodeValue}"`); + // Verify that user can see R marker in command history + await t.expect(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssCommandExecutionDateTime).textContent) + .contains(rawModeIcon, 'No raw mode icon in command history'); +}); +test + .before(async t => { + // Add new databases using API + await acceptLicenseTerms(); + await addNewStandaloneDatabasesApi(databasesForAdding); + // Reload Page + await common.reloadPage(); + await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName); + // Go to Workbench page + await t.click(myRedisDatabasePage.workbenchButton); + }) + .after(async t => { + // Clear and delete database + await deleteStandaloneDatabasesApi(databasesForAdding); + })('Save Raw mode state', async t => { + //Send command in raw mode + await t.click(workbenchPage.rawModeBtn); + await workbenchPage.sendCommandsArrayInWorkbench(commandsForSend); + // Verify that user can see saved Raw mode state after page refresh + await common.reloadPage(); + await workbenchPage.sendCommandInWorkbench(commandsForSend[1]); + await workbenchPage.checkWorkbenchCommandResult(commandsForSend[1], `"${unicodeValue}"`); + // Go to another database + await t.click(myRedisDatabasePage.myRedisDBButton); + await myRedisDatabasePage.clickOnDBByName(databasesForAdding[1].databaseName); + // Go to Workbench page + await t.click(myRedisDatabasePage.workbenchButton); + // Verify that user can see saved Raw mode state after re-connection to another DB + await workbenchPage.sendCommandInWorkbench(commandsForSend[1]); + await workbenchPage.checkWorkbenchCommandResult(commandsForSend[1], `"${unicodeValue}"`); + // Verify that currently selected mode is applied when User re-run the command from history + await t.click(workbenchPage.queryCardContainer.nth(0).find(workbenchPage.cssReRunCommandButton)); + await workbenchPage.checkWorkbenchCommandResult(commandsForSend[1], `"${unicodeValue}"`); + }); +test + .meta({ env: env.desktop }) + .before(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); + // Go to Workbench page + await t.click(myRedisDatabasePage.workbenchButton); + }) + .after(async t => { + //Drop index, documents and database + await t.switchToMainWindow(); + await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); + await deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + })('Display Raw mode for plugins', async t => { + const commandsForSend = [ + `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`, + `HMSET product:1 name "${unicodeValue}"`, + `FT.SEARCH ${indexName} "${unicodeValue}"` + ]; + //Send command in raw mode + await t.click(workbenchPage.rawModeBtn); + await workbenchPage.sendCommandsArrayInWorkbench(commandsForSend); + //Check the FT.SEARCH result + await t.switchToIframe(workbenchPage.iframe); + const name = workbenchPage.queryTableResult.withText(unicodeValue); + await t.expect(name.exists).ok('The added key name field is not converted to Unicode'); + }); diff --git a/tests/e2e/tests/regression/workbench/scripting-area.e2e.ts b/tests/e2e/tests/regression/workbench/scripting-area.e2e.ts index 9ba5292a8d..4a3ada6189 100644 --- a/tests/e2e/tests/regression/workbench/scripting-area.e2e.ts +++ b/tests/e2e/tests/regression/workbench/scripting-area.e2e.ts @@ -15,7 +15,7 @@ const settingsPage = new SettingsPage(); const indexName = chance.word({ length: 5 }); let keyName = chance.word({ length: 10 }); -fixture`Scripting area at Workbench` +fixture `Scripting area at Workbench` .meta({ type: 'regression' }) .page(commonUrl) .beforeEach(async t => { @@ -40,7 +40,7 @@ test //Go to Settings page await t.click(myRedisDatabasePage.settingsButton); //Specify Commands in pipeline - await t.click(settingsPage.accordionAdvancedSettings); + await t.click(settingsPage.accordionWorkbenchSettings); await settingsPage.changeCommandsInPipeline('1'); //Go to Workbench page await t.click(myRedisDatabasePage.workbenchButton); @@ -68,7 +68,7 @@ test //Go to Settings page await t.click(myRedisDatabasePage.settingsButton); //Specify Commands in pipeline - await t.click(settingsPage.accordionAdvancedSettings); + await t.click(settingsPage.accordionWorkbenchSettings); await settingsPage.changeCommandsInPipeline('1'); //Go to Workbench page await t.click(myRedisDatabasePage.workbenchButton); diff --git a/tests/e2e/tests/regression/workbench/workbench-pipeline.e2e.ts b/tests/e2e/tests/regression/workbench/workbench-pipeline.e2e.ts index 417d8ab8a9..e35338d5b2 100644 --- a/tests/e2e/tests/regression/workbench/workbench-pipeline.e2e.ts +++ b/tests/e2e/tests/regression/workbench/workbench-pipeline.e2e.ts @@ -21,7 +21,7 @@ fixture `Workbench Pipeline` await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); // Go to Settings page - Pipeline mode await t.click(myRedisDatabasePage.settingsButton); - await t.click(settingsPage.accordionAdvancedSettings); + await t.click(settingsPage.accordionWorkbenchSettings); }) .afterEach(async () => { //Delete database @@ -31,7 +31,7 @@ test .meta({ env: env.web })('Verify that user can see the text in settings for pipeline with link', async t => { const pipelineText = 'Sets the size of a command batch for the pipeline(opens in a new tab or window) mode in Workbench. 0 or 1 pipelines every command.'; // Verify text in setting for pipeline - await t.expect(settingsPage.accordionAdvancedSettings.textContent).contains(pipelineText, 'Text is incorrect'); + await t.expect(settingsPage.accordionWorkbenchSettings.textContent).contains(pipelineText, 'Text is incorrect'); await t.click(settingsPage.pipelineLink); // Check new opened window page with the correct URL await t.expect(getPageUrl()).eql(externalPageLink, 'The opened page is incorrect'); diff --git a/tests/e2e/tests/regression/workbench/workbench-re-cluster.e2e.ts b/tests/e2e/tests/regression/workbench/workbench-re-cluster.e2e.ts index 249ca34014..b29039de55 100644 --- a/tests/e2e/tests/regression/workbench/workbench-re-cluster.e2e.ts +++ b/tests/e2e/tests/regression/workbench/workbench-re-cluster.e2e.ts @@ -10,13 +10,15 @@ import { import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { cloudDatabaseConfig, commonUrl, ossClusterConfig, ossSentinelConfig, redisEnterpriseClusterConfig } from '../../../helpers/conf'; import { deleteOSSClusterDatabaseApi, deleteAllSentinelDatabasesApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const common = new Common(); const commandForSend1 = 'info'; const commandForSend2 = 'FT._LIST'; -const verifyCommandsInWorkbench = async() => { +const verifyCommandsInWorkbench = async(): Promise => { const multipleCommands = [ 'info', 'command', @@ -27,7 +29,7 @@ const verifyCommandsInWorkbench = async() => { await workbenchPage.sendCommandInWorkbench(commandForSend1); await workbenchPage.sendCommandInWorkbench(commandForSend2); //Check that all the previous run commands are saved and displayed - await t.eval(() => location.reload()); + await common.reloadPage(); await t.expect(workbenchPage.queryCardCommand.withExactText(commandForSend1).exists).ok('The previous run commands are saved'); await t.expect(workbenchPage.queryCardCommand.withExactText(commandForSend2).exists).ok('The previous run commands are saved'); //Send multiple commands in one query @@ -53,6 +55,7 @@ test await verifyCommandsInWorkbench(); }); test + .meta({ rte: rte.reCloud }) .before(async() => { await acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); }) diff --git a/tests/e2e/tests/smoke/browser/edit-key-value.e2e.ts b/tests/e2e/tests/smoke/browser/edit-key-value.e2e.ts index 99c445b1a0..9975c5db40 100644 --- a/tests/e2e/tests/smoke/browser/edit-key-value.e2e.ts +++ b/tests/e2e/tests/smoke/browser/edit-key-value.e2e.ts @@ -1,41 +1,95 @@ import { rte } from '../../../helpers/constants'; -import { deleteDatabase, acceptTermsAddDatabaseOrConnectToRedisStack } from '../../../helpers/database'; +import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; import { BrowserPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; -import { Chance } from 'chance'; +import { Common } from '../../../helpers/common'; +import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; const browserPage = new BrowserPage(); -const chance = new Chance(); +const common = new Common(); -let keyName = chance.word({ length: 10 }); +const keyTTL = '2147476121'; +const keyValueBefore = 'ValueBeforeEdit!'; +const keyValueAfter = 'ValueAfterEdit!'; +let keyName = common.generateWord(10); fixture `Edit Key values verification` - .meta({ type: 'smoke' }) + .meta({ type: 'smoke', rte: rte.standalone }) .page(commonUrl) - .beforeEach(async () => { - await acceptTermsAddDatabaseOrConnectToRedisStack(ossStandaloneConfig, ossStandaloneConfig.databaseName); + .beforeEach(async() => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); }) - .afterEach(async () => { + .afterEach(async() => { //Clear and delete database await browserPage.deleteKeyByName(keyName); - await deleteDatabase(ossStandaloneConfig.databaseName); - }) -test - .meta({ rte: rte.standalone }) - ('Verify that user can edit String value', async t => { - keyName = chance.word({ length: 10 }); - const keyTTL = '2147476121'; - const keyValueBefore = 'StringValueBeforeEdit!'; - const keyValueAfter = 'StringValueBeforeEdit!'; - - //Add string key - await browserPage.addStringKey(keyName, keyValueBefore, keyTTL); - //Check the key value before edit - let keyValueFromDetails = await browserPage.getStringKeyValue(); - await t.expect(keyValueFromDetails).contains(keyValueBefore, 'The value of the key'); - //Edit String key value - await browserPage.editStringKeyValue(keyValueAfter); - //Check the key value after edit - keyValueFromDetails = await browserPage.getStringKeyValue(); - await t.expect(keyValueFromDetails).contains(keyValueAfter, 'The value of the key'); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); }); +test('Verify that user can edit String value', async t => { + keyName = common.generateWord(10); + //Add string key + await browserPage.addStringKey(keyName, keyValueBefore, keyTTL); + //Check the key value before edit + let keyValue = await browserPage.getStringKeyValue(); + await t.expect(keyValue).contains(keyValueBefore, 'The value is incorrect'); + //Edit String key value + await browserPage.editStringKeyValue(keyValueAfter); + //Check the key value after edit + keyValue = await browserPage.getStringKeyValue(); + await t.expect(keyValue).contains(keyValueAfter, 'Edited value is incorrect'); +}); +test('Verify that user can edit Zset Key member', async t => { + keyName = common.generateWord(10); + const scoreBefore = '5'; + const scoreAfter = '10'; + //Add zset key + await browserPage.addZSetKey(keyName, scoreBefore, keyTTL, keyValueBefore); + //Check the key score before edit + let zsetScore = await browserPage.getZsetKeyScore(); + await t.expect(zsetScore).eql(scoreBefore, 'Score is incorrect'); + //Edit Zset key score + await browserPage.editZsetKeyScore(scoreAfter); + //Check Zset key score after edit + zsetScore = await browserPage.getZsetKeyScore(); + await t.expect(zsetScore).contains(scoreAfter, 'Score is not edited'); +}); +test('Verify that user can edit Hash Key field', async t => { + const fieldName = 'test'; + keyName = common.generateWord(10); + //Add Hash key + await browserPage.addHashKey(keyName, keyTTL, fieldName, keyValueBefore); + //Check the key value before edit + let keyValue = await browserPage.getHashKeyValue(); + await t.expect(keyValue).eql(keyValueBefore, 'The value is incorrect'); + //Edit Hash key value + await browserPage.editHashKeyValue(keyValueAfter); + //Check Hash key value after edit + keyValue = await browserPage.getHashKeyValue(); + await t.expect(keyValue).contains(keyValueAfter, 'Edited value is incorrect'); +}); +test('Verify that user can edit List Key element', async t => { + keyName = common.generateWord(10); + //Add List key + await browserPage.addListKey(keyName, keyTTL, keyValueBefore); + //Check the key value before edit + let keyValue = await browserPage.getListKeyValue(); + await t.expect(keyValue).eql(keyValueBefore, 'The value is incorrect'); + //Edit List key value + await browserPage.editListKeyValue(keyValueAfter); + //Check List key value after edit + keyValue = await browserPage.getListKeyValue(); + await t.expect(keyValue).contains(keyValueAfter, 'Edited value is incorrect'); +}); +test('Verify that user can edit JSON Key value', async t => { + const jsonValueBefore = '{"name":"xyz"}'; + const jsonEditedValue = '"xyz test"'; + const jsonValueAfter = '{name:"xyz test"}'; + keyName = common.generateWord(10); + //Add JSON key with json object + await browserPage.addJsonKey(keyName, jsonValueBefore, keyTTL); + //Check the key value before edit + await t.expect(await browserPage.getJsonKeyValue()).eql('{name:"xyz"}', 'The value is incorrect'); + //Edit JSON key value + await browserPage.editJsonKeyValue(jsonEditedValue); + //Check JSON key value after edit + await t.expect(await browserPage.getJsonKeyValue()).contains(jsonValueAfter, 'Edited value is incorrect'); +}); diff --git a/tests/e2e/tests/smoke/browser/json-key.e2e.ts b/tests/e2e/tests/smoke/browser/json-key.e2e.ts index 756c09dacf..f06b00a71a 100644 --- a/tests/e2e/tests/smoke/browser/json-key.e2e.ts +++ b/tests/e2e/tests/smoke/browser/json-key.e2e.ts @@ -35,8 +35,7 @@ test await t.expect(browserPage.addJsonObjectButton.exists).ok('The existence of the add Json object button', { timeout: 10000 }); await t.expect(browserPage.jsonKeyValue.textContent).eql(jsonObjectValue, 'The json object value'); }); -//skipped due the issue https://redislabs.atlassian.net/browse/RI-2866 -test.skip +test .meta({ rte: rte.standalone })('Verify that user can add key with value to any level of JSON structure', async t => { keyName = chance.word({ length: 10 }); //Add Json key with json object diff --git a/tests/e2e/tests/smoke/database/add-db-from-welcome-page.e2e.ts b/tests/e2e/tests/smoke/database/add-db-from-welcome-page.e2e.ts index 3579afefee..ebd81e477a 100644 --- a/tests/e2e/tests/smoke/database/add-db-from-welcome-page.e2e.ts +++ b/tests/e2e/tests/smoke/database/add-db-from-welcome-page.e2e.ts @@ -1,27 +1,55 @@ -import { rte } from '../../../helpers/constants'; +import { ClientFunction } from 'testcafe'; +import { env, rte } from '../../../helpers/constants'; import { acceptLicenseTerms, addNewStandaloneDatabase, deleteDatabase } from '../../../helpers/database'; import { MyRedisDatabasePage, AddRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; +import { deleteAllDatabasesApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const addRedisDatabasePage = new AddRedisDatabasePage(); +const common = new Common(); +const getPageUrl = ClientFunction(() => window.location.href); +const sourcePage = 'https://developer.redis.com/create/from-source/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight'; +const dockerPage = 'https://developer.redis.com/create/docker/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight'; +const homebrewPage = 'https://developer.redis.com/create/homebrew/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight'; +const promoPage = 'https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_offer_jan'; fixture `Add database from welcome page` .meta({ type: 'smoke' }) .page(commonUrl) .beforeEach(async() => { await acceptLicenseTerms(); - }) - .afterEach(async() => { - //Delete database - await deleteDatabase(ossStandaloneConfig.databaseName); + await deleteAllDatabasesApi(); + // Reload Page + await common.reloadPage(); }); test + .after(async() => { + //Delete database + await deleteDatabase(ossStandaloneConfig.databaseName); + }) .meta({ rte: rte.standalone })('Verify that user can add first DB from Welcome page', async t => { - //Delete all the databases to open Welcome page - await myRedisDatabasePage.deleteAllDatabases(); await t.expect(addRedisDatabasePage.welcomePageTitle.exists).ok('The welcome page title'); //Add database from Welcome page await addNewStandaloneDatabase(ossStandaloneConfig); await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossStandaloneConfig.databaseName).exists).ok('The database adding', { timeout: 10000 }); }); +test + .meta({ env: env.web, rte: rte.standalone })('Verify that all the links are valid from Welcome page', async t => { + // Verify build from source link + await t.click(addRedisDatabasePage.buildFromSource); + await t.expect(getPageUrl()).eql(sourcePage, 'Build from source link is not valid'); + await t.switchToParentWindow(); + // Verify build from docker link + await t.click(addRedisDatabasePage.buildFromDocker); + await t.expect(getPageUrl()).eql(dockerPage, 'Build from docker page is not valid'); + await t.switchToParentWindow(); + // Verify build from homebrew link + await t.click(addRedisDatabasePage.buildFromHomebrew); + await t.expect(getPageUrl()).eql(homebrewPage, 'Build from homebrew page is not valid'); + await t.switchToParentWindow(); + // Verify promo button link + await t.click(myRedisDatabasePage.promoButton); + await t.expect(getPageUrl()).eql(promoPage, 'Promotion link is not valid'); + }); diff --git a/tests/e2e/tests/smoke/workbench/scripting-area.e2e.ts b/tests/e2e/tests/smoke/workbench/scripting-area.e2e.ts index 22ea38a61f..12a6b8eec0 100644 --- a/tests/e2e/tests/smoke/workbench/scripting-area.e2e.ts +++ b/tests/e2e/tests/smoke/workbench/scripting-area.e2e.ts @@ -8,9 +8,11 @@ import { ossStandaloneConfig } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; +import { Common } from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const common = new Common(); fixture `Scripting area at Workbench` .meta({type: 'smoke'}) @@ -64,7 +66,7 @@ test await workbenchPage.sendCommandInWorkbench(commandForSend1); await workbenchPage.sendCommandInWorkbench(commandForSend2); //Check that all the previous run commands are saved and displayed - await t.eval(() => location.reload()); + await common.reloadPage(); await t.expect(workbenchPage.queryCardCommand.withExactText(commandForSend1).exists).ok('The previous run commands are saved'); await t.expect(workbenchPage.queryCardCommand.withExactText(commandForSend2).exists).ok('The previous run commands are saved'); //Send multiple commands in one query diff --git a/tests/e2e/web.runner.ts b/tests/e2e/web.runner.ts index 0287f32905..9eaac2f074 100644 --- a/tests/e2e/web.runner.ts +++ b/tests/e2e/web.runner.ts @@ -6,14 +6,14 @@ import testcafe from 'testcafe'; return t .createRunner() .src((process.env.TEST_FILES || 'tests/**/*.e2e.ts').split('\n')) - .browsers(['chromium:headless']) + .browsers(['chromium:headless --cache --allow-insecure-localhost --ignore-certificate-errors']) .filter((_testName, _fixtureName, _fixturePath, testMeta): boolean => { return testMeta.env !== 'desktop' }) .screenshots({ path: 'report/screenshots/', takeOnFails: true, - pathPattern: '${USERAGENT}/${DATE}_${TIME}/${FIXTURE}_${TEST_INDEX}.png', + pathPattern: '${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST_ID}_${FILE_INDEX}.png', }) .reporter([ 'spec', @@ -24,6 +24,10 @@ import testcafe from 'testcafe'; { name: 'json', output: './results/e2e.results.json' + }, + { + name: 'html', + output: './report/report.html' } ]) .run({ diff --git a/tsconfig.json b/tsconfig.json index 3de380deb1..d186db58cb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,6 +27,7 @@ "emitDecoratorMetadata": true, "resolveJsonModule": true, "allowJs": true, + "skipLibCheck": true, "paths": { "uiSrc/*": [ "redisinsight/ui/src/*" diff --git a/yarn.lock b/yarn.lock index 0933d31d66..ba1b26827b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1630,6 +1630,28 @@ resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-2.7.2.tgz#f34b8aa0c49f0dd55eb7eba577081299cbf3f90b" integrity sha512-rYEi46+gIzufyYUAoHDnRzkWGxajpD9vVXFQ3g1vbjrBm6P7MBmm+s/fqPa46sxa+8FOUdEuRQKaugo5a4JWpw== +"@mswjs/cookies@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-0.2.2.tgz#b4e207bf6989e5d5427539c2443380a33ebb922b" + integrity sha512-mlN83YSrcFgk7Dm1Mys40DLssI1KdJji2CMKN8eOlBqsTADYzj2+jWzsANsUTFbxDMWPD5e9bfA1RGqBpS3O1g== + dependencies: + "@types/set-cookie-parser" "^2.4.0" + set-cookie-parser "^2.4.6" + +"@mswjs/interceptors@^0.17.2": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.17.4.tgz#815f1519ab7642b826d6837eb3aa0899ea8fe070" + integrity sha512-8oKWrOQ1P0Wj0kf3ak8WETuymknw2Tl2s1op8OzZctpNF3zSJSdEbcQs0pm347mwsM+95V6POBMv3W4812pEeg== + dependencies: + "@open-draft/until" "^1.0.3" + "@types/debug" "^4.1.7" + "@xmldom/xmldom" "^0.7.5" + debug "^4.3.3" + headers-polyfill "^3.0.4" + outvariant "^1.2.1" + strict-event-emitter "^0.2.4" + web-encoding "^1.1.5" + "@nestjs/cli@^7.0.0": version "7.5.4" resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-7.5.4.tgz#d4cdce388d7f6a32dabdf5bab909af23653f7740" @@ -1713,6 +1735,11 @@ mkdirp "^1.0.4" rimraf "^3.0.2" +"@open-draft/until@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" + integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== + "@pmmmwh/react-refresh-webpack-plugin@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.3.tgz#1eec460596d200c0236bf195b078a5d1df89b766" @@ -1730,6 +1757,59 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + "@reduxjs/toolkit@^1.6.2": version "1.6.2" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.2.tgz#2f2b5365df77dd6697da28fdf44f33501ed9ba37" @@ -1944,6 +2024,11 @@ "@testing-library/dom" "^8.5.0" "@types/react-dom" "^18.0.0" +"@testing-library/user-event@^14.4.3": + version "14.4.3" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.4.3.tgz#af975e367743fa91989cd666666aec31a8f50591" + integrity sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q== + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -2046,11 +2131,226 @@ dependencies: "@types/node" "*" +"@types/cookie@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" + integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== + "@types/cookiejar@*": version "2.1.2" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== +"@types/d3-array@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.3.tgz#87d990bf504d14ad6b16766979d04e943c046dac" + integrity sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ== + +"@types/d3-axis@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.1.tgz#6afc20744fa5cc0cbc3e2bd367b140a79ed3e7a8" + integrity sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-brush@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.1.tgz#ae5f17ce391935ca88b29000e60ee20452c6357c" + integrity sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-chord@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.1.tgz#54c8856c19c8e4ab36a53f73ba737de4768ad248" + integrity sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw== + +"@types/d3-color@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.0.tgz#6594da178ded6c7c3842f3cc0ac84b156f12f2d4" + integrity sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA== + +"@types/d3-contour@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.1.tgz#9ff4e2fd2a3910de9c5097270a7da8a6ef240017" + integrity sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ== + dependencies: + "@types/d3-array" "*" + "@types/geojson" "*" + +"@types/d3-delaunay@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz#006b7bd838baec1511270cb900bf4fc377bbbf41" + integrity sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ== + +"@types/d3-dispatch@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz#a1b18ae5fa055a6734cb3bd3cbc6260ef19676e3" + integrity sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw== + +"@types/d3-drag@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.1.tgz#fb1e3d5cceeee4d913caa59dedf55c94cb66e80f" + integrity sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-dsv@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.0.tgz#f3c61fb117bd493ec0e814856feb804a14cfc311" + integrity sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A== + +"@types/d3-ease@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.0.tgz#c29926f8b596f9dadaeca062a32a45365681eae0" + integrity sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA== + +"@types/d3-fetch@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.1.tgz#f9fa88b81aa2eea5814f11aec82ecfddbd0b8fe0" + integrity sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw== + dependencies: + "@types/d3-dsv" "*" + +"@types/d3-force@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.3.tgz#76cb20d04ae798afede1ea6e41750763ff5a9c82" + integrity sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA== + +"@types/d3-format@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.1.tgz#194f1317a499edd7e58766f96735bdc0216bb89d" + integrity sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg== + +"@types/d3-geo@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.0.2.tgz#e7ec5f484c159b2c404c42d260e6d99d99f45d9a" + integrity sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ== + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.0.tgz#4561bb7ace038f247e108295ef77b6a82193ac25" + integrity sha512-g+sey7qrCa3UbsQlMZZBOHROkFqx7KZKvUpRzI/tAp/8erZWpYq7FgNKvYwebi2LaEiVs1klhUfd3WCThxmmWQ== + +"@types/d3-interpolate@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc" + integrity sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.0.tgz#939e3a784ae4f80b1fde8098b91af1776ff1312b" + integrity sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg== + +"@types/d3-polygon@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.0.tgz#5200a3fa793d7736fa104285fa19b0dbc2424b93" + integrity sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw== + +"@types/d3-quadtree@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz#433112a178eb7df123aab2ce11c67f51cafe8ff5" + integrity sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw== + +"@types/d3-random@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.1.tgz#5c8d42b36cd4c80b92e5626a252f994ca6bfc953" + integrity sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ== + +"@types/d3-scale-chromatic@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#103124777e8cdec85b20b51fd3397c682ee1e954" + integrity sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw== + +"@types/d3-scale@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.2.tgz#41be241126af4630524ead9cb1008ab2f0f26e69" + integrity sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA== + dependencies: + "@types/d3-time" "*" + +"@types/d3-selection@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.3.tgz#57be7da68e7d9c9b29efefd8ea5a9ef1171e42ba" + integrity sha512-Mw5cf6nlW1MlefpD9zrshZ+DAWL4IQ5LnWfRheW6xwsdaWOb6IRRu2H7XPAQcyXEx1D7XQWgdoKR83ui1/HlEA== + +"@types/d3-shape@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.0.tgz#1d87a6ddcf28285ef1e5c278ca4bdbc0658f3505" + integrity sha512-jYIYxFFA9vrJ8Hd4Se83YI6XF+gzDL1aC5DCsldai4XYYiVNdhtpGbA/GM6iyQ8ayhSp3a148LY34hy7A4TxZA== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.0.tgz#ee7b6e798f8deb2d9640675f8811d0253aaa1946" + integrity sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw== + +"@types/d3-time@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.0.tgz#e1ac0f3e9e195135361fa1a1d62f795d87e6e819" + integrity sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg== + +"@types/d3-timer@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.0.tgz#e2505f1c21ec08bda8915238e397fb71d2fc54ce" + integrity sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g== + +"@types/d3-transition@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.2.tgz#393dc3e3d55009a43cc6f252e73fccab6d78a8a4" + integrity sha512-jo5o/Rf+/u6uerJ/963Dc39NI16FQzqwOc54bwvksGAdVfvDrqDpVeq95bEvPtBwLCVZutAEyAtmSyEMxN7vxQ== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-zoom@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.1.tgz#4bfc7e29625c4f79df38e2c36de52ec3e9faf826" + integrity sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3@^7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.0.tgz#fc5cac5b1756fc592a3cf1f3dc881bf08225f515" + integrity sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA== + dependencies: + "@types/d3-array" "*" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-delaunay" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-zoom" "*" + "@types/date-fns@^2.6.0": version "2.6.0" resolved "https://registry.yarnpkg.com/@types/date-fns/-/date-fns-2.6.0.tgz#b062ca46562002909be0c63a6467ed173136acc1" @@ -2058,7 +2358,7 @@ dependencies: date-fns "*" -"@types/debug@^4.0.0", "@types/debug@^4.1.6": +"@types/debug@^4.0.0", "@types/debug@^4.1.6", "@types/debug@^4.1.7": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== @@ -2129,6 +2429,11 @@ dependencies: "@types/node" "*" +"@types/geojson@*": + version "7946.0.10" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249" + integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA== + "@types/glob@^7.1.1": version "7.1.4" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672" @@ -2228,6 +2533,11 @@ jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" +"@types/js-levenshtein@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5" + integrity sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g== + "@types/jsdom@^16.2.6": version "16.2.13" resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-16.2.13.tgz#126c8b7441b159d6234610a48de77b6066f1823f" @@ -2264,6 +2574,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.176.tgz#641150fc1cda36fbfa329de603bbb175d7ee20c0" integrity sha512-xZmuPTa3rlZoIbtDUyJKZQimJV3bxCmzMIO2c9Pz9afyDro6kr7R79GwcB6mRhuoPmV2p1Vb66WOJH7F886WKQ== +"@types/long@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + "@types/mdast@^3.0.0", "@types/mdast@^3.0.3": version "3.0.10" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" @@ -2296,10 +2611,10 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== -"@types/node@*": - version "18.6.2" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.2.tgz#ffc5f0f099d27887c8d9067b54e55090fcd54126" - integrity sha512-KcfkBq9H4PI6Vpu5B/KoPeuVDAbmi+2mDBqGPGUgoL7yXQtcWGu2vJWmmRkneWK3Rh0nIAX192Aa87AqKHYChQ== +"@types/node@*", "@types/node@>=13.7.0": + version "18.7.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.6.tgz#31743bc5772b6ac223845e18c3fc26f042713c83" + integrity sha512-EdxgKRXgYsNITy5mjjXjVE/CS8YENSdhiagGrLqjG0pvA2owgJ6i4l7wy/PFZGC0B1/H20lWKN7ONVDNYDZm7A== "@types/node@14.14.10": version "14.14.10" @@ -2466,7 +2781,16 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^17.0.1": +"@types/react@*", "@types/react@^18.0.20": + version "18.0.20" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.20.tgz#e4c36be3a55eb5b456ecf501bd4a00fd4fd0c9ab" + integrity sha512-MWul1teSPxujEHVwZl4a5HxQ9vVNsjTchVA+xRqv/VYGCuKGAU6UhfrTdF5aBefwD1BHUD8i/zq+O/vyCm/FrA== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/react@^17.0.1": version "17.0.37" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959" integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg== @@ -2524,6 +2848,13 @@ "@types/mime" "^1" "@types/node" "*" +"@types/set-cookie-parser@^2.4.0": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.2.tgz#b6a955219b54151bfebd4521170723df5e13caad" + integrity sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w== + dependencies: + "@types/node" "*" + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" @@ -3070,6 +3401,11 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.3.0.tgz#2730c770f5f1f132767c63dcaaa4ec28f8c56a6c" integrity sha512-k2p2VrONcYVX1wRRrf0f3X2VGltLWcv+JzXRBDmvCxGlCeESx4OXw91TsWeKOkp784uNoVQo313vxJFHXPPwfw== +"@xmldom/xmldom@^0.7.5": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d" + integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A== + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -3085,6 +3421,11 @@ resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== +"@zxing/text-encoding@0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b" + integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA== + abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -3628,6 +3969,11 @@ attr-accept@^2.2.1: resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -4383,6 +4729,14 @@ chalk@3.0.0, chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + chalk@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -4789,6 +5143,11 @@ commander@4.1.1, commander@^4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@7, commander@^7.0.0, commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -4804,11 +5163,6 @@ commander@^6.1.0, commander@^6.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== -commander@^7.0.0, commander@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -4974,6 +5328,11 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + cookiejar@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" @@ -5357,6 +5716,250 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.5.tgz#7fdec6a28a67ae18647c51668a9ff95bb2fa7bb8" integrity sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ== +"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.0.tgz#15bf96cd9b7333e02eb8de8053d78962eafcff14" + integrity sha512-3yXFQo0oG3QCxbF06rMPFyGRMGJNS7NvsV1+2joOjbBE+9xvWQ8+GcMJAjRCzw06zQ3/arXeJgbPYcjUCuC+3g== + dependencies: + internmap "1 - 2" + +d3-axis@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322" + integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw== + +d3-brush@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c" + integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "3" + d3-transition "3" + +d3-chord@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966" + integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g== + dependencies: + d3-path "1 - 3" + +"d3-color@1 - 3", d3-color@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-contour@4: + version "4.0.0" + resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.0.tgz#5a1337c6da0d528479acdb5db54bc81a0ff2ec6b" + integrity sha512-7aQo0QHUTu/Ko3cP9YK9yUTxtoDEiDGwnBHyLxG5M4vqlBkO/uixMRele3nfsfj6UXOcuReVpVXzAboGraYIJw== + dependencies: + d3-array "^3.2.0" + +d3-delaunay@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.2.tgz#7fd3717ad0eade2fc9939f4260acfb503f984e92" + integrity sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ== + dependencies: + delaunator "5" + +"d3-dispatch@1 - 3", d3-dispatch@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + +"d3-drag@2 - 3", d3-drag@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + dependencies: + d3-dispatch "1 - 3" + d3-selection "3" + +"d3-dsv@1 - 3", d3-dsv@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73" + integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q== + dependencies: + commander "7" + iconv-lite "0.6" + rw "1" + +"d3-ease@1 - 3", d3-ease@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +d3-fetch@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22" + integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw== + dependencies: + d3-dsv "1 - 3" + +d3-force@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4" + integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg== + dependencies: + d3-dispatch "1 - 3" + d3-quadtree "1 - 3" + d3-timer "1 - 3" + +"d3-format@1 - 3", d3-format@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +d3-geo@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.0.1.tgz#4f92362fd8685d93e3b1fae0fd97dc8980b1ed7e" + integrity sha512-Wt23xBych5tSy9IYAM1FR2rWIBFWa52B/oF/GYe5zbdHrg08FU8+BuI6X4PvTwPDdqdAdq04fuWJpELtsaEjeA== + dependencies: + d3-array "2.5.0 - 3" + +d3-hierarchy@3: + version "3.1.2" + resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" + integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA== + +"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +"d3-path@1 - 3", d3-path@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.0.1.tgz#f09dec0aaffd770b7995f1a399152bf93052321e" + integrity sha512-gq6gZom9AFZby0YLduxT1qmrp4xpBA1YZr19OI717WIdKE2OM5ETq5qrHLb301IgxhLwcuxvGZVLeeWc/k1I6w== + +d3-polygon@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398" + integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg== + +"d3-quadtree@1 - 3", d3-quadtree@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f" + integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw== + +d3-random@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4" + integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ== + +d3-scale-chromatic@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a" + integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g== + dependencies: + d3-color "1 - 3" + d3-interpolate "1 - 3" + +d3-scale@4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +"d3-selection@2 - 3", d3-selection@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + +d3-shape@3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.1.0.tgz#c8a495652d83ea6f524e482fca57aa3f8bc32556" + integrity sha512-tGDh1Muf8kWjEDT/LswZJ8WF85yDZLvVJpYU9Nq+8+yW1Z5enxrmXOhTArlkaElU+CTn0OTVNli+/i+HP45QEQ== + dependencies: + d3-path "1 - 3" + +"d3-time-format@2 - 4", d3-time-format@4: + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.0.0.tgz#65972cb98ae2d4954ef5c932e8704061335d4975" + integrity sha512-zmV3lRnlaLI08y9IMRXSDshQb5Nj77smnfpnd2LrBa/2K281Jijactokeak14QacHs/kKq0AQ121nidNYlarbQ== + dependencies: + d3-array "2 - 3" + +"d3-timer@1 - 3", d3-timer@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + +"d3-transition@2 - 3", d3-transition@3: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + +d3-zoom@3: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" + +d3@^7.6.1: + version "7.6.1" + resolved "https://registry.yarnpkg.com/d3/-/d3-7.6.1.tgz#b21af9563485ed472802f8c611cc43be6c37c40c" + integrity sha512-txMTdIHFbcpLx+8a0IFhZsbp+PfBBPt8yfbmukZTQFroKuFqIwqswF0qE5JXWefylaAVpSXFoKm3yP+jpNLFLw== + dependencies: + d3-array "3" + d3-axis "3" + d3-brush "3" + d3-chord "3" + d3-color "3" + d3-contour "4" + d3-delaunay "6" + d3-dispatch "3" + d3-drag "3" + d3-dsv "3" + d3-ease "3" + d3-fetch "3" + d3-force "3" + d3-format "3" + d3-geo "3" + d3-hierarchy "3" + d3-interpolate "3" + d3-path "3" + d3-polygon "3" + d3-quadtree "3" + d3-random "3" + d3-scale "4" + d3-scale-chromatic "3" + d3-selection "3" + d3-shape "3" + d3-time "3" + d3-time-format "4" + d3-timer "3" + d3-transition "3" + d3-zoom "3" + damerau-levenshtein@^1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz#64368003512a1a6992593741a09a9d31a836f55d" @@ -5560,6 +6163,13 @@ del@^4.1.1: pify "^4.0.1" rimraf "^2.6.3" +delaunator@5: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b" + integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw== + dependencies: + robust-predicates "^3.0.0" + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -6255,7 +6865,7 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.1.1" -es-abstract@^1.17.2, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2, es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.1: +es-abstract@^1.17.2, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2, es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.0, es-abstract@^1.20.1: version "1.20.1" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.1.tgz#027292cd6ef44bd12b1913b828116f54787d1814" integrity sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA== @@ -6658,7 +7268,7 @@ eventemitter3@^4.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.0.0, events@^3.2.0: +events@^3.0.0, events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -7114,6 +7724,13 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.7: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -7893,6 +8510,11 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +headers-polyfill@^3.0.4: + version "3.0.10" + resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.0.10.tgz#51a72c0d9c32594fd23854a564c3d6c80b46b065" + integrity sha512-lOhQU7iG3AMcjmb8NIWCa+KwfJw5bY44BoWPtrj5A4iDbSD3ylGf5QcYr0ZyQnhkKQ2GgWNLdF2rfrXtXlF3nQ== + hex-color-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" @@ -8222,7 +8844,7 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.6.2: +iconv-lite@0.6, iconv-lite@^0.6.2: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -8380,6 +9002,27 @@ inquirer@7.3.3: strip-ansi "^6.0.0" through "^2.3.6" +inquirer@^8.2.0: + version "8.2.4" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4" + integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.5.5" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + wrap-ansi "^7.0.0" + internal-ip@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" @@ -8397,6 +9040,11 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + interpret@^1.0.0, interpret@^1.2.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -8534,7 +9182,7 @@ is-buffer@^2.0.0: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== -is-callable@^1.1.4, is-callable@^1.2.4: +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== @@ -8660,6 +9308,13 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== +is-generator-function@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + is-glob@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" @@ -8702,6 +9357,11 @@ is-negative-zero@^2.0.2: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== +is-node-process@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.0.1.tgz#4fc7ac3a91e8aac58175fe0578abbc56f2831b23" + integrity sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ== + is-npm@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8" @@ -8836,6 +9496,17 @@ is-symbol@^1.0.2, is-symbol@^1.0.3: dependencies: has-symbols "^1.0.2" +is-typed-array@^1.1.3, is-typed-array@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.9.tgz#246d77d2871e7d9f5aeb1d54b9f52c71329ece67" + integrity sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-abstract "^1.20.0" + for-each "^0.3.3" + has-tostringtag "^1.0.0" + is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -8974,6 +9645,13 @@ jake@^10.8.5: filelist "^1.0.1" minimatch "^3.0.4" +java-object-serialization@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/java-object-serialization/-/java-object-serialization-0.1.1.tgz#b20f34a619df3ce4b58980d7d9893048a696a0bd" + integrity sha512-m1bd/kPwNjbrMrIITzMGWoXiSU5fzUg6IdzGDrmGNPahNQ5YVzY9dv5qQzAcRC7oy0C0CwDseatdeI6SKO8MLA== + dependencies: + tslib "^2.1.0" + jest-changed-files@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-27.5.1.tgz#a348aed00ec9bf671cc58a66fcbe7c3dfd6a68f5" @@ -9440,11 +10118,21 @@ jest@^27.5.1: import-local "^3.0.2" jest-cli "^27.5.1" +jpickle@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/jpickle/-/jpickle-0.4.1.tgz#17eea5d3624cccec38f86e89a059abd05952364a" + integrity sha512-XHSTQUtl/Yv2cNzeQ7NKFuK0Z3d1iqQC2iEwV0cu3/dG6PV3DgACEQ3wGagHRDV6MKzypPWwHVSDY8kBWkbfTA== + js-base64@^2.1.8: version "2.6.4" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== +js-levenshtein@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" + integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -9953,6 +10641,11 @@ loglevel@^1.6.8: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + longest-streak@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.0.1.tgz#c97315b7afa0e7d9525db9a5a2953651432bdc5d" @@ -10895,6 +11588,31 @@ ms@^2.0.0, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +msw@^0.45.0: + version "0.45.0" + resolved "https://registry.yarnpkg.com/msw/-/msw-0.45.0.tgz#7bab4ff0a03875aa17b7fefd5fbeea0b71f95957" + integrity sha512-aZgYsSJWYLHj5wZs/g640GP5AK6gfHC6dKTED8bforKZx10IJ+b0z7Y+0infEKD4gNT3orj7tiUZe4pxgpY0SQ== + dependencies: + "@mswjs/cookies" "^0.2.2" + "@mswjs/interceptors" "^0.17.2" + "@open-draft/until" "^1.0.3" + "@types/cookie" "^0.4.1" + "@types/js-levenshtein" "^1.1.1" + chalk "4.1.1" + chokidar "^3.4.2" + cookie "^0.4.2" + headers-polyfill "^3.0.4" + inquirer "^8.2.0" + is-node-process "^1.0.1" + js-levenshtein "^1.1.6" + node-fetch "^2.6.7" + outvariant "^1.3.0" + path-to-regexp "^6.2.0" + statuses "^2.0.0" + strict-event-emitter "^0.2.0" + type-fest "^1.2.2" + yargs "^17.3.1" + multicast-dns-service-types@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" @@ -11032,6 +11750,13 @@ node-emoji@^1.10.0: dependencies: lodash "^4.17.21" +node-fetch@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + node-forge@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" @@ -11449,7 +12174,7 @@ ora@5.1.0: strip-ansi "^6.0.0" wcwidth "^1.0.1" -ora@^5.1.0: +ora@^5.1.0, ora@^5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== @@ -11489,6 +12214,11 @@ os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= +outvariant@^1.2.1, outvariant@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.3.0.tgz#c39723b1d2cba729c930b74bf962317a81b9b1c9" + integrity sha512-yeWM9k6UPfG/nzxdaPlJkB2p08hCg4xP6Lx99F+vP8YF7xyZVfTmJjrrNalkmzudD4WFvNLVudQikqUmF8zhVQ== + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -11743,6 +12473,11 @@ path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +path-to-regexp@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" + integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -11776,6 +12511,11 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +php-serialize@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/php-serialize/-/php-serialize-4.0.2.tgz#869bd4e01c2d26ac41c1d3e8058ab00ec072e3e5" + integrity sha512-73K9MqCnRn07sXxOht6kVLg+fg1lf/VYpecKy4n9ABcw1PJIAWfaxuQKML27EjolGHWxlXTy3rfh59AGrcUvIA== + picocolors@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" @@ -12331,6 +13071,25 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= +protobufjs@^6.10.2: + version "6.11.3" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74" + integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + proxy-addr@~2.0.5: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -12476,6 +13235,14 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" +rawproto@^0.7.6: + version "0.7.6" + resolved "https://registry.yarnpkg.com/rawproto/-/rawproto-0.7.6.tgz#82c272f6d8de7a20be433487fe84753527604c12" + integrity sha512-7DOBnDK8iApEAdzqglJuY5KfhY8sLZi97enlPLGma5/QW44vY46GgIyC6CyJFLvFmgz3e3LpxscvUxa2XdEgEA== + dependencies: + protobufjs "^6.10.2" + yargs "^16.2.0" + rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -13375,6 +14142,11 @@ roarr@^2.15.3: semver-compare "^1.0.0" sprintf-js "^1.1.2" +robust-predicates@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a" + integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g== + run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -13387,6 +14159,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rw@1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== + rxjs@6.6.3, rxjs@^6.5.2, rxjs@^6.6.0, rxjs@^6.6.3: version "6.6.3" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" @@ -13394,10 +14171,10 @@ rxjs@6.6.3, rxjs@^6.5.2, rxjs@^6.6.0, rxjs@^6.6.3: dependencies: tslib "^1.9.0" -rxjs@^7.5.2: - version "7.5.5" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" - integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== +rxjs@^7.5.2, rxjs@^7.5.5: + version "7.5.6" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc" + integrity sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw== dependencies: tslib "^2.1.0" @@ -13650,6 +14427,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-cookie-parser@^2.4.6: + version "2.5.1" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.5.1.tgz#ddd3e9a566b0e8e0862aca974a6ac0e01349430b" + integrity sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ== + set-immediate-shim@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" @@ -14146,6 +14928,11 @@ static-extend@^0.1.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +statuses@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + stdout-stream@^1.4.0: version "1.4.1" resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.1.tgz#5ac174cdd5cd726104aa0c0b2bd83815d8d535de" @@ -14172,6 +14959,13 @@ stream-http@^2.7.2: to-arraybuffer "^1.0.0" xtend "^4.0.0" +strict-event-emitter@^0.2.0, strict-event-emitter@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.2.4.tgz#365714f0c95f059db31064ca745d5b33e5b30f6e" + integrity sha512-xIqTLS5azUH1djSUsLH9DbP6UnM/nI18vu8d43JigCQEoVsnY+mrlE+qv6kYqs6/1OkMnMIiL6ffedQSZStuoQ== + dependencies: + events "^3.3.0" + string-argv@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" @@ -14770,6 +15564,11 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + tree-kill@1.2.2, tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" @@ -14974,10 +15773,10 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^1.0.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.2.2.tgz#1930bc36b2064f7ab4aa307a6d1b65965199c698" - integrity sha512-pfkPYCcuV0TJoo/jlsUeWNV8rk7uMU6ocnYNvca1Vu+pyKi8Rl8Zo2scPt9O72gCsXIm+dMxOOWuA3VFDSdzWA== +type-fest@^1.0.2, type-fest@^1.2.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" @@ -15453,6 +16252,18 @@ util@^0.11.0: dependencies: inherits "2.0.3" +util@^0.12.3: + version "0.12.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253" + integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + safe-buffer "^5.1.2" + which-typed-array "^1.1.2" + utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" @@ -15619,11 +16430,25 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-encoding@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/web-encoding/-/web-encoding-1.1.5.tgz#fc810cf7667364a6335c939913f5051d3e0c4864" + integrity sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA== + dependencies: + util "^0.12.3" + optionalDependencies: + "@zxing/text-encoding" "0.9.0" + web-namespaces@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + webidl-conversions@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff" @@ -15842,11 +16667,24 @@ whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" +whatwg-fetch@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" + integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== + whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^8.0.0, whatwg-url@^8.5.0: version "8.7.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77" @@ -15877,6 +16715,18 @@ which-pm-runs@^1.0.0: resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= +which-typed-array@^1.1.2: + version "1.1.8" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.8.tgz#0cfd53401a6f334d90ed1125754a42ed663eb01f" + integrity sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-abstract "^1.20.0" + for-each "^0.3.3" + has-tostringtag "^1.0.0" + is-typed-array "^1.1.9" + which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -16054,6 +16904,11 @@ yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^21.0.0: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + yargs@^13.3.0, yargs@^13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" @@ -16083,18 +16938,18 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.1: - version "17.2.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea" - integrity sha512-XfR8du6ua4K6uLGm5S6fA+FIJom/MdJcFNVY8geLlp2v8GYbOXD4EB1tPNZsRn4vBzKGMgb5DRZMeWuFc2GO8Q== +yargs@^17.0.1, yargs@^17.3.1: + version "17.5.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e" + integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== dependencies: cliui "^7.0.2" escalade "^3.1.1" get-caller-file "^2.0.5" require-directory "^2.1.1" - string-width "^4.2.0" + string-width "^4.2.3" y18n "^5.0.5" - yargs-parser "^20.2.2" + yargs-parser "^21.0.0" yarn-deduplicate@^3.1.0: version "3.1.0"