diff --git a/.circleci/build/release-docker.sh b/.circleci/build/release-docker.sh index 85f4129a8c..c56b7e221c 100755 --- a/.circleci/build/release-docker.sh +++ b/.circleci/build/release-docker.sh @@ -2,7 +2,7 @@ set -e HELP="Args: --v - Semver (2.60.0) +-v - Semver (2.62.0) -d - Build image repository (Ex: -d redisinsight) -r - Target repository (Ex: -r redis/redisinsight) " diff --git a/.circleci/config.yml b/.circleci/config.yml index 250cc8ca72..57090b5132 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,1847 +1,25 @@ version: 2.1 -aliases: - keychain: &keychain - run: - name: Add cert to the keychain - command: | - security create-keychain -p mysecretpassword $KEYCHAIN - security default-keychain -s $KEYCHAIN - security unlock-keychain -p mysecretpassword $KEYCHAIN - security set-keychain-settings -u -t 10000000 $KEYCHAIN - security import certs/mac-developer.p12 -k $KEYCHAIN -P "$CSC_KEY_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productbuild - security import certs/mas-distribution.p12 -k $KEYCHAIN -P "$CSC_MAS_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productbuild - security import certs/mac-installer.p12 -k $KEYCHAIN -P "$CSC_MAC_INSTALLER_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productbuild - security set-key-partition-list -S apple-tool:,apple: -s -k mysecretpassword $KEYCHAIN - environment: - KEYCHAIN: redisinsight.keychain - import: &import - run: - name: User certutil to import certificate - command: certutil -p %WIN_CSC_KEY_PASSWORD% -importpfx certs\redislabs_win.pfx - shell: cmd.exe - sign: &sign - run: - name: Sign application - command: | - $filePath = $(Get-ChildItem release -Filter Redis-Insight*.exe | % { $_.FullName }) - $filePathWithQuotes = '"{0}"' -f $filePath - & "C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x86\signtool.exe" sign /a /sm /n "Redis Labs Inc." /fd sha256 /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /v $FilePathWithQuotes - shell: powershell.exe - fileScan: &fileScan - run: - name: Virustotal file scan - command: &virusfilescan | - uploadUrl=$(curl -sq -XGET https://www.virustotal.com/api/v3/files/upload_url -H "x-apikey: $VIRUSTOTAL_API_KEY" | jq -r '.data') - uploadFile=$("/usr/bin/find" /tmp/release -name ${FILE_NAME}) - echo "File to upload: ${uploadFile}" - analysedId=$(curl -sq -XPOST "${uploadUrl}" -H "x-apikey: $VIRUSTOTAL_API_KEY" --form file=@"${uploadFile}" | jq -r '.data.id') - if [ $analysedId == "null" ]; then - echo 'Status is null, something went wrong'; exit 1; - fi - echo "export ANALYZED_ID=${analysedId}" >> $BASH_ENV - echo "Virustotal Analyzed id: ${analysedId}" - sleep 10 - shell: /bin/bash - urlScan: &urlScan - run: - name: Virustotal url scan - command: &virusurlscan | - echo "Url to check: ${URL}" - - analysedId=$(curl -sq -XPOST https://www.virustotal.com/api/v3/urls -H "x-apikey: $VIRUSTOTAL_API_KEY" --form url=${URL} | jq -r '.data.id') - - if [ $analysedId == "null" ]; then - echo 'Status is null, something went wrong'; exit 1; - fi - echo "export ANALYZED_ID=${analysedId}" >> $BASH_ENV - echo "Virustotal Analyzed id: ${analysedId}" - sleep 10 - shell: /bin/bash - validate: &validate - run: - name: Virustotal validate scan results - command: &virusValidate | - analyzeStatus=$(curl -sq -XGET https://www.virustotal.com/api/v3/analyses/${ANALYZED_ID} -H "x-apikey: $VIRUSTOTAL_API_KEY" | jq -r '.data.attributes.status') - if [ $analyzeStatus == "null" ]; then - echo 'Status is null, something went wrong'; exit 1; - fi - - currentOperation="50" - until [ "$currentOperation" == "0" ]; do - if [ "$analyzeStatus" == "completed" ] - then - echo "Current status: ${analyzeStatus}"; break; - else - echo "Current status: ${analyzeStatus}, retries left: ${currentOperation} "; - analyzeStatus=$(curl -sq -XGET https://www.virustotal.com/api/v3/analyses/${ANALYZED_ID} -H "x-apikey: $VIRUSTOTAL_API_KEY" | jq -r '.data.attributes.status'); - sleep 20; - currentOperation=$[$currentOperation - 1]; - fi - done - - analyzeStats=$(curl -sq -XGET https://www.virustotal.com/api/v3/analyses/${ANALYZED_ID} -H "x-apikey: $VIRUSTOTAL_API_KEY" | jq -r '.data.attributes.stats') - analazedHarmless=$(echo ${analyzeStats} | jq '.harmless') - analazedMalicious=$(echo ${analyzeStats} | jq '.malicious') - analazedSuspicious=$(echo ${analyzeStats} | jq '.suspicious') - - if [ "$analyzeStatus" != "completed" ]; then - echo 'Analyse is not completed'; exit 1; - fi - echo "Results:" - echo "analazedHarmless: ${analazedHarmless}, analazedMalicious: ${analazedMalicious}, analazedSuspicious: ${analazedSuspicious}" - - if [ "$analazedMalicious" != "0" ] || [ "$analazedSuspicious" != "0" ]; then - echo "export VIRUS_CHECK_FAILED=true" >> $BASH_ENV - echo 'Found dangers'; exit 0; - fi - - echo "export VIRUS_CHECK_FAILED=false" >> $BASH_ENV - echo "export SKIP_VIRUSTOTAL_REPORT=true" >> $BASH_ENV - echo 'Passed'; - shell: /bin/bash - no_output_timeout: 15m - virustotalReport: &virustotalReport - run: - name: Virustotal slack report - command: &virusreport | - if [ "$SKIP_VIRUSTOTAL_REPORT" == "true" ]; then - exit 0; - fi - - FILE_NAME=virustotal.report.json - BUILD_NAME=$BUILD_NAME FILE_NAME=$FILE_NAME VIRUS_CHECK_FAILED=$VIRUS_CHECK_FAILED node .circleci/virustotal-report.js && - curl -H "Content-type: application/json" --data @$FILE_NAME -H "Authorization: Bearer ${SLACK_TEST_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage - - if [ "$VIRUS_CHECK_FAILED" == "true" ]; then - echo 'Found dangers'; exit 1; - fi - shell: /bin/bash - iTestsNames: &iTestsNames - - oss-st-5 # OSS Standalone v5 - - oss-st-5-pass # OSS Standalone v5 with admin pass required - - oss-st-6 # OSS Standalone v6 and all modules - # TODO: Investigate why it randomly fails - # - oss-st-big # OSS Standalone v6 and all modules and predefined amount of data inside (~3-4M) - - mods-preview # OSS Standalone and all preview modules - - oss-st-6-tls # OSS Standalone v6 with TLS enabled - - oss-st-6-tls-auth # OSS Standalone v6 with TLS auth required -# - oss-st-6-tls-auth-ssh # OSS Standalone v6 with TLS auth required through ssh - - oss-clu # OSS Cluster - - oss-clu-tls # OSS Cluster with TLS enabled - - oss-sent # OSS Sentinel - - oss-sent-tls-auth # OSS Sentinel with TLS auth - - 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 - - oss-sent-tls-auth # OSS Sentinel with TLS auth - guides-filter: &guidesFilter - filters: - branches: - only: - - guides - dev-filter: &devFilter - filters: - branches: - only: - - main - - /^build\/.*/ - stage-filter: &stageFilter - filters: - branches: - only: - - /^release.*/ - prod-filter: &prodFilter - filters: - branches: - only: - - latest - ui-deps-cache-key: &uiDepsCacheKey - key: v1-ui-deps-{{ checksum "yarn.lock" }} - api-deps-cache-key: &apiDepsCacheKey - key: v1-ui-deps-{{ checksum "redisinsight/api/yarn.lock" }} - manual-build-conditions: &manual-build-conditions - or: - - << pipeline.parameters.linux >> - - << pipeline.parameters.mac >> - - << pipeline.parameters.windows >> - - << pipeline.parameters.docker >> - ignore-for-manual-build: &ignore-for-manual-build - when: - not: *manual-build-conditions - -orbs: - win: circleci/windows@2.4.1 - node: circleci/node@5.2.0 - aws: circleci/aws-cli@2.0.3 - executors: - linux-executor: - machine: - image: ubuntu-2004:2023.04.2 - linux-executor-dlc: - machine: - image: ubuntu-2004:2023.04.2 - docker_layer_caching: true docker-node: docker: - image: cimg/node:20.15 - docker: - docker: - - image: cibuilds/docker:19.03.5 - macos: - macos: - xcode: 14.2.0 - -parameters: - linux: - type: string - default: &ignore "" - mac: - type: string - default: *ignore - windows: - type: string - default: *ignore - docker: - type: string - default: *ignore - redis_client: - type: string - default: "" - env: - type: string - default: "stage" jobs: - # Test jobs - unit-tests-ui: + # Placeholder job + placeholder-job: executor: docker-node steps: - checkout - - restore_cache: - <<: *uiDepsCacheKey - - run: - name: UI PROD dependencies audit - command: | - FILENAME=ui.prod.deps.audit.json - yarn audit --groups dependencies --json > $FILENAME || true && - FILENAME=$FILENAME DEPS="UI prod" node .circleci/deps-audit-report.js && - curl -H "Content-type: application/json" --data @slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage - - run: - name: UI DEV dependencies audit - command: | - FILENAME=ui.dev.deps.audit.json - yarn audit --groups devDependencies --json > $FILENAME || true && - FILENAME=$FILENAME DEPS="UI dev" node .circleci/deps-audit-report.js && - curl -H "Content-type: application/json" --data @slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage - - run: - name: Code analysis - command: | - SKIP_POSTINSTALL=1 yarn install - - FILENAME=ui.lint.audit.json - WORKDIR="." - yarn lint:ui -f json -o $FILENAME || true && - FILENAME=$FILENAME WORKDIR=$WORKDIR TARGET="UI" node .circleci/lint-report.js && - curl -H "Content-type: application/json" --data @$WORKDIR/slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage - - FILENAME=rest.lint.audit.json - yarn lint -f json -o $FILENAME || true && - FILENAME=$FILENAME WORKDIR=$WORKDIR TARGET="REST" node .circleci/lint-report.js && - curl -H "Content-type: application/json" --data @$WORKDIR/slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage - - run: - name: Unit tests UI - command: | - yarn test:cov --ci --silent - - save_cache: - <<: *uiDepsCacheKey - paths: - - ./node_modules - unit-tests-api: - executor: docker-node - steps: - - checkout - - restore_cache: - <<: *apiDepsCacheKey - - run: - name: API PROD dependencies scan - command: | - FILENAME=api.prod.deps.audit.json - yarn --cwd redisinsight/api audit --groups dependencies --json > $FILENAME || true && - FILENAME=$FILENAME DEPS="API prod" node .circleci/deps-audit-report.js && - curl -H "Content-type: application/json" --data @slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage - - run: - name: API DEV dependencies scan - command: | - FILENAME=api.dev.deps.audit.json - yarn --cwd redisinsight/api audit --groups devDependencies --json > $FILENAME || true && - FILENAME=$FILENAME DEPS="API dev" node .circleci/deps-audit-report.js && - curl -H "Content-type: application/json" --data @slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage - - run: - name: Code analysis - command: | - yarn --cwd redisinsight/api - - FILENAME=api.lint.audit.json - WORKDIR="./redisinsight/api" - yarn lint:api -f json -o $FILENAME || true && - FILENAME=$FILENAME WORKDIR=$WORKDIR TARGET="API" node .circleci/lint-report.js && - curl -H "Content-type: application/json" --data @$WORKDIR/slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage - - run: - name: Unit tests API - command: | - yarn --cwd redisinsight/api/ test:cov --ci - - save_cache: - <<: *apiDepsCacheKey - paths: - - ./redisinsight/api/node_modules - integration-tests-run: - executor: linux-executor-dlc - parameters: - rte: - description: Redis Test Environment name - type: string - build: - description: Backend build to run tests over - type: enum - default: local - enum: ['local', 'docker', 'saas'] - report: - description: Send report for test run to slack - type: boolean - default: false - redis_client: - description: Library to use for redis connection - type: string - default: "" - steps: - - checkout - - restore_cache: - <<: *apiDepsCacheKey - - when: - condition: - equal: [ 'docker', << parameters.build >> ] - steps: - - attach_workspace: - at: /tmp - - run: - name: Load built docker image from workspace - command: | - docker image load -i /tmp/release/docker/docker-linux-alpine.amd64.tar - - run: - name: Run tests - command: | - if [ << parameters.redis_client >> != "" ]; then - export RI_REDIS_CLIENTS_FORCE_STRATEGY=<< parameters.redis_client >> - fi - - ./redisinsight/api/test/test-runs/start-test-run.sh -r << parameters.rte >> -t << parameters.build >> - mkdir -p mkdir itest/coverages && mkdir -p itest/results - cp ./redisinsight/api/test/test-runs/coverage/test-run-result.json ./itest/results/<< parameters.rte >>.result.json - cp ./redisinsight/api/test/test-runs/coverage/test-run-result.xml ./itest/results/<< parameters.rte >>.result.xml - cp ./redisinsight/api/test/test-runs/coverage/test-run-coverage.json ./itest/coverages/<< parameters.rte >>.coverage.json - - when: - condition: - equal: [ true, << parameters.report >> ] - steps: - - run: - name: Send report - when: always - command: | - ITEST_NAME=<< parameters.rte >> node ./.circleci/itest-results.js - curl -H "Content-type: application/json" --data @itests.report.json -H "Authorization: Bearer $SLACK_TEST_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage - - store_test_results: - path: ./itest/results - - persist_to_workspace: - root: . - paths: - - ./itest/results/<< parameters.rte >>.result.json - - ./itest/coverages/<< parameters.rte >>.coverage.json - integration-tests-coverage: - executor: linux-executor - steps: - - checkout - - attach_workspace: - at: /tmp - - run: - name: Calculate coverage across all tests runs - command: | - sudo mkdir -p /usr/src/app - sudo cp -a ./redisinsight/api/. /usr/src/app/ - 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-dlc - parameters: - report: - description: Send report for test run to slack - type: boolean - default: false - parallelism: - description: Number of threads to run tests - type: integer - default: 1 - parallelism: << parameters.parallelism >> - steps: - - checkout - - node/install: - install-yarn: true - node-version: '20.15' - - attach_workspace: - at: . - - run: sudo apt-get install net-tools - - run: - name: Install WM - command: sudo apt install fluxbox - - run: - name: Run X11 - command: | - Xvfb :99 -screen 0 1920x1080x24 & - sleep 3 - fluxbox & - # - run: - # name: Clone mocked RDI server - # command: | - # git clone https://$GH_RDI_MOCKED_SERVER_KEY@github.com/RedisInsight/RDI_server_mocked.git tests/e2e/rte/rdi - - run: - name: .AppImage tests - command: | - cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/electron/**/*.e2e.ts" | circleci tests split --split-by=timings) && cd ../.. - .circleci/e2e/test.app-image.sh - - when: - condition: - equal: [ true, << parameters.report >> ] - steps: - - run: - name: Send report - when: always - command: | - APP_BUILD_TYPE="Electron (Linux)" node ./.circleci/e2e-results.js - 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 - parameters: - report: - description: Send report for test run to slack - type: boolean - default: false - parallelism: - description: Number of threads to run tests - type: integer - default: 1 - parallelism: << parameters.parallelism >> - steps: - - checkout - - attach_workspace: - at: . - - run: - command: | - nvm install 20.15 - nvm use 20.15 - npm install --global yarn - - run: - command: | - cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/electron/**/*.e2e.ts" | circleci tests split --split-by=timings) && cd ../.. - .circleci/e2e/test.exe.cmd - shell: bash.exe - - when: - condition: - equal: [ true, << parameters.report >> ] - steps: - - run: - name: Send report - when: always - command: | - APP_BUILD_TYPE="Electron (Windows)" node ./.circleci/e2e-results.js - 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 - 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-dlc - parameters: - build: - description: Backend build to run tests over - type: enum - default: local - enum: ['local', 'docker'] - report: - description: Send report for test run to slack - type: boolean - default: false - parallelism: - description: Number of threads to run tests - type: integer - default: 1 - parallelism: << parameters.parallelism >> - steps: - - checkout - - when: - condition: - equal: [ 'docker', << parameters.build >> ] - steps: - - attach_workspace: - at: /tmp - - run: - name: Load built docker image from workspace - command: | - docker image load -i /tmp/release/docker/docker-linux-alpine.amd64.tar - # - run: - # name: Clone mocked RDI server - # command: | - # git clone https://$GH_RDI_MOCKED_SERVER_KEY@github.com/RedisInsight/RDI_server_mocked.git tests/e2e/rte/rdi - - run: - name: Run tests - command: | - cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/web/**/*.e2e.ts" | circleci tests split --split-by=timings) && cd ../.. - TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \ - RI_SERVER_TLS_CERT="$RI_SERVER_TLS_CERT" \ - RI_SERVER_TLS_KEY="$RI_SERVER_TLS_KEY" \ - docker-compose \ - -f tests/e2e/rte.docker-compose.yml \ - -f tests/e2e/docker.web.docker-compose.yml \ - up --abort-on-container-exit --force-recreate --build - no_output_timeout: 5m - - when: - condition: - equal: [ 'local', << parameters.build >> ] - steps: - - run: - name: Run tests - command: | - cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/web/**/*.e2e.ts" | circleci tests split --split-by=timings) && cd ../.. - TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \ - RI_SERVER_TLS_CERT="$RI_SERVER_TLS_CERT" \ - RI_SERVER_TLS_KEY="$RI_SERVER_TLS_KEY" \ - docker-compose \ - -f tests/e2e/rte.docker-compose.yml \ - -f tests/e2e/local.web.docker-compose.yml \ - up --abort-on-container-exit --force-recreate - no_output_timeout: 5m - - when: - condition: - equal: [ true, << parameters.report >> ] - steps: - - run: - name: Send report - when: always - command: | - node ./.circleci/e2e-results.js - 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 - manual-build-validate: - executor: docker-node - parameters: - os: - type: string - default: "" - target: - type: string - default: "" - steps: - - checkout - - run: - command: | - node .circleci/build/manual-build-validate.js << parameters.os >> << parameters.target >> - setup-sign-certificates: - executor: linux-executor - steps: - - run: - name: Setup sign certificates - command: | - mkdir -p certs - echo "$CSC_P12_BASE64" | base64 -id > certs/mac-developer.p12 - echo "$CSC_MAC_INSTALLER_P12_BASE64" | base64 -id > certs/mac-installer.p12 - echo "$CSC_MAS_P12_BASE64" | base64 -id > certs/mas-distribution.p12 - echo "$WIN_CSC_PFX_BASE64" | base64 -id > certs/redislabs_win.pfx - - persist_to_workspace: - root: . - paths: - - certs - setup-build: - executor: docker - parameters: - env: - description: Build environment (stage || prod) - type: enum - default: stage - enum: [ 'dev', 'stage', 'prod' ] - steps: - - checkout - - run: - command: | - mkdir electron - - CURRENT_VERSION=$(jq -r ".version" redisinsight/package.json) - echo "Build version: $CURRENT_VERSION" - cp ./redisinsight/package.json ./electron/package.json - echo "$VERSION" > electron/version - exit 0 - - - persist_to_workspace: - root: /root/project - paths: - - electron - linux: - executor: linux-executor - resource_class: large - parameters: - env: - description: Build environment (stage || prod) - type: enum - default: stage - enum: ['stage', 'prod', 'dev'] - target: - description: Build target - type: string - default: "" - steps: - - checkout - - node/install: - install-yarn: true - node-version: '20.15' - - attach_workspace: - at: . - - run: - command: | - cp ./electron/package.json ./redisinsight/ - - run: - name: install dependencies - command: | - sudo apt-get update -y && sudo apt-get install -y rpm flatpak flatpak-builder ca-certificates - flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo - flatpak install flathub --no-deps --arch x86_64 --assumeyes \ - runtime/org.freedesktop.Sdk/x86_64/20.08 \ - runtime/org.freedesktop.Platform/x86_64/20.08 \ - org.electronjs.Electron2.BaseApp/x86_64/20.08 - - yarn --cwd redisinsight/api/ install --ignore-optional - yarn --cwd redisinsight/ install --ignore-optional - yarn install - no_output_timeout: 15m - - run: - name: Install plugins dependencies and build plugins - command: | - yarn build:statics - no_output_timeout: 15m - - run: - name: Build linux AppImage and deb - command: | - if [ << parameters.env >> == 'prod' ]; then - yarn package:prod - exit 0; - fi - - export RI_CLOUD_IDP_AUTHORIZE_URL=$RI_CLOUD_IDP_AUTHORIZE_URL_STAGE - export RI_CLOUD_IDP_TOKEN_URL=$RI_CLOUD_IDP_TOKEN_URL_STAGE - export RI_CLOUD_IDP_ISSUER=$RI_CLOUD_IDP_ISSUER_STAGE - export RI_CLOUD_IDP_CLIENT_ID=$RI_CLOUD_IDP_CLIENT_ID_STAGE - export RI_CLOUD_IDP_REDIRECT_URI=$RI_CLOUD_IDP_REDIRECT_URI_STAGE - export RI_CLOUD_IDP_GOOGLE_ID=$RI_CLOUD_IDP_GOOGLE_ID_STAGE - export RI_CLOUD_IDP_GH_ID=$RI_CLOUD_IDP_GH_ID_STAGE - export RI_CLOUD_API_URL=$RI_CLOUD_API_URL_STAGE - export RI_CLOUD_CAPI_URL=$RI_CLOUD_CAPI_URL_STAGE - export RI_FEATURES_CONFIG_URL=$RI_FEATURES_CONFIG_URL_STAGE - - if [ << parameters.env >> == 'stage' ]; then - RI_UPGRADES_LINK=$RI_UPGRADES_LINK_STAGE RI_SEGMENT_WRITE_KEY=$RI_SEGMENT_WRITE_KEY_STAGE yarn package:stage --linux << parameters.target >> - exit 0; - fi - - RI_UPGRADES_LINK='' RI_SEGMENT_WRITE_KEY='' yarn package:stage --linux << parameters.target >> - - persist_to_workspace: - root: . - paths: - - release/Redis-Insight*.deb - - release/Redis-Insight*.rpm - - release/Redis-Insight*.AppImage - - release/Redis-Insight*.flatpak - - release/Redis-Insight*.snap - - release/*-linux.yml - macosx: - executor: macos - resource_class: macos.m1.medium.gen1 - parameters: - env: - description: Build environment (stage || prod) - type: enum - default: stage - enum: ['stage', 'prod', 'dev'] - redisstack: - description: Build RedisStack archives - type: boolean - default: true - target: - description: Build target - type: string - default: "" - steps: - - checkout - - node/install: - node-version: '20.15' - - attach_workspace: - at: . - - run: - command: | - cp ./electron/package.json ./redisinsight/ - - <<: *keychain - - run: - name: install dependencies - command: | - yarn install - yarn --cwd redisinsight/api/ install - yarn --cwd redisinsight/ install - yarn build:statics - no_output_timeout: 15m - - run: - name: Build macos dmg - command: | - unset CSC_LINK - export CSC_IDENTITY_AUTO_DISCOVERY=true - export CSC_KEYCHAIN=redisinsight.keychain - - if [ << parameters.env >> == 'prod' ]; then - yarn package:prod - yarn package:mas - rm -rf release/mac - mv release/mas-universal/Redis-Insight-mac-universal-mas.pkg release/Redis-Insight-mac-universal-mas.pkg - exit 0; - fi - - export RI_CLOUD_IDP_AUTHORIZE_URL=$RI_CLOUD_IDP_AUTHORIZE_URL_STAGE - export RI_CLOUD_IDP_TOKEN_URL=$RI_CLOUD_IDP_TOKEN_URL_STAGE - export RI_CLOUD_IDP_ISSUER=$RI_CLOUD_IDP_ISSUER_STAGE - export RI_CLOUD_IDP_CLIENT_ID=$RI_CLOUD_IDP_CLIENT_ID_STAGE - export RI_CLOUD_IDP_REDIRECT_URI=$RI_CLOUD_IDP_REDIRECT_URI_STAGE - export RI_CLOUD_IDP_GOOGLE_ID=$RI_CLOUD_IDP_GOOGLE_ID_STAGE - export RI_CLOUD_IDP_GH_ID=$RI_CLOUD_IDP_GH_ID_STAGE - export RI_CLOUD_API_URL=$RI_CLOUD_API_URL_STAGE - export RI_CLOUD_CAPI_URL=$RI_CLOUD_CAPI_URL_STAGE - export RI_UPGRADES_LINK='' - export RI_SEGMENT_WRITE_KEY='' - export RI_FEATURES_CONFIG_URL=$RI_FEATURES_CONFIG_URL_STAGE - - if [ << parameters.env >> == 'stage' ]; then - export RI_UPGRADES_LINK=$RI_UPGRADES_LINK_STAGE - export RI_SEGMENT_WRITE_KEY=$RI_SEGMENT_WRITE_KEY_STAGE - fi - - # handle manual builds - if [ << parameters.target >> ]; then - yarn package:stage --mac << parameters.target >> - rm -rf release/mac - exit 0; - fi - - yarn package:stage && yarn package:mas - rm -rf release/mac - mv release/mas-universal/Redis-Insight-mac-universal-mas.pkg release/Redis-Insight-mac-universal-mas.pkg - no_output_timeout: 60m - - when: - condition: - equal: [ true, << parameters.redisstack >> ] - steps: - - run: - name: Repack dmg to tar - command: | - ARCH=x64 ./.circleci/redisstack/dmg.repack.sh - ARCH=arm64 ./.circleci/redisstack/dmg.repack.sh - - persist_to_workspace: - root: . - paths: - - release/Redis-Insight*.zip - - release/Redis-Insight*.dmg - - release/Redis-Insight*.dmg.blockmap - - release/Redis-Insight*.pkg - - release/*-mac.yml - - release/redisstack - windows: - executor: - name: win/default - parameters: - env: - description: Build environment (stage || prod) - type: enum - default: stage - enum: ['stage', 'prod', 'dev'] - target: - description: Build target - type: string - default: "" - steps: - - checkout - - attach_workspace: - at: . - - run: - command: | - cp ./electron/package.json ./redisinsight/ - - run: - name: Build windows exe - command: | - nvm install 20.15 - nvm use 20.15 - npm install --global yarn - - # set ALL_REDIS_COMMANDS=$(curl $ALL_REDIS_COMMANDS_RAW_URL) - # install dependencies - yarn install - yarn --cwd redisinsight/api/ install - yarn --cwd redisinsight/ install - yarn build:statics:win - - if [ << parameters.env >> == 'prod' ]; then - yarn package:prod - rm -rf release/win-unpacked - exit 0; - fi - - export RI_CLOUD_IDP_AUTHORIZE_URL=$RI_CLOUD_IDP_AUTHORIZE_URL_STAGE - export RI_CLOUD_IDP_TOKEN_URL=$RI_CLOUD_IDP_TOKEN_URL_STAGE - export RI_CLOUD_IDP_ISSUER=$RI_CLOUD_IDP_ISSUER_STAGE - export RI_CLOUD_IDP_CLIENT_ID=$RI_CLOUD_IDP_CLIENT_ID_STAGE - export RI_CLOUD_IDP_REDIRECT_URI=$RI_CLOUD_IDP_REDIRECT_URI_STAGE - export RI_CLOUD_IDP_GOOGLE_ID=$RI_CLOUD_IDP_GOOGLE_ID_STAGE - export RI_CLOUD_IDP_GH_ID=$RI_CLOUD_IDP_GH_ID_STAGE - export RI_CLOUD_API_URL=$RI_CLOUD_API_URL_STAGE - export RI_CLOUD_CAPI_URL=$RI_CLOUD_CAPI_URL_STAGE - export RI_FEATURES_CONFIG_URL=$RI_FEATURES_CONFIG_URL_STAGE - - if [ << parameters.env >> == 'stage' ]; then - RI_UPGRADES_LINK=$RI_UPGRADES_LINK_STAGE RI_SEGMENT_WRITE_KEY=$RI_SEGMENT_WRITE_KEY_STAGE yarn package:stage --win << parameters.target >> - else - RI_UPGRADES_LINK='' RI_SEGMENT_WRITE_KEY='' yarn package:stage --win << parameters.target >> - fi - - rm -rf release/win-unpacked - shell: bash.exe - no_output_timeout: 20m - - persist_to_workspace: - root: . - paths: - - release/Redis-Insight*.exe - - release/Redis-Insight*.exe.blockmap - - release/*.yml - virustotal-file: - executor: linux-executor - parameters: - ext: - description: File extension - type: string - steps: - - checkout - - attach_workspace: - at: /tmp/release - - run: - name: export FILE_NAME environment variable - command: | - echo 'export FILE_NAME="Redis-Insight*<< parameters.ext >>"' >> $BASH_ENV - - <<: *fileScan - - <<: *validate - virustotal-url: - executor: linux-executor - parameters: - fileName: - description: File name - type: string - steps: - - checkout - - run: - name: export URL environment variable - command: | - echo 'export URL="https://download.redisinsight.redis.com/latest/<< parameters.fileName >>"' >> $BASH_ENV - echo 'export BUILD_NAME="<< parameters.fileName >>"' >> $BASH_ENV - - <<: *urlScan - - <<: *validate - - <<: *virustotalReport - virustotal-report: - executor: linux-executor - steps: - - checkout - - run: - name: Send virustotal passed report - command: | - echo 'export VIRUS_CHECK_FAILED=0' >> $BASH_ENV - echo 'export SKIP_VIRUSTOTAL_REPORT=false' >> $BASH_ENV - - <<: *virustotalReport - docker: - executor: linux-executor - parameters: - env: - type: enum - default: staging - enum: [ 'staging', 'production' ] - steps: - - checkout - - node/install: - install-yarn: true - node-version: '20.15' - - run: - name: Install dependencies - command: | - sudo apt-get update -y && sudo apt-get install -y ca-certificates qemu-user-static - - run: - name: Build sources - command: ./.circleci/build/build.sh - - run: - name: Build web archives - command: | - unset npm_config_keytar_binary_host_mirror - unset npm_config_node_sqlite3_binary_host_mirror - - # Docker sources - PLATFORM=linux ARCH=x64 LIBC=musl .circleci/build/build_modules.sh - PLATFORM=linux ARCH=arm64 LIBC=musl .circleci/build/build_modules.sh - - # Redis Stack + VSC Linux - PLATFORM=linux ARCH=x64 .circleci/build/build_modules.sh - PLATFORM=linux ARCH=arm64 .circleci/build/build_modules.sh - - # VSC Darwin - PLATFORM=darwin ARCH=x64 .circleci/build/build_modules.sh - PLATFORM=darwin ARCH=arm64 .circleci/build/build_modules.sh - # VSC Windows - PLATFORM=win32 ARCH=x64 .circleci/build/build_modules.sh - - run: - name: Build Docker (x64, arm64) - command: | - TELEMETRY=$RI_SEGMENT_WRITE_KEY_DEV - - if [ << parameters.env >> == 'production' ]; then - TELEMETRY=$RI_SEGMENT_WRITE_KEY - fi - - if [ << parameters.env >> == 'staging' ]; then - TELEMETRY=$RI_SEGMENT_WRITE_KEY_STAGE - fi - - # Build alpine x64 image - docker buildx build \ - -f .circleci/build/build.Dockerfile \ - --platform linux/amd64 \ - --build-arg DIST=release/web/Redis-Insight-web-linux-musl.x64.tar.gz \ - --build-arg NODE_ENV=<< parameters.env >> \ - --build-arg RI_SEGMENT_WRITE_KEY="$TELEMETRY" \ - -t redisinsight:amd64 \ - . - - # Build alpine arm64 image - docker buildx build \ - -f .circleci/build/build.Dockerfile \ - --platform linux/arm64 \ - --build-arg DIST=release/web/Redis-Insight-web-linux-musl.arm64.tar.gz \ - --build-arg NODE_ENV=<< parameters.env >> \ - --build-arg RI_SEGMENT_WRITE_KEY="$TELEMETRY" \ - -t redisinsight:arm64 \ - . - - mkdir -p release/docker - docker image save -o release/docker/docker-linux-alpine.amd64.tar redisinsight:amd64 - docker image save -o release/docker/docker-linux-alpine.arm64.tar redisinsight:arm64 - - persist_to_workspace: - root: . - paths: - - ./release - - licenses-check: - executor: linux-executor - steps: - - checkout - - restore_cache: - <<: *uiDepsCacheKey - <<: *apiDepsCacheKey - - run: - name: Run install all dependencies - command: | - yarn install - yarn --cwd redisinsight/api install - yarn --cwd tests/e2e install - # Install plugins dependencies - export pluginsOnlyInstall=1 - yarn build:statics - - run: - name: Generate licenses csv files and send csv data to google sheet - command: | - npm i -g license-checker - - echo "$GOOGLE_ACCOUNT_SERVICE_KEY_BASE64" | base64 -id > gasKey.json - SPREADSHEET_ID=$GOOGLE_SPREADSHEET_DEPENDENCIES_ID node .circleci/deps-licenses-report.js - - store_artifacts: - path: licenses - destination: licenses - - # Release jobs - store-build-artifacts: - executor: linux-executor - steps: - - attach_workspace: - at: . - - store_artifacts: - path: release - destination: release - release-aws-private-dev: - executor: linux-executor - steps: - - checkout - - attach_workspace: - at: . - - run: - name: publish - command: | - rm release/._* ||: - chmod +x .circleci/build/sum_sha256.sh - .circleci/build/sum_sha256.sh - applicationVersion=$(jq -r '.version' redisinsight/package.json) - - aws s3 cp release/ s3://${AWS_BUCKET_NAME_TEST}/private/builds/${CIRCLE_BUILD_NUM} --recursive - release-aws-private: - executor: linux-executor - steps: - - checkout - - attach_workspace: - at: . - - store_artifacts: - path: release - destination: release - - run: - name: prepare release - command: | - rm release/._* ||: - - run: - name: publish - command: | - chmod +x .circleci/build/sum_sha256.sh - .circleci/build/sum_sha256.sh - applicationVersion=$(jq -r '.version' redisinsight/package.json) - - aws s3 cp release/ s3://${AWS_BUCKET_NAME}/private/${applicationVersion} --recursive - - release-docker: - executor: linux-executor - steps: - - checkout - - attach_workspace: - at: . - - run: - name: Release docker images - command: | - appVersion=$(jq -r '.version' redisinsight/package.json) - - docker login -u $DOCKER_USER -p $DOCKER_PASS - - ./.circleci/build/release-docker.sh \ - -d redisinsight \ - -r $DOCKER_REPO \ - -v $appVersion - - docker login -u $DOCKER_V1_USER -p $DOCKER_V1_PASS - - ./.circleci/build/release-docker.sh \ - -d redisinsight \ - -r $DOCKER_V1_REPO \ - -v $appVersion - - publish-prod-aws: - executor: linux-executor - steps: - - checkout - - run: - name: Init variables - command: | - latestYmlFileName="latest.yml" - downloadLatestFolderPath="public/latest" - upgradeLatestFolderPath="public/upgrades" - releasesFolderPath="public/releases" - appName=$(jq -r '.productName' electron-builder.json) - appVersion=$(jq -r '.version' redisinsight/package.json) - - echo "export downloadLatestFolderPath=${downloadLatestFolderPath}" >> $BASH_ENV - echo "export upgradeLatestFolderPath=${upgradeLatestFolderPath}" >> $BASH_ENV - echo "export releasesFolderPath=${releasesFolderPath}" >> $BASH_ENV - echo "export applicationName=${appName}" >> $BASH_ENV - echo "export applicationVersion=${appVersion}" >> $BASH_ENV - echo "export appFileName=Redis-Insight" >> $BASH_ENV - - # download latest.yml file to get last public version - aws s3 cp s3://${AWS_BUCKET_NAME}/${downloadLatestFolderPath}/${latestYmlFileName} . - - versionLine=$(head -1 ${latestYmlFileName}) - versionLineArr=(${versionLine/:// }) - previousAppVersion=${versionLineArr[1]} - - echo "export previousApplicationVersion=${previousAppVersion}" >> $BASH_ENV - - - run: - name: Publish AWS S3 - command: | - # remove previous build from the latest directory /public/latest - aws s3 rm s3://${AWS_BUCKET_NAME}/${downloadLatestFolderPath} --recursive - - # remove previous build from the upgrade directory /public/upgrades - aws s3 rm s3://${AWS_BUCKET_NAME}/${upgradeLatestFolderPath} --recursive - - # copy current version apps for download to /public/latest - aws s3 cp s3://${AWS_BUCKET_NAME}/private/${applicationVersion}/ \ - s3://${AWS_BUCKET_NAME}/${downloadLatestFolderPath} --recursive --exclude "*.zip" - - # copy current version apps for upgrades to /public/upgrades - aws s3 cp s3://${AWS_BUCKET_NAME}/private/${applicationVersion}/ \ - s3://${AWS_BUCKET_NAME}/${upgradeLatestFolderPath} --recursive - - # !MOVE current version apps to releases folder /public/releases - aws s3 mv s3://${AWS_BUCKET_NAME}/private/${applicationVersion}/ \ - s3://${AWS_BUCKET_NAME}/${releasesFolderPath}/${applicationVersion} --recursive - - # invalidate cloudfront cash - aws cloudfront create-invalidation --distribution-id ${AWS_DISTRIBUTION_ID} --paths "/*" - - - run: - name: Add tags for all objects and create S3 metrics - command: | - - # declare all tags - declare -A tag0=( - [arch]='x64' - [platform]='macos' - [objectDownload]=${appFileName}'-mac-x64.dmg' - [objectUpgrade]=${appFileName}'-mac-x64.zip' - ) - - declare -A tag1=( - [arch]='arm64' - [platform]='macos' - [objectDownload]=${appFileName}'-mac-arm64.dmg' - [objectUpgrade]=${appFileName}'-mac-arm64.zip' - ) - - declare -A tag2=( - [arch]='x64' - [platform]='windows' - [objectDownload]=${appFileName}'-win-installer.exe' - ) - - declare -A tag3=( - [arch]='x64' - [platform]='linux_AppImage' - [objectDownload]=${appFileName}'-linux-x86_64.AppImage' - ) - - declare -A tag4=( - [arch]='x64' - [platform]='linux_deb' - [objectDownload]=${appFileName}'-linux-amd64.deb' - ) - - declare -A tag5=( - [arch]='x64' - [platform]='linux_rpm' - [objectDownload]=${appFileName}'-linux-x86_64.rpm' - ) - - # loop for add all tags to each app and create metrics - declare -n tag - for tag in ${!tag@}; do - - designation0="downloads" - designation1="upgrades" - - id0="${tag[platform]}_${tag[arch]}_${designation0}_${applicationVersion}" - id1="${tag[platform]}_${tag[arch]}_${designation1}_${applicationVersion}" - - # add tags to each app for download - aws s3api put-object-tagging \ - --bucket ${AWS_BUCKET_NAME} \ - --key ${downloadLatestFolderPath}/${tag[objectDownload]} \ - --tagging '{"TagSet": [{ "Key": "version", "Value": "'"${applicationVersion}"'" }, {"Key": "platform", "Value": "'"${tag[platform]}"'"}, {"Key": "arch", "Value": "'"${tag[arch]}"'"}, { "Key": "designation", "Value": "'"${designation0}"'" }]}' - - # add tags to each app for upgrades - aws s3api put-object-tagging \ - --bucket ${AWS_BUCKET_NAME} \ - --key ${upgradeLatestFolderPath}/${tag[objectUpgrade]:=${tag[objectDownload]}} \ - --tagging '{"TagSet": [{ "Key": "version", "Value": "'"${applicationVersion}"'" }, {"Key": "platform", "Value": "'"${tag[platform]}"'"}, {"Key": "arch", "Value": "'"${tag[arch]}"'"}, { "Key": "designation", "Value": "'"${designation1}"'" }]}' - - # Create metrics for all tags for downloads to S3 - aws s3api put-bucket-metrics-configuration \ - --bucket ${AWS_BUCKET_NAME} \ - --id ${id0} \ - --metrics-configuration '{"Id": "'"${id0}"'", "Filter": {"And": {"Tags": [{"Key": "platform", "Value": "'"${tag[platform]}"'"}, {"Key": "arch", "Value": "'"${tag[arch]}"'"}, {"Key": "designation", "Value": "'"${designation0}"'"}, {"Key": "version", "Value": "'"${applicationVersion}"'"} ]}}}' - - # Create metrics for all tags for upgrades to S3 - aws s3api put-bucket-metrics-configuration \ - --bucket ${AWS_BUCKET_NAME} \ - --id ${id1} \ - --metrics-configuration '{"Id": "'"${id1}"'", "Filter": {"And": {"Tags": [{"Key": "platform", "Value": "'"${tag[platform]}"'"}, {"Key": "arch", "Value": "'"${tag[arch]}"'"}, {"Key": "designation", "Value": "'"${designation1}"'"}, {"Key": "version", "Value": "'"${applicationVersion}"'"}]}}}' - - done workflows: - # FE Unit tests for "fe/feature" or "fe/bugfix" branches only - frontend-tests: - <<: *ignore-for-manual-build - jobs: - - 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: - <<: *ignore-for-manual-build - jobs: - - unit-tests-api: - name: UTest - API - filters: - branches: - only: - - /^be/feature.*/ - - /^be/bugfix.*/ - - integration-tests-run: - redis_client: << pipeline.parameters.redis_client >> - 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.*/ - - setup-sign-certificates: - name: Setup sign certificates (stage) - requires: - - Start E2E Tests - - setup-build: - name: Setup build (stage) - requires: - - Setup sign certificates (stage) - - linux: - name: Build app - Linux (stage) - env: stage - target: AppImage - requires: - - Setup build (stage) - - docker: - name: Build docker image - requires: - - Start E2E Tests - - e2e-tests: - name: E2ETest - build: docker - parallelism: 4 - requires: - - Build docker image - - e2e-app-image: - name: E2ETest (AppImage) - parallelism: 1 - requires: - - Build app - Linux (stage) - # Workflow for feature, bugfix, main branches - feature-main-branch: - <<: *ignore-for-manual-build - 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 - redis_client: - - << pipeline.parameters.redis_client >> - name: ITest - << matrix.rte >> (code) - requires: - - Start All Tests - - integration-tests-coverage: - name: ITest - Final coverage - requires: - - itest-code - # E2E tests - - setup-sign-certificates: - name: Setup sign certificates (stage) - requires: - - Start All Tests - - setup-build: - name: Setup build (stage) - requires: - - Setup sign certificates (stage) - - linux: - name: Build app - Linux (stage) - requires: - - Setup build (stage) - - docker: - name: Build docker image - requires: - - Start All Tests - - e2e-tests: - name: E2ETest - build: docker - parallelism: 4 - requires: - - Build docker image - - e2e-app-image: - name: E2ETest (AppImage) - parallelism: 2 - requires: - - Build app - Linux (stage) - # Approve to build - - approve: - name: Build App - type: approval - requires: - - UTest - UI - - UTest - API - - ITest - Final coverage - filters: - branches: - only: - - /^e2e/feature.*/ - - /^e2e/bugfix.*/ - # Manual builds using web UI - manual-build-linux: - when: << pipeline.parameters.linux >> - jobs: - - manual-build-validate: - name: Validating build parameters - os: linux - target: << pipeline.parameters.linux >> - - setup-sign-certificates: - name: Setup sign certificates (<< pipeline.parameters.env >>) - requires: - - Validating build parameters - - setup-build: - name: Setup build (<< pipeline.parameters.env >>) - env: << pipeline.parameters.env >> - requires: - - Setup sign certificates (<< pipeline.parameters.env >>) - - linux: - name: Build app - Linux (<< pipeline.parameters.env >>) - env: << pipeline.parameters.env >> - target: << pipeline.parameters.linux >> - requires: - - Setup build (<< pipeline.parameters.env >>) - - store-build-artifacts: - name: Store build artifacts (<< pipeline.parameters.env >>) - requires: - - Build app - Linux (<< pipeline.parameters.env >>) - manual-build-mac: - when: << pipeline.parameters.mac >> - jobs: - - manual-build-validate: - name: Validating build parameters - os: mac - target: << pipeline.parameters.mac >> - - setup-sign-certificates: - name: Setup sign certificates (<< pipeline.parameters.env >>) - requires: - - Validating build parameters - - setup-build: - name: Setup build (<< pipeline.parameters.env >>) - env: << pipeline.parameters.env >> - requires: - - Setup sign certificates (<< pipeline.parameters.env >>) - - macosx: - name: Build app - MacOS (<< pipeline.parameters.env >>) - env: << pipeline.parameters.env >> - redisstack: false - target: << pipeline.parameters.mac >> - requires: - - Setup build (<< pipeline.parameters.env >>) - - store-build-artifacts: - name: Store build artifacts (<< pipeline.parameters.env >>) - requires: - - Build app - MacOS (<< pipeline.parameters.env >>) - manual-build-windows: - when: << pipeline.parameters.windows >> - jobs: - - manual-build-validate: - name: Validating build parameters - os: windows - target: << pipeline.parameters.windows >> - - setup-sign-certificates: - name: Setup sign certificates (<< pipeline.parameters.env >>) - requires: - - Validating build parameters - - setup-build: - name: Setup build (<< pipeline.parameters.env >>) - env: << pipeline.parameters.env >> - requires: - - Setup sign certificates (<< pipeline.parameters.env >>) - - windows: - name: Build app - Windows (<< pipeline.parameters.env >>) - env: << pipeline.parameters.env >> - target: << pipeline.parameters.windows >> - requires: - - Setup build (<< pipeline.parameters.env >>) - - store-build-artifacts: - name: Store build artifacts (<< pipeline.parameters.env >>) - requires: - - Build app - Windows (<< pipeline.parameters.env >>) - manual-build-docker: - when: << pipeline.parameters.docker >> - jobs: - - manual-build-validate: - name: Validating build parameters - os: docker - target: << pipeline.parameters.docker >> - - setup-sign-certificates: - name: Setup sign certificates (<< pipeline.parameters.env >>) - requires: - - Validating build parameters - - setup-build: - name: Setup build (<< pipeline.parameters.env >>) - env: << pipeline.parameters.env >> - requires: - - Setup sign certificates (<< pipeline.parameters.env >>) - - docker: - name: Build docker images (<< pipeline.parameters.env >>) - requires: - - Setup build (<< pipeline.parameters.env >>) - - store-build-artifacts: - name: Store build artifacts (<< pipeline.parameters.env >>) - requires: - - Build docker images (<< pipeline.parameters.env >>) - - # build electron app (dev) from "build" branches - build: - <<: *ignore-for-manual-build - jobs: - - setup-sign-certificates: - name: Setup sign certificates (dev) - filters: - branches: - only: - - /^build.*/ - - setup-build: - name: Setup build (dev) - env: dev - requires: - - Setup sign certificates (dev) - - linux: - name: Build app - Linux (dev) - env: dev - requires: &devBuildRequire - - Setup build (dev) - - macosx: - name: Build app - MacOS (dev) - env: dev - requires: *devBuildRequire - - windows: - name: Build app - Windows (dev) - env: dev - requires: *devBuildRequire - - docker: - name: Build docker images (dev) - requires: *devBuildRequire - - store-build-artifacts: - name: Store build artifacts (dev) - requires: - - Build app - Linux (dev) - - Build app - MacOS (dev) - - Build app - Windows (dev) - - Build docker images (dev) - - # build electron app (dev) for internal use only - internal: - <<: *ignore-for-manual-build + # Placeholder workflow + placeholder: jobs: - - setup-sign-certificates: - name: Setup sign certificates (dev) + - placeholder-job: + name: Placeholder filters: branches: only: - - /^internal.*/ - - setup-build: - name: Setup build (dev) - env: dev - requires: - - Setup sign certificates (dev) - - linux: - name: Build app - Linux (dev) - env: dev - requires: &devBuildRequire - - Setup build (dev) - - macosx: - name: Build app - MacOS (dev) - env: dev - requires: *devBuildRequire - - windows: - name: Build app - Windows (dev) - env: dev - requires: *devBuildRequire - - docker: - name: Build docker images (dev) - requires: *devBuildRequire - # release to private AWS (dev) - - release-aws-private-dev: - name: Release private AWS dev - requires: - - Build app - Linux (dev) - - Build app - MacOS (dev) - - Build app - Windows (dev) - - Build docker images (dev) - - # Main workflow for release/* and latest branches only - release: - <<: *ignore-for-manual-build - 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 - redis_client: - - << pipeline.parameters.redis_client >> - name: ITest - << matrix.rte >> (code) - filters: *releaseAndLatestFilter - - integration-tests-coverage: - name: ITest - Final coverage - requires: - - itest-code - - # ================== STAGE ================== - # prebuild (stage) - - setup-sign-certificates: - name: Setup sign certificates (stage) - requires: - - UTest - UI - - UTest - API - - ITest - Final coverage - <<: *stageFilter - - setup-build: - name: Setup build (stage) - requires: - - Setup sign certificates (stage) - # build electron app (stage) - - linux: - name: Build app - Linux (stage) - requires: &stageElectronBuildRequires - - Setup build (stage) - - macosx: - name: Build app - MacOS (stage) - requires: *stageElectronBuildRequires - - windows: - name: Build app - Windows (stage) - requires: *stageElectronBuildRequires - - docker: - name: Build docker images (stage) - requires: *stageElectronBuildRequires - # e2e desktop tests on AppImage build - - e2e-app-image: - name: E2ETest (AppImage) - parallelism: 2 - requires: - - Build app - Linux (stage) - # e2e docker tests - - e2e-tests: - name: E2ETest - build: docker - parallelism: 4 - requires: - - Build docker images (stage) - - store-build-artifacts: - name: Store build artifacts (stage) - requires: - - Build app - Linux (stage) - - Build app - MacOS (stage) - - Build app - Windows (stage) - - Build docker images (stage) - - # Needs approval from QA team that build was tested before merging to latest - - qa-approve: - name: Approved by QA team - type: approval - requires: - - Build app - Linux (stage) - - Build app - MacOS (stage) - - Build app - Windows (stage) - - Build docker images (stage) - - # ================== PROD ================== - # build and release electron app (prod) - - setup-sign-certificates: - name: Setup sign certificates (prod) - requires: - - UTest - UI - - UTest - API - - ITest - Final coverage - <<: *prodFilter - - setup-build: - name: Setup build (prod) - env: prod - requires: - - Setup sign certificates (prod) - - linux: - name: Build app - Linux (prod) - env: prod - requires: &prodElectronBuildRequires - - Setup build (prod) - - macosx: - name: Build app - MacOS (prod) - env: prod - requires: *prodElectronBuildRequires - - windows: - name: Build app - Windows (prod) - env: prod - requires: *prodElectronBuildRequires - - docker: - name: Build docker images (prod) - env: production - requires: *prodElectronBuildRequires - # e2e desktop tests on AppImage build - - e2e-app-image: - name: E2ETest (AppImage) - parallelism: 2 - requires: - - Build app - Linux (prod) - # e2e docker tests - - e2e-tests: - name: E2ETest - build: docker - parallelism: 4 - requires: - - Build docker images (prod) - # virus check all electron apps (prod) - - virustotal-file: - name: Virus check - AppImage (prod) - ext: .AppImage - requires: - - Build app - Linux (prod) - - virustotal-file: - name: Virus check - deb (prod) - ext: .deb - requires: - - Build app - Linux (prod) - - virustotal-file: - name: Virus check - rpm (prod) - ext: .rpm - requires: - - Build app - Linux (prod) - - virustotal-file: - name: Virus check - snap (prod) - ext: .snap - requires: - - Build app - Linux (prod) - - virustotal-file: - name: Virus check x64 - dmg (prod) - ext: -x64.dmg - requires: - - Build app - MacOS (prod) - - virustotal-file: - name: Virus check arm64 - dmg (prod) - ext: -arm64.dmg - requires: - - Build app - MacOS (prod) - - virustotal-file: - name: Virus check MAS - pkg (prod) - ext: -mas.pkg - requires: - - Build app - MacOS (prod) - - virustotal-file: - name: Virus check - exe (prod) - ext: .exe - requires: - - Build app - Windows (prod) - # upload release to prerelease AWS folder - - release-aws-private: - name: Release AWS S3 Private (prod) - requires: - - Virus check - AppImage (prod) - - Virus check - deb (prod) - - Virus check - rpm (prod) - - Virus check - snap (prod) - - Virus check x64 - dmg (prod) - - Virus check arm64 - dmg (prod) - - Virus check MAS - pkg (prod) - - Virus check - exe (prod) - - Build docker images (prod) - # Manual approve for publish release - - approve-publish: - name: Approve Publish Release (prod) - type: approval - 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" - - release-docker: - name: Release docker images - requires: - - Approve Publish Release (prod) - <<: *prodFilter # double check for "latest" - # Nightly tests - nightly: - triggers: - - schedule: - cron: '0 0 * * *' - filters: - branches: - only: - - main - jobs: - # build docker image - - docker: - name: Build docker image - # build desktop app - - setup-sign-certificates: - name: Setup sign certificates (stage) - - setup-build: - name: Setup build (stage) - requires: - - Setup sign certificates (stage) - - linux: - name: Build app - Linux (stage) - requires: - - Setup build (stage) - # - windows: - # name: Build app - Windows (stage) - # requires: - # - Setup build (stage) - # integration tests on docker image build - - integration-tests-run: - matrix: - alias: itest-docker - parameters: - rte: *iTestsNames - build: ['docker'] - report: [true] - name: ITest - << matrix.rte >> (docker) - requires: - - Build docker image - # e2e web tests on docker image build - - e2e-tests: - name: E2ETest - Nightly - parallelism: 4 - build: docker - report: true - requires: - - Build docker image - # e2e desktop tests on AppImage build - - e2e-app-image: - name: E2ETest (AppImage) - Nightly - parallelism: 2 - report: true - requires: - - Build app - Linux (stage) - - - virustotal-url: - name: Virus check - AppImage (nightly) - fileName: Redis-Insight-linux-x86_64.AppImage - - virustotal-url: - name: Virus check - deb (nightly) - fileName: Redis-Insight-linux-amd64.deb - - virustotal-url: - name: Virus check - rpm (nightly) - fileName: Redis-Insight-linux-x86_64.rpm - - virustotal-url: - name: Virus check - snap (nightly) - fileName: Redis-Insight-linux-amd64.snap - - virustotal-url: - name: Virus check x64 - dmg (nightly) - fileName: Redis-Insight-mac-x64.dmg - - virustotal-url: - name: Virus check arm64 - dmg (nightly) - fileName: Redis-Insight-mac-arm64.dmg - - virustotal-url: - name: Virus check MAS - pkg (nightly) - fileName: Redis-Insight-mac-universal-mas.pkg - - virustotal-url: - name: Virus check - exe (nightly) - fileName: Redis-Insight-win-installer.exe - - virustotal-report: - name: Virus check report (prod) - requires: - - Virus check - AppImage (nightly) - - Virus check - deb (nightly) - - Virus check - rpm (nightly) - - Virus check - snap (nightly) - - Virus check x64 - dmg (nightly) - - Virus check arm64 - dmg (nightly) - - Virus check MAS - pkg (nightly) - - Virus check - exe (nightly) - - # # e2e desktop tests on exe build - # - e2e-exe: - # name: E2ETest (exe) - Nightly - # parallelism: 4 - # report: true - # requires: - # - Build app - Windows (stage) - - weekly: - triggers: - - schedule: - cron: '0 0 * * 1' - filters: - branches: - only: - - main - jobs: - # Process all licenses - - licenses-check: - name: Process licenses of packages + - placeholder diff --git a/.circleci/config.yml.backup b/.circleci/config.yml.backup new file mode 100644 index 0000000000..2a23560fd3 --- /dev/null +++ b/.circleci/config.yml.backup @@ -0,0 +1,1868 @@ +version: 2.1 + +aliases: + keychain: &keychain + run: + name: Add cert to the keychain + command: | + security create-keychain -p mysecretpassword $KEYCHAIN + security default-keychain -s $KEYCHAIN + security unlock-keychain -p mysecretpassword $KEYCHAIN + security set-keychain-settings -u -t 10000000 $KEYCHAIN + security import certs/mac-developer.p12 -k $KEYCHAIN -P "$CSC_KEY_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productbuild + security import certs/mas-distribution.p12 -k $KEYCHAIN -P "$CSC_MAS_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productbuild + security import certs/mac-installer.p12 -k $KEYCHAIN -P "$CSC_MAC_INSTALLER_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productbuild + security set-key-partition-list -S apple-tool:,apple: -s -k mysecretpassword $KEYCHAIN + environment: + KEYCHAIN: redisinsight.keychain + import: &import + run: + name: User certutil to import certificate + command: certutil -p %WIN_CSC_KEY_PASSWORD% -importpfx certs\redislabs_win.pfx + shell: cmd.exe + sign: &sign + run: + name: Sign application + command: | + $filePath = $(Get-ChildItem release -Filter Redis-Insight*.exe | % { $_.FullName }) + $filePathWithQuotes = '"{0}"' -f $filePath + & "C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x86\signtool.exe" sign /a /sm /n "Redis Labs Inc." /fd sha256 /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /v $FilePathWithQuotes + shell: powershell.exe + fileScan: &fileScan + run: + name: Virustotal file scan + command: &virusfilescan | + uploadUrl=$(curl -sq -XGET https://www.virustotal.com/api/v3/files/upload_url -H "x-apikey: $VIRUSTOTAL_API_KEY" | jq -r '.data') + uploadFile=$("/usr/bin/find" /tmp/release -name ${FILE_NAME}) + echo "File to upload: ${uploadFile}" + analysedId=$(curl -sq -XPOST "${uploadUrl}" -H "x-apikey: $VIRUSTOTAL_API_KEY" --form file=@"${uploadFile}" | jq -r '.data.id') + if [ $analysedId == "null" ]; then + echo 'Status is null, something went wrong'; exit 1; + fi + echo "export ANALYZED_ID=${analysedId}" >> $BASH_ENV + echo "Virustotal Analyzed id: ${analysedId}" + sleep 10 + shell: /bin/bash + urlScan: &urlScan + run: + name: Virustotal url scan + command: &virusurlscan | + echo "Url to check: ${URL}" + + analysedId=$(curl -sq -XPOST https://www.virustotal.com/api/v3/urls -H "x-apikey: $VIRUSTOTAL_API_KEY" --form url=${URL} | jq -r '.data.id') + + if [ $analysedId == "null" ]; then + echo 'Status is null, something went wrong'; exit 1; + fi + echo "export ANALYZED_ID=${analysedId}" >> $BASH_ENV + echo "Virustotal Analyzed id: ${analysedId}" + sleep 10 + shell: /bin/bash + validate: &validate + run: + name: Virustotal validate scan results + command: &virusValidate | + analyzeStatus=$(curl -sq -XGET https://www.virustotal.com/api/v3/analyses/${ANALYZED_ID} -H "x-apikey: $VIRUSTOTAL_API_KEY" | jq -r '.data.attributes.status') + if [ $analyzeStatus == "null" ]; then + echo 'Status is null, something went wrong'; exit 1; + fi + + currentOperation="50" + until [ "$currentOperation" == "0" ]; do + if [ "$analyzeStatus" == "completed" ] + then + echo "Current status: ${analyzeStatus}"; break; + else + echo "Current status: ${analyzeStatus}, retries left: ${currentOperation} "; + analyzeStatus=$(curl -sq -XGET https://www.virustotal.com/api/v3/analyses/${ANALYZED_ID} -H "x-apikey: $VIRUSTOTAL_API_KEY" | jq -r '.data.attributes.status'); + sleep 20; + currentOperation=$[$currentOperation - 1]; + fi + done + + analyzeStats=$(curl -sq -XGET https://www.virustotal.com/api/v3/analyses/${ANALYZED_ID} -H "x-apikey: $VIRUSTOTAL_API_KEY" | jq -r '.data.attributes.stats') + analazedHarmless=$(echo ${analyzeStats} | jq '.harmless') + analazedMalicious=$(echo ${analyzeStats} | jq '.malicious') + analazedSuspicious=$(echo ${analyzeStats} | jq '.suspicious') + + if [ "$analyzeStatus" != "completed" ]; then + echo 'Analyse is not completed'; exit 1; + fi + echo "Results:" + echo "analazedHarmless: ${analazedHarmless}, analazedMalicious: ${analazedMalicious}, analazedSuspicious: ${analazedSuspicious}" + + if [ "$analazedMalicious" != "0" ] || [ "$analazedSuspicious" != "0" ]; then + echo "export VIRUS_CHECK_FAILED=true" >> $BASH_ENV + echo 'Found dangers'; exit 0; + fi + + echo "export VIRUS_CHECK_FAILED=false" >> $BASH_ENV + echo "export SKIP_VIRUSTOTAL_REPORT=true" >> $BASH_ENV + echo 'Passed'; + shell: /bin/bash + no_output_timeout: 15m + virustotalReport: &virustotalReport + run: + name: Virustotal slack report + command: &virusreport | + if [ "$SKIP_VIRUSTOTAL_REPORT" == "true" ]; then + exit 0; + fi + + FILE_NAME=virustotal.report.json + BUILD_NAME=$BUILD_NAME FILE_NAME=$FILE_NAME VIRUS_CHECK_FAILED=$VIRUS_CHECK_FAILED node .circleci/virustotal-report.js && + curl -H "Content-type: application/json" --data @$FILE_NAME -H "Authorization: Bearer ${SLACK_TEST_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage + + if [ "$VIRUS_CHECK_FAILED" == "true" ]; then + echo 'Found dangers'; exit 1; + fi + shell: /bin/bash + iTestsNames: &iTestsNames + - oss-st-5 # OSS Standalone v5 + - oss-st-5-pass # OSS Standalone v5 with admin pass required + - oss-st-6 # OSS Standalone v6 and all modules + # TODO: Investigate why it randomly fails + # - oss-st-big # OSS Standalone v6 and all modules and predefined amount of data inside (~3-4M) + - mods-preview # OSS Standalone and all preview modules + - oss-st-6-tls # OSS Standalone v6 with TLS enabled + - oss-st-6-tls-auth # OSS Standalone v6 with TLS auth required +# - oss-st-6-tls-auth-ssh # OSS Standalone v6 with TLS auth required through ssh + - oss-clu # OSS Cluster + - oss-clu-tls # OSS Cluster with TLS enabled + - oss-sent # OSS Sentinel + - oss-sent-tls-auth # OSS Sentinel with TLS auth + - 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 + - oss-sent-tls-auth # OSS Sentinel with TLS auth + guides-filter: &guidesFilter + filters: + branches: + only: + - guides + dev-filter: &devFilter + filters: + branches: + only: + - main + - /^build\/.*/ + stage-filter: &stageFilter + filters: + branches: + only: + - /^release.*/ + prod-filter: &prodFilter + filters: + branches: + only: + - latest + ui-deps-cache-key: &uiDepsCacheKey + key: v1-ui-deps-{{ checksum "yarn.lock" }} + api-deps-cache-key: &apiDepsCacheKey + key: v1-ui-deps-{{ checksum "redisinsight/api/yarn.lock" }} + manual-build-conditions: &manual-build-conditions + or: + - << pipeline.parameters.linux >> + - << pipeline.parameters.mac >> + - << pipeline.parameters.windows >> + - << pipeline.parameters.docker >> + ignore-for-manual-build: &ignore-for-manual-build + when: + not: *manual-build-conditions + +orbs: + win: circleci/windows@2.4.1 + node: circleci/node@5.2.0 + aws: circleci/aws-cli@2.0.3 + +executors: + linux-executor: + machine: + image: ubuntu-2004:2023.04.2 + linux-executor-dlc: + machine: + image: ubuntu-2004:2023.04.2 + docker_layer_caching: true + docker-node: + docker: + - image: cimg/node:20.15 + docker: + docker: + - image: cibuilds/docker:19.03.5 + macos: + macos: + xcode: 14.2.0 + +parameters: + linux: + type: string + default: &ignore "" + mac: + type: string + default: *ignore + windows: + type: string + default: *ignore + docker: + type: string + default: *ignore + redis_client: + type: string + default: "" + env: + type: string + default: "stage" + +jobs: + # Test jobs + unit-tests-ui: + executor: docker-node + steps: + - checkout + - restore_cache: + <<: *uiDepsCacheKey + - run: + name: UI PROD dependencies audit + command: | + FILENAME=ui.prod.deps.audit.json + yarn audit --groups dependencies --json > $FILENAME || true && + FILENAME=$FILENAME DEPS="UI prod" node .circleci/deps-audit-report.js && + curl -H "Content-type: application/json" --data @slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage + - run: + name: UI DEV dependencies audit + command: | + FILENAME=ui.dev.deps.audit.json + yarn audit --groups devDependencies --json > $FILENAME || true && + FILENAME=$FILENAME DEPS="UI dev" node .circleci/deps-audit-report.js && + curl -H "Content-type: application/json" --data @slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage + - run: + name: Code analysis + command: | + SKIP_POSTINSTALL=1 yarn install + + FILENAME=ui.lint.audit.json + WORKDIR="." + yarn lint:ui -f json -o $FILENAME || true && + FILENAME=$FILENAME WORKDIR=$WORKDIR TARGET="UI" node .circleci/lint-report.js && + curl -H "Content-type: application/json" --data @$WORKDIR/slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage + + FILENAME=rest.lint.audit.json + yarn lint -f json -o $FILENAME || true && + FILENAME=$FILENAME WORKDIR=$WORKDIR TARGET="REST" node .circleci/lint-report.js && + curl -H "Content-type: application/json" --data @$WORKDIR/slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage + - run: + name: Unit tests UI + command: | + yarn test:cov --ci --silent + - save_cache: + <<: *uiDepsCacheKey + paths: + - ./node_modules + unit-tests-api: + executor: docker-node + steps: + - checkout + - restore_cache: + <<: *apiDepsCacheKey + - run: + name: API PROD dependencies scan + command: | + FILENAME=api.prod.deps.audit.json + yarn --cwd redisinsight/api audit --groups dependencies --json > $FILENAME || true && + FILENAME=$FILENAME DEPS="API prod" node .circleci/deps-audit-report.js && + curl -H "Content-type: application/json" --data @slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage + - run: + name: API DEV dependencies scan + command: | + FILENAME=api.dev.deps.audit.json + yarn --cwd redisinsight/api audit --groups devDependencies --json > $FILENAME || true && + FILENAME=$FILENAME DEPS="API dev" node .circleci/deps-audit-report.js && + curl -H "Content-type: application/json" --data @slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage + - run: + name: Code analysis + command: | + yarn --cwd redisinsight/api + + FILENAME=api.lint.audit.json + WORKDIR="./redisinsight/api" + yarn lint:api -f json -o $FILENAME || true && + FILENAME=$FILENAME WORKDIR=$WORKDIR TARGET="API" node .circleci/lint-report.js && + curl -H "Content-type: application/json" --data @$WORKDIR/slack.$FILENAME -H "Authorization: Bearer ${SLACK_AUDIT_REPORT_KEY}" -X POST https://slack.com/api/chat.postMessage + - run: + name: Unit tests API + command: | + yarn --cwd redisinsight/api/ test:cov --ci + - save_cache: + <<: *apiDepsCacheKey + paths: + - ./redisinsight/api/node_modules + integration-tests-run: + executor: linux-executor-dlc + parameters: + rte: + description: Redis Test Environment name + type: string + build: + description: Backend build to run tests over + type: enum + default: local + enum: ['local', 'docker', 'saas'] + report: + description: Send report for test run to slack + type: boolean + default: false + redis_client: + description: Library to use for redis connection + type: string + default: "" + steps: + - checkout + - restore_cache: + <<: *apiDepsCacheKey + - when: + condition: + equal: [ 'docker', << parameters.build >> ] + steps: + - attach_workspace: + at: /tmp + - run: + name: Load built docker image from workspace + command: | + docker image load -i /tmp/release/docker/docker-linux-alpine.amd64.tar + - run: + name: Run tests + command: | + if [ << parameters.redis_client >> != "" ]; then + export RI_REDIS_CLIENTS_FORCE_STRATEGY=<< parameters.redis_client >> + fi + + ./redisinsight/api/test/test-runs/start-test-run.sh -r << parameters.rte >> -t << parameters.build >> + mkdir -p mkdir itest/coverages && mkdir -p itest/results + cp ./redisinsight/api/test/test-runs/coverage/test-run-result.json ./itest/results/<< parameters.rte >>.result.json + cp ./redisinsight/api/test/test-runs/coverage/test-run-result.xml ./itest/results/<< parameters.rte >>.result.xml + cp ./redisinsight/api/test/test-runs/coverage/test-run-coverage.json ./itest/coverages/<< parameters.rte >>.coverage.json + - when: + condition: + equal: [ true, << parameters.report >> ] + steps: + - run: + name: Send report + when: always + command: | + ITEST_NAME=<< parameters.rte >> node ./.circleci/itest-results.js + curl -H "Content-type: application/json" --data @itests.report.json -H "Authorization: Bearer $SLACK_TEST_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage + - store_test_results: + path: ./itest/results + - persist_to_workspace: + root: . + paths: + - ./itest/results/<< parameters.rte >>.result.json + - ./itest/coverages/<< parameters.rte >>.coverage.json + integration-tests-coverage: + executor: linux-executor + steps: + - checkout + - attach_workspace: + at: /tmp + - run: + name: Calculate coverage across all tests runs + command: | + sudo mkdir -p /usr/src/app + sudo cp -a ./redisinsight/api/. /usr/src/app/ + 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-dlc + parameters: + report: + description: Send report for test run to slack + type: boolean + default: false + parallelism: + description: Number of threads to run tests + type: integer + default: 1 + parallelism: << parameters.parallelism >> + steps: + - checkout + - node/install: + install-yarn: true + node-version: '20.15' + - attach_workspace: + at: . + - run: sudo apt-get install net-tools + - run: sudo apt-get install xdotool + - run: sudo apt-get install -y desktop-file-utils + - run: + name: Install Google Chrome + command: | + wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' + sudo apt-get update + sudo apt-get install -y google-chrome-stable + sudo apt-get install -y \ + libnss3 \ + libgconf-2-4 \ + libxss1 \ + libasound2 + xdg-settings set default-web-browser google-chrome.desktop + - run: + name: Install WM + command: sudo apt install fluxbox + - run: + name: Install Xvfb + command: sudo apt-get install -y xvfb + - run: + name: Start Xvfb + command: | + if [ -f /tmp/.X99-lock ]; then rm /tmp/.X99-lock; fi + Xvfb :99 -ac -screen 0 1920x1080x24 & + sleep 3 + fluxbox & + export DISPLAY=:99 + echo $DISPLAY + # - run: + # name: Clone mocked RDI server + # command: | + # git clone https://$GH_RDI_MOCKED_SERVER_KEY@github.com/RedisInsight/RDI_server_mocked.git tests/e2e/rte/rdi + - run: + name: .AppImage tests + command: | + cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/electron/**/*.e2e.ts" | circleci tests split --split-by=timings) && cd ../.. + .circleci/e2e/test.app-image.sh + - when: + condition: + equal: [ true, << parameters.report >> ] + steps: + - run: + name: Send report + when: always + command: | + APP_BUILD_TYPE="Electron (Linux)" node ./.circleci/e2e-results.js + 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 + parameters: + report: + description: Send report for test run to slack + type: boolean + default: false + parallelism: + description: Number of threads to run tests + type: integer + default: 1 + parallelism: << parameters.parallelism >> + steps: + - checkout + - attach_workspace: + at: . + - run: + command: | + nvm install 20.15 + nvm use 20.15 + npm install --global yarn + - run: + command: | + cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/electron/**/*.e2e.ts" | circleci tests split --split-by=timings) && cd ../.. + .circleci/e2e/test.exe.cmd + shell: bash.exe + - when: + condition: + equal: [ true, << parameters.report >> ] + steps: + - run: + name: Send report + when: always + command: | + APP_BUILD_TYPE="Electron (Windows)" node ./.circleci/e2e-results.js + 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 + 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-dlc + parameters: + build: + description: Backend build to run tests over + type: enum + default: local + enum: ['local', 'docker'] + report: + description: Send report for test run to slack + type: boolean + default: false + parallelism: + description: Number of threads to run tests + type: integer + default: 1 + parallelism: << parameters.parallelism >> + steps: + - checkout + - when: + condition: + equal: [ 'docker', << parameters.build >> ] + steps: + - attach_workspace: + at: /tmp + - run: + name: Load built docker image from workspace + command: | + docker image load -i /tmp/release/docker/docker-linux-alpine.amd64.tar + # - run: + # name: Clone mocked RDI server + # command: | + # git clone https://$GH_RDI_MOCKED_SERVER_KEY@github.com/RedisInsight/RDI_server_mocked.git tests/e2e/rte/rdi + - run: + name: Run tests + command: | + cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/web/**/*.e2e.ts" | circleci tests split --split-by=timings) && cd ../.. + TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \ + RI_SERVER_TLS_CERT="$RI_SERVER_TLS_CERT" \ + RI_SERVER_TLS_KEY="$RI_SERVER_TLS_KEY" \ + docker-compose \ + -f tests/e2e/rte.docker-compose.yml \ + -f tests/e2e/docker.web.docker-compose.yml \ + up --abort-on-container-exit --force-recreate --build + no_output_timeout: 5m + - when: + condition: + equal: [ 'local', << parameters.build >> ] + steps: + - run: + name: Run tests + command: | + cd tests/e2e && export TEST_FILES=$(circleci tests glob "tests/web/**/*.e2e.ts" | circleci tests split --split-by=timings) && cd ../.. + TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \ + RI_SERVER_TLS_CERT="$RI_SERVER_TLS_CERT" \ + RI_SERVER_TLS_KEY="$RI_SERVER_TLS_KEY" \ + docker-compose \ + -f tests/e2e/rte.docker-compose.yml \ + -f tests/e2e/local.web.docker-compose.yml \ + up --abort-on-container-exit --force-recreate + no_output_timeout: 5m + - when: + condition: + equal: [ true, << parameters.report >> ] + steps: + - run: + name: Send report + when: always + command: | + node ./.circleci/e2e-results.js + 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 + manual-build-validate: + executor: docker-node + parameters: + os: + type: string + default: "" + target: + type: string + default: "" + steps: + - checkout + - run: + command: | + node .circleci/build/manual-build-validate.js << parameters.os >> << parameters.target >> + setup-sign-certificates: + executor: linux-executor + steps: + - run: + name: Setup sign certificates + command: | + mkdir -p certs + echo "$CSC_P12_BASE64" | base64 -id > certs/mac-developer.p12 + echo "$CSC_MAC_INSTALLER_P12_BASE64" | base64 -id > certs/mac-installer.p12 + echo "$CSC_MAS_P12_BASE64" | base64 -id > certs/mas-distribution.p12 + echo "$WIN_CSC_PFX_BASE64" | base64 -id > certs/redislabs_win.pfx + - persist_to_workspace: + root: . + paths: + - certs + setup-build: + executor: docker + parameters: + env: + description: Build environment (stage || prod) + type: enum + default: stage + enum: [ 'dev', 'stage', 'prod' ] + steps: + - checkout + - run: + command: | + mkdir electron + + CURRENT_VERSION=$(jq -r ".version" redisinsight/package.json) + echo "Build version: $CURRENT_VERSION" + cp ./redisinsight/package.json ./electron/package.json + echo "$VERSION" > electron/version + exit 0 + + - persist_to_workspace: + root: /root/project + paths: + - electron + linux: + executor: linux-executor + resource_class: large + parameters: + env: + description: Build environment (stage || prod) + type: enum + default: stage + enum: ['stage', 'prod', 'dev'] + target: + description: Build target + type: string + default: "" + steps: + - checkout + - node/install: + install-yarn: true + node-version: '20.15' + - attach_workspace: + at: . + - run: + command: | + cp ./electron/package.json ./redisinsight/ + - run: + name: install dependencies + command: | + sudo apt-get update -y && sudo apt-get install -y rpm flatpak flatpak-builder ca-certificates + flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo + flatpak install flathub --no-deps --arch x86_64 --assumeyes \ + runtime/org.freedesktop.Sdk/x86_64/20.08 \ + runtime/org.freedesktop.Platform/x86_64/20.08 \ + org.electronjs.Electron2.BaseApp/x86_64/20.08 + + yarn --cwd redisinsight/api/ install --ignore-optional + yarn --cwd redisinsight/ install --ignore-optional + yarn install + no_output_timeout: 15m + - run: + name: Install plugins dependencies and build plugins + command: | + yarn build:statics + no_output_timeout: 15m + - run: + name: Build linux AppImage and deb + command: | + if [ << parameters.env >> == 'prod' ]; then + yarn package:prod + exit 0; + fi + + export RI_CLOUD_IDP_AUTHORIZE_URL=$RI_CLOUD_IDP_AUTHORIZE_URL_STAGE + export RI_CLOUD_IDP_TOKEN_URL=$RI_CLOUD_IDP_TOKEN_URL_STAGE + export RI_CLOUD_IDP_ISSUER=$RI_CLOUD_IDP_ISSUER_STAGE + export RI_CLOUD_IDP_CLIENT_ID=$RI_CLOUD_IDP_CLIENT_ID_STAGE + export RI_CLOUD_IDP_REDIRECT_URI=$RI_CLOUD_IDP_REDIRECT_URI_STAGE + export RI_CLOUD_IDP_GOOGLE_ID=$RI_CLOUD_IDP_GOOGLE_ID_STAGE + export RI_CLOUD_IDP_GH_ID=$RI_CLOUD_IDP_GH_ID_STAGE + export RI_CLOUD_API_URL=$RI_CLOUD_API_URL_STAGE + export RI_CLOUD_CAPI_URL=$RI_CLOUD_CAPI_URL_STAGE + export RI_FEATURES_CONFIG_URL=$RI_FEATURES_CONFIG_URL_STAGE + + if [ << parameters.env >> == 'stage' ]; then + RI_UPGRADES_LINK=$RI_UPGRADES_LINK_STAGE RI_SEGMENT_WRITE_KEY=$RI_SEGMENT_WRITE_KEY_STAGE yarn package:stage --linux << parameters.target >> + exit 0; + fi + + RI_UPGRADES_LINK='' RI_SEGMENT_WRITE_KEY='' yarn package:stage --linux << parameters.target >> + - persist_to_workspace: + root: . + paths: + - release/Redis-Insight*.deb + - release/Redis-Insight*.rpm + - release/Redis-Insight*.AppImage + - release/Redis-Insight*.flatpak + - release/Redis-Insight*.snap + - release/*-linux.yml + macosx: + executor: macos + resource_class: macos.m1.medium.gen1 + parameters: + env: + description: Build environment (stage || prod) + type: enum + default: stage + enum: ['stage', 'prod', 'dev'] + redisstack: + description: Build RedisStack archives + type: boolean + default: true + target: + description: Build target + type: string + default: "" + steps: + - checkout + - node/install: + node-version: '20.15' + - attach_workspace: + at: . + - run: + command: | + cp ./electron/package.json ./redisinsight/ + - <<: *keychain + - run: + name: install dependencies + command: | + yarn install + yarn --cwd redisinsight/api/ install + yarn --cwd redisinsight/ install + yarn build:statics + no_output_timeout: 15m + - run: + name: Build macos dmg + command: | + unset CSC_LINK + export CSC_IDENTITY_AUTO_DISCOVERY=true + export CSC_KEYCHAIN=redisinsight.keychain + + if [ << parameters.env >> == 'prod' ]; then + yarn package:prod + yarn package:mas + rm -rf release/mac + mv release/mas-universal/Redis-Insight-mac-universal-mas.pkg release/Redis-Insight-mac-universal-mas.pkg + exit 0; + fi + + export RI_CLOUD_IDP_AUTHORIZE_URL=$RI_CLOUD_IDP_AUTHORIZE_URL_STAGE + export RI_CLOUD_IDP_TOKEN_URL=$RI_CLOUD_IDP_TOKEN_URL_STAGE + export RI_CLOUD_IDP_ISSUER=$RI_CLOUD_IDP_ISSUER_STAGE + export RI_CLOUD_IDP_CLIENT_ID=$RI_CLOUD_IDP_CLIENT_ID_STAGE + export RI_CLOUD_IDP_REDIRECT_URI=$RI_CLOUD_IDP_REDIRECT_URI_STAGE + export RI_CLOUD_IDP_GOOGLE_ID=$RI_CLOUD_IDP_GOOGLE_ID_STAGE + export RI_CLOUD_IDP_GH_ID=$RI_CLOUD_IDP_GH_ID_STAGE + export RI_CLOUD_API_URL=$RI_CLOUD_API_URL_STAGE + export RI_CLOUD_CAPI_URL=$RI_CLOUD_CAPI_URL_STAGE + export RI_UPGRADES_LINK='' + export RI_SEGMENT_WRITE_KEY='' + export RI_FEATURES_CONFIG_URL=$RI_FEATURES_CONFIG_URL_STAGE + + if [ << parameters.env >> == 'stage' ]; then + export RI_UPGRADES_LINK=$RI_UPGRADES_LINK_STAGE + export RI_SEGMENT_WRITE_KEY=$RI_SEGMENT_WRITE_KEY_STAGE + fi + + # handle manual builds + if [ << parameters.target >> ]; then + yarn package:stage --mac << parameters.target >> + rm -rf release/mac + exit 0; + fi + + yarn package:stage && yarn package:mas + rm -rf release/mac + mv release/mas-universal/Redis-Insight-mac-universal-mas.pkg release/Redis-Insight-mac-universal-mas.pkg + no_output_timeout: 60m + - when: + condition: + equal: [ true, << parameters.redisstack >> ] + steps: + - run: + name: Repack dmg to tar + command: | + ARCH=x64 ./.circleci/redisstack/dmg.repack.sh + ARCH=arm64 ./.circleci/redisstack/dmg.repack.sh + - persist_to_workspace: + root: . + paths: + - release/Redis-Insight*.zip + - release/Redis-Insight*.dmg + - release/Redis-Insight*.dmg.blockmap + - release/Redis-Insight*.pkg + - release/*-mac.yml + - release/redisstack + windows: + executor: + name: win/default + parameters: + env: + description: Build environment (stage || prod) + type: enum + default: stage + enum: ['stage', 'prod', 'dev'] + target: + description: Build target + type: string + default: "" + steps: + - checkout + - attach_workspace: + at: . + - run: + command: | + cp ./electron/package.json ./redisinsight/ + - run: + name: Build windows exe + command: | + nvm install 20.15 + nvm use 20.15 + npm install --global yarn + + # set ALL_REDIS_COMMANDS=$(curl $ALL_REDIS_COMMANDS_RAW_URL) + # install dependencies + yarn install + yarn --cwd redisinsight/api/ install + yarn --cwd redisinsight/ install + yarn build:statics:win + + if [ << parameters.env >> == 'prod' ]; then + yarn package:prod + rm -rf release/win-unpacked + exit 0; + fi + + export RI_CLOUD_IDP_AUTHORIZE_URL=$RI_CLOUD_IDP_AUTHORIZE_URL_STAGE + export RI_CLOUD_IDP_TOKEN_URL=$RI_CLOUD_IDP_TOKEN_URL_STAGE + export RI_CLOUD_IDP_ISSUER=$RI_CLOUD_IDP_ISSUER_STAGE + export RI_CLOUD_IDP_CLIENT_ID=$RI_CLOUD_IDP_CLIENT_ID_STAGE + export RI_CLOUD_IDP_REDIRECT_URI=$RI_CLOUD_IDP_REDIRECT_URI_STAGE + export RI_CLOUD_IDP_GOOGLE_ID=$RI_CLOUD_IDP_GOOGLE_ID_STAGE + export RI_CLOUD_IDP_GH_ID=$RI_CLOUD_IDP_GH_ID_STAGE + export RI_CLOUD_API_URL=$RI_CLOUD_API_URL_STAGE + export RI_CLOUD_CAPI_URL=$RI_CLOUD_CAPI_URL_STAGE + export RI_FEATURES_CONFIG_URL=$RI_FEATURES_CONFIG_URL_STAGE + + if [ << parameters.env >> == 'stage' ]; then + RI_UPGRADES_LINK=$RI_UPGRADES_LINK_STAGE RI_SEGMENT_WRITE_KEY=$RI_SEGMENT_WRITE_KEY_STAGE yarn package:stage --win << parameters.target >> + else + RI_UPGRADES_LINK='' RI_SEGMENT_WRITE_KEY='' yarn package:stage --win << parameters.target >> + fi + + rm -rf release/win-unpacked + shell: bash.exe + no_output_timeout: 20m + - persist_to_workspace: + root: . + paths: + - release/Redis-Insight*.exe + - release/Redis-Insight*.exe.blockmap + - release/*.yml + virustotal-file: + executor: linux-executor + parameters: + ext: + description: File extension + type: string + steps: + - checkout + - attach_workspace: + at: /tmp/release + - run: + name: export FILE_NAME environment variable + command: | + echo 'export FILE_NAME="Redis-Insight*<< parameters.ext >>"' >> $BASH_ENV + - <<: *fileScan + - <<: *validate + virustotal-url: + executor: linux-executor + parameters: + fileName: + description: File name + type: string + steps: + - checkout + - run: + name: export URL environment variable + command: | + echo 'export URL="https://download.redisinsight.redis.com/latest/<< parameters.fileName >>"' >> $BASH_ENV + echo 'export BUILD_NAME="<< parameters.fileName >>"' >> $BASH_ENV + - <<: *urlScan + - <<: *validate + - <<: *virustotalReport + virustotal-report: + executor: linux-executor + steps: + - checkout + - run: + name: Send virustotal passed report + command: | + echo 'export VIRUS_CHECK_FAILED=0' >> $BASH_ENV + echo 'export SKIP_VIRUSTOTAL_REPORT=false' >> $BASH_ENV + - <<: *virustotalReport + docker: + executor: linux-executor + parameters: + env: + type: enum + default: staging + enum: [ 'staging', 'production' ] + steps: + - checkout + - node/install: + install-yarn: true + node-version: '20.15' + - run: + name: Install dependencies + command: | + sudo apt-get update -y && sudo apt-get install -y ca-certificates qemu-user-static + - run: + name: Build sources + command: ./.circleci/build/build.sh + - run: + name: Build web archives + command: | + unset npm_config_keytar_binary_host_mirror + unset npm_config_node_sqlite3_binary_host_mirror + + # Docker sources + PLATFORM=linux ARCH=x64 LIBC=musl .circleci/build/build_modules.sh + PLATFORM=linux ARCH=arm64 LIBC=musl .circleci/build/build_modules.sh + + # Redis Stack + VSC Linux + PLATFORM=linux ARCH=x64 .circleci/build/build_modules.sh + PLATFORM=linux ARCH=arm64 .circleci/build/build_modules.sh + + # VSC Darwin + PLATFORM=darwin ARCH=x64 .circleci/build/build_modules.sh + PLATFORM=darwin ARCH=arm64 .circleci/build/build_modules.sh + # VSC Windows + PLATFORM=win32 ARCH=x64 .circleci/build/build_modules.sh + - run: + name: Build Docker (x64, arm64) + command: | + TELEMETRY=$RI_SEGMENT_WRITE_KEY_DEV + + if [ << parameters.env >> == 'production' ]; then + TELEMETRY=$RI_SEGMENT_WRITE_KEY + fi + + if [ << parameters.env >> == 'staging' ]; then + TELEMETRY=$RI_SEGMENT_WRITE_KEY_STAGE + fi + + # Build alpine x64 image + docker buildx build \ + -f .circleci/build/build.Dockerfile \ + --platform linux/amd64 \ + --build-arg DIST=release/web/Redis-Insight-web-linux-musl.x64.tar.gz \ + --build-arg NODE_ENV=<< parameters.env >> \ + --build-arg RI_SEGMENT_WRITE_KEY="$TELEMETRY" \ + -t redisinsight:amd64 \ + . + + # Build alpine arm64 image + docker buildx build \ + -f .circleci/build/build.Dockerfile \ + --platform linux/arm64 \ + --build-arg DIST=release/web/Redis-Insight-web-linux-musl.arm64.tar.gz \ + --build-arg NODE_ENV=<< parameters.env >> \ + --build-arg RI_SEGMENT_WRITE_KEY="$TELEMETRY" \ + -t redisinsight:arm64 \ + . + + mkdir -p release/docker + docker image save -o release/docker/docker-linux-alpine.amd64.tar redisinsight:amd64 + docker image save -o release/docker/docker-linux-alpine.arm64.tar redisinsight:arm64 + - persist_to_workspace: + root: . + paths: + - ./release + + licenses-check: + executor: linux-executor + steps: + - checkout + - restore_cache: + <<: *uiDepsCacheKey + <<: *apiDepsCacheKey + - run: + name: Run install all dependencies + command: | + yarn install + yarn --cwd redisinsight/api install + yarn --cwd tests/e2e install + # Install plugins dependencies + export pluginsOnlyInstall=1 + yarn build:statics + - run: + name: Generate licenses csv files and send csv data to google sheet + command: | + npm i -g license-checker + + echo "$GOOGLE_ACCOUNT_SERVICE_KEY_BASE64" | base64 -id > gasKey.json + SPREADSHEET_ID=$GOOGLE_SPREADSHEET_DEPENDENCIES_ID node .circleci/deps-licenses-report.js + - store_artifacts: + path: licenses + destination: licenses + + # Release jobs + store-build-artifacts: + executor: linux-executor + steps: + - attach_workspace: + at: . + - store_artifacts: + path: release + destination: release + release-aws-private-dev: + executor: linux-executor + steps: + - checkout + - attach_workspace: + at: . + - run: + name: publish + command: | + rm release/._* ||: + chmod +x .circleci/build/sum_sha256.sh + .circleci/build/sum_sha256.sh + applicationVersion=$(jq -r '.version' redisinsight/package.json) + + aws s3 cp release/ s3://${AWS_BUCKET_NAME_TEST}/private/builds/${CIRCLE_BUILD_NUM} --recursive + release-aws-private: + executor: linux-executor + steps: + - checkout + - attach_workspace: + at: . + - store_artifacts: + path: release + destination: release + - run: + name: prepare release + command: | + rm release/._* ||: + - run: + name: publish + command: | + chmod +x .circleci/build/sum_sha256.sh + .circleci/build/sum_sha256.sh + applicationVersion=$(jq -r '.version' redisinsight/package.json) + + aws s3 cp release/ s3://${AWS_BUCKET_NAME}/private/${applicationVersion} --recursive + + release-docker: + executor: linux-executor + steps: + - checkout + - attach_workspace: + at: . + - run: + name: Release docker images + command: | + appVersion=$(jq -r '.version' redisinsight/package.json) + + docker login -u $DOCKER_USER -p $DOCKER_PASS + + ./.circleci/build/release-docker.sh \ + -d redisinsight \ + -r $DOCKER_REPO \ + -v $appVersion + + docker login -u $DOCKER_V1_USER -p $DOCKER_V1_PASS + + ./.circleci/build/release-docker.sh \ + -d redisinsight \ + -r $DOCKER_V1_REPO \ + -v $appVersion + + publish-prod-aws: + executor: linux-executor + steps: + - checkout + - run: + name: Init variables + command: | + latestYmlFileName="latest.yml" + downloadLatestFolderPath="public/latest" + upgradeLatestFolderPath="public/upgrades" + releasesFolderPath="public/releases" + appName=$(jq -r '.productName' electron-builder.json) + appVersion=$(jq -r '.version' redisinsight/package.json) + + echo "export downloadLatestFolderPath=${downloadLatestFolderPath}" >> $BASH_ENV + echo "export upgradeLatestFolderPath=${upgradeLatestFolderPath}" >> $BASH_ENV + echo "export releasesFolderPath=${releasesFolderPath}" >> $BASH_ENV + echo "export applicationName=${appName}" >> $BASH_ENV + echo "export applicationVersion=${appVersion}" >> $BASH_ENV + echo "export appFileName=Redis-Insight" >> $BASH_ENV + + # download latest.yml file to get last public version + aws s3 cp s3://${AWS_BUCKET_NAME}/${downloadLatestFolderPath}/${latestYmlFileName} . + + versionLine=$(head -1 ${latestYmlFileName}) + versionLineArr=(${versionLine/:// }) + previousAppVersion=${versionLineArr[1]} + + echo "export previousApplicationVersion=${previousAppVersion}" >> $BASH_ENV + + - run: + name: Publish AWS S3 + command: | + # remove previous build from the latest directory /public/latest + aws s3 rm s3://${AWS_BUCKET_NAME}/${downloadLatestFolderPath} --recursive + + # remove previous build from the upgrade directory /public/upgrades + aws s3 rm s3://${AWS_BUCKET_NAME}/${upgradeLatestFolderPath} --recursive + + # copy current version apps for download to /public/latest + aws s3 cp s3://${AWS_BUCKET_NAME}/private/${applicationVersion}/ \ + s3://${AWS_BUCKET_NAME}/${downloadLatestFolderPath} --recursive --exclude "*.zip" + + # copy current version apps for upgrades to /public/upgrades + aws s3 cp s3://${AWS_BUCKET_NAME}/private/${applicationVersion}/ \ + s3://${AWS_BUCKET_NAME}/${upgradeLatestFolderPath} --recursive + + # !MOVE current version apps to releases folder /public/releases + aws s3 mv s3://${AWS_BUCKET_NAME}/private/${applicationVersion}/ \ + s3://${AWS_BUCKET_NAME}/${releasesFolderPath}/${applicationVersion} --recursive + + # invalidate cloudfront cash + aws cloudfront create-invalidation --distribution-id ${AWS_DISTRIBUTION_ID} --paths "/*" + + - run: + name: Add tags for all objects and create S3 metrics + command: | + + # declare all tags + declare -A tag0=( + [arch]='x64' + [platform]='macos' + [objectDownload]=${appFileName}'-mac-x64.dmg' + [objectUpgrade]=${appFileName}'-mac-x64.zip' + ) + + declare -A tag1=( + [arch]='arm64' + [platform]='macos' + [objectDownload]=${appFileName}'-mac-arm64.dmg' + [objectUpgrade]=${appFileName}'-mac-arm64.zip' + ) + + declare -A tag2=( + [arch]='x64' + [platform]='windows' + [objectDownload]=${appFileName}'-win-installer.exe' + ) + + declare -A tag3=( + [arch]='x64' + [platform]='linux_AppImage' + [objectDownload]=${appFileName}'-linux-x86_64.AppImage' + ) + + declare -A tag4=( + [arch]='x64' + [platform]='linux_deb' + [objectDownload]=${appFileName}'-linux-amd64.deb' + ) + + declare -A tag5=( + [arch]='x64' + [platform]='linux_rpm' + [objectDownload]=${appFileName}'-linux-x86_64.rpm' + ) + + # loop for add all tags to each app and create metrics + declare -n tag + for tag in ${!tag@}; do + + designation0="downloads" + designation1="upgrades" + + id0="${tag[platform]}_${tag[arch]}_${designation0}_${applicationVersion}" + id1="${tag[platform]}_${tag[arch]}_${designation1}_${applicationVersion}" + + # add tags to each app for download + aws s3api put-object-tagging \ + --bucket ${AWS_BUCKET_NAME} \ + --key ${downloadLatestFolderPath}/${tag[objectDownload]} \ + --tagging '{"TagSet": [{ "Key": "version", "Value": "'"${applicationVersion}"'" }, {"Key": "platform", "Value": "'"${tag[platform]}"'"}, {"Key": "arch", "Value": "'"${tag[arch]}"'"}, { "Key": "designation", "Value": "'"${designation0}"'" }]}' + + # add tags to each app for upgrades + aws s3api put-object-tagging \ + --bucket ${AWS_BUCKET_NAME} \ + --key ${upgradeLatestFolderPath}/${tag[objectUpgrade]:=${tag[objectDownload]}} \ + --tagging '{"TagSet": [{ "Key": "version", "Value": "'"${applicationVersion}"'" }, {"Key": "platform", "Value": "'"${tag[platform]}"'"}, {"Key": "arch", "Value": "'"${tag[arch]}"'"}, { "Key": "designation", "Value": "'"${designation1}"'" }]}' + + # Create metrics for all tags for downloads to S3 + aws s3api put-bucket-metrics-configuration \ + --bucket ${AWS_BUCKET_NAME} \ + --id ${id0} \ + --metrics-configuration '{"Id": "'"${id0}"'", "Filter": {"And": {"Tags": [{"Key": "platform", "Value": "'"${tag[platform]}"'"}, {"Key": "arch", "Value": "'"${tag[arch]}"'"}, {"Key": "designation", "Value": "'"${designation0}"'"}, {"Key": "version", "Value": "'"${applicationVersion}"'"} ]}}}' + + # Create metrics for all tags for upgrades to S3 + aws s3api put-bucket-metrics-configuration \ + --bucket ${AWS_BUCKET_NAME} \ + --id ${id1} \ + --metrics-configuration '{"Id": "'"${id1}"'", "Filter": {"And": {"Tags": [{"Key": "platform", "Value": "'"${tag[platform]}"'"}, {"Key": "arch", "Value": "'"${tag[arch]}"'"}, {"Key": "designation", "Value": "'"${designation1}"'"}, {"Key": "version", "Value": "'"${applicationVersion}"'"}]}}}' + + done + + +workflows: + # FE Unit tests for "fe/feature" or "fe/bugfix" branches only + frontend-tests: + <<: *ignore-for-manual-build + jobs: + - 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: + <<: *ignore-for-manual-build + jobs: + - unit-tests-api: + name: UTest - API + filters: + branches: + only: + - /^be/feature.*/ + - /^be/bugfix.*/ + - integration-tests-run: + redis_client: << pipeline.parameters.redis_client >> + 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.*/ + - setup-sign-certificates: + name: Setup sign certificates (stage) + requires: + - Start E2E Tests + - setup-build: + name: Setup build (stage) + requires: + - Setup sign certificates (stage) + - linux: + name: Build app - Linux (stage) + env: stage + target: AppImage + requires: + - Setup build (stage) + - docker: + name: Build docker image + requires: + - Start E2E Tests + - e2e-tests: + name: E2ETest + build: docker + parallelism: 4 + requires: + - Build docker image + - e2e-app-image: + name: E2ETest (AppImage) + parallelism: 1 + requires: + - Build app - Linux (stage) + # Workflow for feature, bugfix, main branches + feature-main-branch: + <<: *ignore-for-manual-build + 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 + redis_client: + - << pipeline.parameters.redis_client >> + name: ITest - << matrix.rte >> (code) + requires: + - Start All Tests + - integration-tests-coverage: + name: ITest - Final coverage + requires: + - itest-code + # E2E tests + - setup-sign-certificates: + name: Setup sign certificates (stage) + requires: + - Start All Tests + - setup-build: + name: Setup build (stage) + requires: + - Setup sign certificates (stage) + - linux: + name: Build app - Linux (stage) + requires: + - Setup build (stage) + - docker: + name: Build docker image + requires: + - Start All Tests + - e2e-tests: + name: E2ETest + build: docker + parallelism: 4 + requires: + - Build docker image + - e2e-app-image: + name: E2ETest (AppImage) + parallelism: 2 + requires: + - Build app - Linux (stage) + # Approve to build + - approve: + name: Build App + type: approval + requires: + - UTest - UI + - UTest - API + - ITest - Final coverage + filters: + branches: + only: + - /^e2e/feature.*/ + - /^e2e/bugfix.*/ + # Manual builds using web UI + manual-build-linux: + when: << pipeline.parameters.linux >> + jobs: + - manual-build-validate: + name: Validating build parameters + os: linux + target: << pipeline.parameters.linux >> + - setup-sign-certificates: + name: Setup sign certificates (<< pipeline.parameters.env >>) + requires: + - Validating build parameters + - setup-build: + name: Setup build (<< pipeline.parameters.env >>) + env: << pipeline.parameters.env >> + requires: + - Setup sign certificates (<< pipeline.parameters.env >>) + - linux: + name: Build app - Linux (<< pipeline.parameters.env >>) + env: << pipeline.parameters.env >> + target: << pipeline.parameters.linux >> + requires: + - Setup build (<< pipeline.parameters.env >>) + - store-build-artifacts: + name: Store build artifacts (<< pipeline.parameters.env >>) + requires: + - Build app - Linux (<< pipeline.parameters.env >>) + manual-build-mac: + when: << pipeline.parameters.mac >> + jobs: + - manual-build-validate: + name: Validating build parameters + os: mac + target: << pipeline.parameters.mac >> + - setup-sign-certificates: + name: Setup sign certificates (<< pipeline.parameters.env >>) + requires: + - Validating build parameters + - setup-build: + name: Setup build (<< pipeline.parameters.env >>) + env: << pipeline.parameters.env >> + requires: + - Setup sign certificates (<< pipeline.parameters.env >>) + - macosx: + name: Build app - MacOS (<< pipeline.parameters.env >>) + env: << pipeline.parameters.env >> + redisstack: false + target: << pipeline.parameters.mac >> + requires: + - Setup build (<< pipeline.parameters.env >>) + - store-build-artifacts: + name: Store build artifacts (<< pipeline.parameters.env >>) + requires: + - Build app - MacOS (<< pipeline.parameters.env >>) + manual-build-windows: + when: << pipeline.parameters.windows >> + jobs: + - manual-build-validate: + name: Validating build parameters + os: windows + target: << pipeline.parameters.windows >> + - setup-sign-certificates: + name: Setup sign certificates (<< pipeline.parameters.env >>) + requires: + - Validating build parameters + - setup-build: + name: Setup build (<< pipeline.parameters.env >>) + env: << pipeline.parameters.env >> + requires: + - Setup sign certificates (<< pipeline.parameters.env >>) + - windows: + name: Build app - Windows (<< pipeline.parameters.env >>) + env: << pipeline.parameters.env >> + target: << pipeline.parameters.windows >> + requires: + - Setup build (<< pipeline.parameters.env >>) + - store-build-artifacts: + name: Store build artifacts (<< pipeline.parameters.env >>) + requires: + - Build app - Windows (<< pipeline.parameters.env >>) + manual-build-docker: + when: << pipeline.parameters.docker >> + jobs: + - manual-build-validate: + name: Validating build parameters + os: docker + target: << pipeline.parameters.docker >> + - setup-sign-certificates: + name: Setup sign certificates (<< pipeline.parameters.env >>) + requires: + - Validating build parameters + - setup-build: + name: Setup build (<< pipeline.parameters.env >>) + env: << pipeline.parameters.env >> + requires: + - Setup sign certificates (<< pipeline.parameters.env >>) + - docker: + name: Build docker images (<< pipeline.parameters.env >>) + requires: + - Setup build (<< pipeline.parameters.env >>) + - store-build-artifacts: + name: Store build artifacts (<< pipeline.parameters.env >>) + requires: + - Build docker images (<< pipeline.parameters.env >>) + + # build electron app (dev) from "build" branches + build: + <<: *ignore-for-manual-build + jobs: + - setup-sign-certificates: + name: Setup sign certificates (dev) + filters: + branches: + only: + - /^build.*/ + - setup-build: + name: Setup build (dev) + env: dev + requires: + - Setup sign certificates (dev) + - linux: + name: Build app - Linux (dev) + env: dev + requires: &devBuildRequire + - Setup build (dev) + - macosx: + name: Build app - MacOS (dev) + env: dev + requires: *devBuildRequire + - windows: + name: Build app - Windows (dev) + env: dev + requires: *devBuildRequire + - docker: + name: Build docker images (dev) + requires: *devBuildRequire + - store-build-artifacts: + name: Store build artifacts (dev) + requires: + - Build app - Linux (dev) + - Build app - MacOS (dev) + - Build app - Windows (dev) + - Build docker images (dev) + + # build electron app (dev) for internal use only + internal: + <<: *ignore-for-manual-build + jobs: + - setup-sign-certificates: + name: Setup sign certificates (dev) + filters: + branches: + only: + - /^internal.*/ + - setup-build: + name: Setup build (dev) + env: dev + requires: + - Setup sign certificates (dev) + - linux: + name: Build app - Linux (dev) + env: dev + requires: &devBuildRequire + - Setup build (dev) + - macosx: + name: Build app - MacOS (dev) + env: dev + requires: *devBuildRequire + - windows: + name: Build app - Windows (dev) + env: dev + requires: *devBuildRequire + - docker: + name: Build docker images (dev) + requires: *devBuildRequire + # release to private AWS (dev) + - release-aws-private-dev: + name: Release private AWS dev + requires: + - Build app - Linux (dev) + - Build app - MacOS (dev) + - Build app - Windows (dev) + - Build docker images (dev) + + # Main workflow for release/* and latest branches only + release: + <<: *ignore-for-manual-build + 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 + redis_client: + - << pipeline.parameters.redis_client >> + name: ITest - << matrix.rte >> (code) + filters: *releaseAndLatestFilter + - integration-tests-coverage: + name: ITest - Final coverage + requires: + - itest-code + + # ================== STAGE ================== + # prebuild (stage) + - setup-sign-certificates: + name: Setup sign certificates (stage) + requires: + - UTest - UI + - UTest - API + - ITest - Final coverage + <<: *stageFilter + - setup-build: + name: Setup build (stage) + requires: + - Setup sign certificates (stage) + # build electron app (stage) + - linux: + name: Build app - Linux (stage) + requires: &stageElectronBuildRequires + - Setup build (stage) + - macosx: + name: Build app - MacOS (stage) + requires: *stageElectronBuildRequires + - windows: + name: Build app - Windows (stage) + requires: *stageElectronBuildRequires + - docker: + name: Build docker images (stage) + requires: *stageElectronBuildRequires + # e2e desktop tests on AppImage build + - e2e-app-image: + name: E2ETest (AppImage) + parallelism: 2 + requires: + - Build app - Linux (stage) + # e2e docker tests + - e2e-tests: + name: E2ETest + build: docker + parallelism: 4 + requires: + - Build docker images (stage) + - store-build-artifacts: + name: Store build artifacts (stage) + requires: + - Build app - Linux (stage) + - Build app - MacOS (stage) + - Build app - Windows (stage) + - Build docker images (stage) + + # Needs approval from QA team that build was tested before merging to latest + - qa-approve: + name: Approved by QA team + type: approval + requires: + - Build app - Linux (stage) + - Build app - MacOS (stage) + - Build app - Windows (stage) + - Build docker images (stage) + + # ================== PROD ================== + # build and release electron app (prod) + - setup-sign-certificates: + name: Setup sign certificates (prod) + requires: + - UTest - UI + - UTest - API + - ITest - Final coverage + <<: *prodFilter + - setup-build: + name: Setup build (prod) + env: prod + requires: + - Setup sign certificates (prod) + - linux: + name: Build app - Linux (prod) + env: prod + requires: &prodElectronBuildRequires + - Setup build (prod) + - macosx: + name: Build app - MacOS (prod) + env: prod + requires: *prodElectronBuildRequires + - windows: + name: Build app - Windows (prod) + env: prod + requires: *prodElectronBuildRequires + - docker: + name: Build docker images (prod) + env: production + requires: *prodElectronBuildRequires + # e2e desktop tests on AppImage build + - e2e-app-image: + name: E2ETest (AppImage) + parallelism: 2 + requires: + - Build app - Linux (prod) + # e2e docker tests + - e2e-tests: + name: E2ETest + build: docker + parallelism: 4 + requires: + - Build docker images (prod) + # virus check all electron apps (prod) + - virustotal-file: + name: Virus check - AppImage (prod) + ext: .AppImage + requires: + - Build app - Linux (prod) + - virustotal-file: + name: Virus check - deb (prod) + ext: .deb + requires: + - Build app - Linux (prod) + - virustotal-file: + name: Virus check - rpm (prod) + ext: .rpm + requires: + - Build app - Linux (prod) + - virustotal-file: + name: Virus check - snap (prod) + ext: .snap + requires: + - Build app - Linux (prod) + - virustotal-file: + name: Virus check x64 - dmg (prod) + ext: -x64.dmg + requires: + - Build app - MacOS (prod) + - virustotal-file: + name: Virus check arm64 - dmg (prod) + ext: -arm64.dmg + requires: + - Build app - MacOS (prod) + - virustotal-file: + name: Virus check MAS - pkg (prod) + ext: -mas.pkg + requires: + - Build app - MacOS (prod) + - virustotal-file: + name: Virus check - exe (prod) + ext: .exe + requires: + - Build app - Windows (prod) + # upload release to prerelease AWS folder + - release-aws-private: + name: Release AWS S3 Private (prod) + requires: + - Virus check - AppImage (prod) + - Virus check - deb (prod) + - Virus check - rpm (prod) + - Virus check - snap (prod) + - Virus check x64 - dmg (prod) + - Virus check arm64 - dmg (prod) + - Virus check MAS - pkg (prod) + - Virus check - exe (prod) + - Build docker images (prod) + # Manual approve for publish release + - approve-publish: + name: Approve Publish Release (prod) + type: approval + 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" + - release-docker: + name: Release docker images + requires: + - Approve Publish Release (prod) + <<: *prodFilter # double check for "latest" + # Nightly tests + nightly: + triggers: + - schedule: + cron: '0 0 * * *' + filters: + branches: + only: + - main + jobs: + # build docker image + - docker: + name: Build docker image + # build desktop app + - setup-sign-certificates: + name: Setup sign certificates (stage) + - setup-build: + name: Setup build (stage) + requires: + - Setup sign certificates (stage) + - linux: + name: Build app - Linux (stage) + requires: + - Setup build (stage) + # - windows: + # name: Build app - Windows (stage) + # requires: + # - Setup build (stage) + # integration tests on docker image build + - integration-tests-run: + matrix: + alias: itest-docker + parameters: + rte: *iTestsNames + build: ['docker'] + report: [true] + name: ITest - << matrix.rte >> (docker) + requires: + - Build docker image + # e2e web tests on docker image build + - e2e-tests: + name: E2ETest - Nightly + parallelism: 4 + build: docker + report: true + requires: + - Build docker image + # e2e desktop tests on AppImage build + - e2e-app-image: + name: E2ETest (AppImage) - Nightly + parallelism: 2 + report: true + requires: + - Build app - Linux (stage) + + - virustotal-url: + name: Virus check - AppImage (nightly) + fileName: Redis-Insight-linux-x86_64.AppImage + - virustotal-url: + name: Virus check - deb (nightly) + fileName: Redis-Insight-linux-amd64.deb + - virustotal-url: + name: Virus check - rpm (nightly) + fileName: Redis-Insight-linux-x86_64.rpm + - virustotal-url: + name: Virus check - snap (nightly) + fileName: Redis-Insight-linux-amd64.snap + - virustotal-url: + name: Virus check x64 - dmg (nightly) + fileName: Redis-Insight-mac-x64.dmg + - virustotal-url: + name: Virus check arm64 - dmg (nightly) + fileName: Redis-Insight-mac-arm64.dmg + - virustotal-url: + name: Virus check MAS - pkg (nightly) + fileName: Redis-Insight-mac-universal-mas.pkg + - virustotal-url: + name: Virus check - exe (nightly) + fileName: Redis-Insight-win-installer.exe + - virustotal-report: + name: Virus check report (prod) + requires: + - Virus check - AppImage (nightly) + - Virus check - deb (nightly) + - Virus check - rpm (nightly) + - Virus check - snap (nightly) + - Virus check x64 - dmg (nightly) + - Virus check arm64 - dmg (nightly) + - Virus check MAS - pkg (nightly) + - Virus check - exe (nightly) + + # # e2e desktop tests on exe build + # - e2e-exe: + # name: E2ETest (exe) - Nightly + # parallelism: 4 + # report: true + # requires: + # - Build app - Windows (stage) + + weekly: + triggers: + - schedule: + cron: '0 0 * * 1' + filters: + branches: + only: + - main + jobs: + # Process all licenses + - licenses-check: + name: Process licenses of packages diff --git a/.circleci/e2e/test.app-image.sh b/.circleci/e2e/test.app-image.sh index 500da5d468..f3f52d7d2f 100755 --- a/.circleci/e2e/test.app-image.sh +++ b/.circleci/e2e/test.app-image.sh @@ -3,20 +3,52 @@ set -e yarn --cwd tests/e2e install -# mount app resources -./release/*.AppImage --appimage-mount >> apppath & +# Create the ri-test directory if it doesn't exist +mkdir -p ri-test + +# Extract the AppImage +chmod +x ./release/*.AppImage +./release/*.AppImage --appimage-extract + +# Move contents of squashfs-root to ri-test and remove squashfs-root folder +mv squashfs-root/* ri-test/ +rm -rf squashfs-root + +# Export custom XDG_DATA_DIRS with ri-test +export XDG_DATA_DIRS="$(pwd)/ri-test:$XDG_DATA_DIRS" # create folder before tests run to prevent permissions issue mkdir -p tests/e2e/remote mkdir -p tests/e2e/rdi -# run rte +# Create a custom .desktop file for RedisInsight +cat > ri-test/redisinsight.desktop <> $GITHUB_OUTPUT + diff --git a/.github/actions/install-all-build-libs/action.yml b/.github/actions/install-all-build-libs/action.yml index 058f360f67..93ce2b7fa1 100644 --- a/.github/actions/install-all-build-libs/action.yml +++ b/.github/actions/install-all-build-libs/action.yml @@ -33,7 +33,7 @@ runs: - name: Setup Node uses: actions/setup-node@v4.0.4 with: - node-version: '20.15' + node-version: '20.18.0' # disable cache for windows # https://github.com/actions/setup-node/issues/975 cache: ${{ runner.os != 'Windows' && 'yarn' || '' }} diff --git a/.github/actions/install-deps/action.yml b/.github/actions/install-deps/action.yml index 62c3fa5ab4..7ed4a1a77f 100644 --- a/.github/actions/install-deps/action.yml +++ b/.github/actions/install-deps/action.yml @@ -30,4 +30,4 @@ runs: # export npm_config_keytar_binary_host_mirror=${{ inputs.keytar-host-mirror }} # export npm_config_node_sqlite3_binary_host_mirror=${{ inputs.sqlite3-host-mirror }} - yarn install + yarn install --frozen-lockfile --network-timeout 1000000 diff --git a/.github/actions/remove-artifacts/action.yml b/.github/actions/remove-artifacts/action.yml new file mode 100644 index 0000000000..2fe4909a5c --- /dev/null +++ b/.github/actions/remove-artifacts/action.yml @@ -0,0 +1,23 @@ +name: Remove all artifacts + +runs: + using: 'composite' + steps: + - name: Merge artifacts by pattern + id: merge-artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: remove-artifacts + pattern: '*' + delete-merged: true + + - name: Delete merged artifact + uses: actions/github-script@v7 + with: + script: | + github.rest.actions.deleteArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: ${{ steps.merge-artifacts.outputs.artifact-id }} + }); + diff --git a/.github/build/build.Dockerfile b/.github/build/build.Dockerfile new file mode 100644 index 0000000000..3a16101820 --- /dev/null +++ b/.github/build/build.Dockerfile @@ -0,0 +1,38 @@ +FROM node:20.14-alpine + +# runtime args and environment variables +ARG DIST=Redis-Insight.tar.gz +ARG NODE_ENV=production +ARG RI_SEGMENT_WRITE_KEY +ENV RI_SEGMENT_WRITE_KEY=${RI_SEGMENT_WRITE_KEY} +ENV NODE_ENV=${NODE_ENV} +ENV RI_SERVE_STATICS=true +ENV RI_BUILD_TYPE='DOCKER_ON_PREMISE' +ENV RI_APP_FOLDER_ABSOLUTE_PATH='/data' + +# this resolves CVE-2023-5363 +# TODO: remove this line once we update to base image that doesn't have this vulnerability +RUN apk update && apk upgrade --no-cache libcrypto3 libssl3 + +# set workdir +WORKDIR /usr/src/app + +# copy artifacts built in previous stage to this one +ADD $DIST /usr/src/app/redisinsight +RUN ls -la /usr/src/app/redisinsight + +# folder to store local database, plugins, logs and all other files +RUN mkdir -p /data && chown -R node:node /data + +# copy the docker entry point script and make it executable +COPY --chown=node:node ./docker-entry.sh ./ +RUN chmod +x docker-entry.sh + +# since RI is hard-code to port 5000, expose it from the container +EXPOSE 5000 + +# don't run the node process as root +USER node + +# serve the application 🚀 +ENTRYPOINT ["./docker-entry.sh", "node", "redisinsight/api/dist/src/main"] diff --git a/.github/build/build.sh b/.github/build/build.sh new file mode 100755 index 0000000000..907587ced2 --- /dev/null +++ b/.github/build/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +# install deps +yarn +yarn --cwd redisinsight/api + +# build + +yarn build:statics +yarn build:ui +yarn --cwd ./redisinsight/api build:prod diff --git a/.github/build/build_modules.sh b/.github/build/build_modules.sh new file mode 100755 index 0000000000..bf01ebd1e8 --- /dev/null +++ b/.github/build/build_modules.sh @@ -0,0 +1,94 @@ +#!/bin/bash +set -e + +PLATFORM=${PLATFORM:-'linux'} +ARCH=${ARCH:-'x64'} +LIBC=${LIBC:-''} +#FILENAME="Redis-Insight-$PLATFORM.$VERSION.$ARCH.zip" +FILENAME="Redis-Insight-web-$PLATFORM" +if [ ! -z $LIBC ] +then + FILENAME="$FILENAME-$LIBC.$ARCH.tar.gz" + export npm_config_target_libc="$LIBC" +else + FILENAME="$FILENAME.$ARCH.tar.gz" +fi + +echo "Building node modules..." +echo "Platform: $PLATFORM" +echo "Arch: $ARCH" +echo "Libc: $LIBC" +echo "npm target libc: $npm_config_target_libc" +echo "Filname: $FILENAME" + +rm -rf redisinsight/api/node_modules + +npm_config_arch="$ARCH" \ +npm_config_target_arch="$ARCH" \ +npm_config_platform="$PLATFORM" \ +npm_config_target_platform="$PLATFORM" \ +yarn --cwd ./redisinsight/api install --production + +cp redisinsight/api/.yarnclean.prod redisinsight/api/.yarnclean +yarn --cwd ./redisinsight/api autoclean --force + +rm -rf redisinsight/build.zip + +cp LICENSE ./redisinsight + +cd redisinsight && tar -czf build.tar.gz \ +--exclude="api/node_modules/**/build/node_gyp_bins/python3" \ +api/node_modules \ +api/dist \ +ui/dist \ +LICENSE \ +&& cd .. + +mkdir -p release/web +cp redisinsight/build.tar.gz release/web/"$FILENAME" + +# Minify build via esbuild +echo "Start minifing workflow" +npm_config_arch="$ARCH" \ +npm_config_target_arch="$ARCH" \ +npm_config_platform="$PLATFORM" \ +npm_config_target_platform="$PLATFORM" \ +yarn --cwd ./redisinsight/api install +yarn --cwd ./redisinsight/api minify:prod + + +PACKAGE_JSON_PATH="./redisinsight/api/package.json" +APP_PACKAGE_JSON_PATH="./redisinsight/package.json" + +# Extract dependencies from the app package.json +BINARY_PACKAGES=$(jq -r '.dependencies | keys[]' "$APP_PACKAGE_JSON_PATH" | jq -R -s -c 'split("\n")[:-1]') + +echo "Binary packages to exclude during minify: $BINARY_PACKAGES" + +# Modify the package.json +jq --argjson keep "$BINARY_PACKAGES" \ + 'del(.devDependencies) | .dependencies |= with_entries(select(.key as $k | $keep | index($k)))' \ + "$PACKAGE_JSON_PATH" > temp.json && mv temp.json "$PACKAGE_JSON_PATH" + +npm_config_arch="$ARCH" \ +npm_config_target_arch="$ARCH" \ +npm_config_platform="$PLATFORM" \ +npm_config_target_platform="$PLATFORM" \ +yarn --cwd ./redisinsight/api install --production +yarn --cwd ./redisinsight/api autoclean --force + +# Compress minified build +cd redisinsight && tar -czf build-mini.tar.gz \ +--exclude="api/node_modules/**/build/node_gyp_bins/python3" \ +api/node_modules \ +api/dist-minified \ +ui/dist \ +LICENSE \ +&& cd .. + +mkdir -p release/web-mini +cp redisinsight/build-mini.tar.gz release/web-mini/"$FILENAME" + +# Restore the original package.json and yarn.lock +git restore redisinsight/api/yarn.lock redisinsight/api/package.json + diff --git a/.github/build/release-docker.sh b/.github/build/release-docker.sh index 4dd3d0fad3..c388a75c91 100755 --- a/.github/build/release-docker.sh +++ b/.github/build/release-docker.sh @@ -2,7 +2,7 @@ set -e HELP="Args: --v - Semver (2.60.0) +-v - Semver (2.62.0) -d - Build image repository (Ex: -d redisinsight) -r - Target repository (Ex: -r redis/redisinsight) " diff --git a/.github/build/sum_sha256.sh b/.github/build/sum_sha256.sh new file mode 100755 index 0000000000..4af88b1e07 --- /dev/null +++ b/.github/build/sum_sha256.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +find ./release -type f -name '*.tar.gz' -execdir sh -c 'sha256sum "$1" > "$1.sha256"' _ {} \; diff --git a/.github/deps-audit-report.js b/.github/deps-audit-report.js new file mode 100644 index 0000000000..c7a2dc82bb --- /dev/null +++ b/.github/deps-audit-report.js @@ -0,0 +1,83 @@ +const fs = require('fs'); +const { exec } = require("child_process"); + +const FILENAME = process.env.FILENAME; +const DEPS = process.env.DEPS || ''; +const file = `${FILENAME}`; +const outputFile = `slack.${FILENAME}`; + +function generateSlackMessage (summary) { + const message = { + text: `DEPS AUDIT: *${DEPS}* result (Branch: *${process.env.GITHUB_REF_NAME}*)` + + `\nScanned ${summary.totalDependencies} dependencies` + + `\n`, + attachments: [], + }; + + if (summary.totalVulnerabilities) { + if (summary.vulnerabilities.critical) { + message.attachments.push({ + title: 'Critical', + color: '#641E16', + text: `${summary.vulnerabilities.critical}`, + }); + } + if (summary.vulnerabilities.high) { + message.attachments.push({ + title: 'High', + color: '#C0392B', + text: `${summary.vulnerabilities.high}`, + }); + } + if (summary.vulnerabilities.moderate) { + message.attachments.push({ + title: 'Moderate', + color: '#F5B041', + text: `${summary.vulnerabilities.moderate}`, + }); + } + if (summary.vulnerabilities.low) { + message.attachments.push({ + title: 'Low', + color: '#F9E79F', + text: `${summary.vulnerabilities.low}`, + }); + } + if (summary.vulnerabilities.info) { + message.attachments.push({ + title: 'Info', + text: `${summary.vulnerabilities.info}`, + }); + } + } else { + message.attachments.push( + { + title: 'No vulnerabilities found', + color: 'good' + } + ); + } + + return message; +} + +async function main() { + const lastAuditLine = await new Promise((resolve, reject) => { + exec(`tail -n 1 ${file}`, (error, stdout, stderr) => { + if (error) { + return reject(error); + } + resolve(stdout); + }) + }) + + const { data: summary } = JSON.parse(`${lastAuditLine}`); + const vulnerabilities = summary?.vulnerabilities || {}; + summary.totalVulnerabilities = Object.values(vulnerabilities).reduce((totalVulnerabilities, val) => totalVulnerabilities + val) + fs.writeFileSync(outputFile, JSON.stringify({ + channel: process.env.SLACK_AUDIT_REPORT_CHANNEL, + ...generateSlackMessage(summary), + })); +} + +main(); diff --git a/.github/deps-licenses-report.js b/.github/deps-licenses-report.js new file mode 100644 index 0000000000..719b3e4bad --- /dev/null +++ b/.github/deps-licenses-report.js @@ -0,0 +1,253 @@ +const fs = require('fs'); +const { join } = require('path'); +const { last, set } = require('lodash'); +const { google } = require('googleapis'); +const { execFile } = require('child_process'); +const csvParser = require('csv-parser'); +const { stringify } = require('csv-stringify'); + +const licenseFolderName = 'licenses'; +const spreadsheetId = process.env.SPREADSHEET_ID; +const outputFilePath = `./${licenseFolderName}/licenses.csv`; +const summaryFilePath = `./${licenseFolderName}/summary.csv`; +const allData = []; +let csvFiles = []; + + +// Main function +async function main() { + const folderPath = './'; + const packageJsons = findPackageJsonFiles(folderPath); // Find all package.json files in the given folder + + console.log('All package.jsons was found:', packageJsons); + + // Create the folder if it doesn't exist + if (!fs.existsSync(licenseFolderName)) { + fs.mkdirSync(licenseFolderName); + } + + try { + await Promise.all(packageJsons.map(runLicenseCheck)); + console.log('All csv files was generated'); + await generateSummary() + await sendLicensesToGoogleSheet() + } catch (error) { + console.error('An error occurred:', error); + process.exit(1); + } +} + +main(); + +// Function to find all package.json files in a given folder +function findPackageJsonFiles(folderPath) { + const packageJsonPaths = []; + const packageJsonName = 'package.json'; + const excludeFolders = ['dist', 'node_modules', 'static', 'electron', 'redisgraph']; + + // Recursive function to search for package.json files + function searchForPackageJson(currentPath) { + const files = fs.readdirSync(currentPath); + + for (const file of files) { + const filePath = join(currentPath, file); + const stats = fs.statSync(filePath); + + if (stats.isDirectory() && !excludeFolders.includes(file)) { + searchForPackageJson(filePath); + } else if (file === packageJsonName) { + packageJsonPaths.push(`./${filePath.slice(0, -packageJsonName.length - 1)}`); + } + } + } + + searchForPackageJson(folderPath); + return packageJsonPaths; +} + +// Function to run license check for a given package.json file +async function runLicenseCheck(path) { + const name = last(path.split('/')) || 'electron'; + + const COMMANDS = [ + `license-checker --start ${path} --csv --out ./${licenseFolderName}/${name}_prod.csv --production`, + `license-checker --start ${path} --csv --out ./${licenseFolderName}/${name}_dev.csv --development`, + ] + + return await Promise.all(COMMANDS.map((command) => { + const [cmd, ...args] = command.split(' '); + return new Promise((resolve, reject) => { + execFile(cmd, args, (error, stdout, stderr) => { + if (error) { + console.error(`Failed command: ${command}, error:`, stderr); + reject(error); + } + resolve(); + }); + }); + })); +} + +async function sendLicensesToGoogleSheet() { + try { + const serviceAccountKey = JSON.parse(fs.readFileSync('./gasKey.json', 'utf-8')); + + // Set up JWT client + const jwtClient = new google.auth.JWT( + serviceAccountKey.client_email, + null, + serviceAccountKey.private_key, + ['https://www.googleapis.com/auth/spreadsheets'] + ); + + const sheets = google.sheets('v4'); + + // Read all .csv files in the 'licenses' folder + csvFiles.forEach((csvFile) => { + // Extract sheet name from file name + const sheetName = csvFile.replace('.csv', '').replaceAll('_', ' '); + + const data = []; + fs.createReadStream(`./${licenseFolderName}/${csvFile}`) + .pipe(csvParser({ headers: false })) + .on('data', (row) => { + data.push(Object.values(row)); + }) + .on('end', async () => { + const resource = { values: data }; + + try { + const response = await sheets.spreadsheets.get({ + auth: jwtClient, + spreadsheetId, + }); + + const sheet = response.data.sheets.find((sheet) => sheet.properties.title === sheetName); + if (sheet) { + // Clear contents of the sheet starting from cell A2 + await sheets.spreadsheets.values.clear({ + auth: jwtClient, + spreadsheetId, + range: `${sheetName}!A1:Z`, // Assuming Z is the last column + }); + } else { + // Create the sheet if it doesn't exist + await sheets.spreadsheets.batchUpdate({ + auth: jwtClient, + spreadsheetId, + resource: set({}, 'requests[0].addSheet.properties.title', sheetName), + }); + } + } catch (error) { + console.error(`Error checking/creating sheet for ${sheetName}:`, error); + } + + try { + await sheets.spreadsheets.values.batchUpdate({ + auth: jwtClient, + spreadsheetId, + resource: { + valueInputOption: 'RAW', + data: [ + { + range: `${sheetName}!A1`, // Use the sheet name as the range and start from A2 + majorDimension: 'ROWS', + values: data, + }, + ], + }, + }); + + console.log(`CSV data has been inserted into ${sheetName} sheet.`); + } catch (err) { + console.error(`Error inserting data for ${sheetName}:`, err); + } + }); + }); + } catch (error) { + console.error('Error loading service account key:', error); + } +} + +// Function to read and process each CSV file +const processCSVFile = (file) => { + return new Promise((resolve, reject) => { + const parser = csvParser({ columns: true, trim: true }); + const input = fs.createReadStream(`./${licenseFolderName}/${file}`); + + parser.on('data', (record) => { + allData.push(record); + }); + + parser.on('end', () => { + resolve(); + }); + + parser.on('error', (err) => { + reject(err); + }); + + input.pipe(parser); + }); +}; + +// Process and aggregate license data +const processLicenseData = () => { + const licenseCountMap = {}; + for (const record of allData) { + const license = record.license; + licenseCountMap[license] = (licenseCountMap[license] || 0) + 1; + } + return licenseCountMap; +}; + +// Create summary CSV data +const createSummaryData = (licenseCountMap) => { + const summaryData = [['License', 'Count']]; + for (const license in licenseCountMap) { + summaryData.push([license, licenseCountMap[license]]); + } + return summaryData; +}; + +// Write summary CSV file +const writeSummaryCSV = async (summaryData) => { + try { + const summaryCsvString = await stringifyPromise(summaryData); + fs.writeFileSync(summaryFilePath, summaryCsvString); + csvFiles.push(last(summaryFilePath.split('/'))); + console.log(`Summary CSV saved as ${summaryFilePath}`); + } catch (err) { + console.error(`Error: ${err}`); + } +}; + +// Stringify as a promise +const stringifyPromise = (data) => { + return new Promise((resolve, reject) => { + stringify(data, (err, csvString) => { + if (err) { + reject(err); + } else { + resolve(csvString); + } + }); + }); +}; + +async function generateSummary() { + csvFiles = fs.readdirSync(licenseFolderName).filter(file => file.endsWith('.csv')).sort(); + + for (const file of csvFiles) { + try { + await processCSVFile(file); + } catch (err) { + console.error(`Error processing ${file}: ${err}`); + } + } + + const licenseCountMap = processLicenseData(); + const summaryData = createSummaryData(licenseCountMap); + + await writeSummaryCSV(summaryData); +} diff --git a/.github/e2e-results.js b/.github/e2e-results.js new file mode 100644 index 0000000000..51c722fc90 --- /dev/null +++ b/.github/e2e-results.js @@ -0,0 +1,58 @@ +const fs = require('fs'); + +let parallelNodeInfo = ''; +// const totalNodes = parseInt(process.env.NODE_TOTAL, 10); +const totalNodes = 4; +if (totalNodes > 1) { + parallelNodeInfo = ` (node: ${parseInt(process.env.NODE_INDEX, 10) + 1}/${totalNodes})` +} + +const file = 'tests/e2e/results/e2e.results.json' +const appBuildType = process.env.APP_BUILD_TYPE || 'Web' +const results = { + message: { + text: `*E2ETest - ${appBuildType}${parallelNodeInfo}* (Branch: *${process.env.GITHUB_REF_NAME}*)` + + `\n`, + attachments: [], + }, +}; + +const result = JSON.parse(fs.readFileSync(file, 'utf-8')) +const testRunResult = { + color: '#36a64f', + title: `Started at: *${result.startTime}`, + text: `Executed ${result.total} in ${(new Date(result.endTime) - new Date(result.startTime)) / 1000}s`, + fields: [ + { + title: 'Passed', + value: result.passed, + short: true, + }, + { + title: 'Skipped', + value: result.skipped, + short: true, + }, + ], +}; +const failed = result.total - result.passed; +if (failed) { + results.passed = false; + testRunResult.color = '#cc0000'; + testRunResult.fields.push({ + title: 'Failed', + value: failed, + short: true, + }); +} + +results.message.attachments.push(testRunResult); + +if (results.passed === false) { + results.message.text = ' ' + results.message.text; +} + +fs.writeFileSync('e2e.report.json', JSON.stringify({ + channel: process.env.SLACK_TEST_REPORT_CHANNEL, + ...results.message, +})); diff --git a/.github/e2e/test.app-image.sh b/.github/e2e/test.app-image.sh new file mode 100755 index 0000000000..9d2b9b2f39 --- /dev/null +++ b/.github/e2e/test.app-image.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +yarn --cwd tests/e2e install + +# mount app resources +chmod +x ./release/*.AppImage +./release/*.AppImage --appimage-mount >> apppath & + +# create folder before tests run to prevent permissions issue +mkdir -p tests/e2e/remote +mkdir -p tests/e2e/rdi + +# run rte +docker compose -f tests/e2e/rte.docker-compose.yml build +docker compose -f tests/e2e/rte.docker-compose.yml up --force-recreate -d -V +./tests/e2e/wait-for-redis.sh localhost 12000 && \ + +# run tests +COMMON_URL=$(tail -n 1 apppath)/resources/app.asar/dist/renderer/index.html \ +ELECTRON_PATH=$(tail -n 1 apppath)/redisinsight \ +RI_SOCKETS_CORS=true \ +yarn --cwd tests/e2e dotenv -e .desktop.env yarn --cwd tests/e2e test:desktop:ci diff --git a/.github/e2e/test.app-image.sso.sh b/.github/e2e/test.app-image.sso.sh new file mode 100755 index 0000000000..aa9d8fcbd9 --- /dev/null +++ b/.github/e2e/test.app-image.sso.sh @@ -0,0 +1,54 @@ +#!/bin/bash +set -e + +yarn --cwd tests/e2e install + +# Create the ri-test directory if it doesn't exist +mkdir -p ri-test + +# Extract the AppImage +chmod +x ./release/*.AppImage +./release/*.AppImage --appimage-extract + +# Move contents of squashfs-root to ri-test and remove squashfs-root folder +mv squashfs-root/* ri-test/ +rm -rf squashfs-root + +# Export custom XDG_DATA_DIRS with ri-test +export XDG_DATA_DIRS="$(pwd)/ri-test:$XDG_DATA_DIRS" + +# create folder before tests run to prevent permissions issue +mkdir -p tests/e2e/remote +mkdir -p tests/e2e/rdi + +# Create a custom .desktop file for RedisInsight +cat > ri-test/redisinsight.desktop < `docker/${file}`) : []; + + // Combine all files into a single array + const allFiles = [...files, ...dockerFiles] + + // Mapping file names to Markdown links and categories + const fileMappings = { + 'Redis-Insight-mac-arm64.dmg': { name: 'Redis Insight for Mac (arm64 DMG)', category: Categories.MacOS }, + 'Redis-Insight-mac-x64.dmg': { name: 'Redis Insight for Mac (x64 DMG)', category: Categories.MacOS }, + 'Redis-Insight-win-installer.exe': { name: 'Redis Insight Windows Installer (exe)', category: Categories.Windows }, + 'Redis-Insight-linux-x86_64.AppImage': { name: 'Redis Insight for Linux (AppImage)', category: Categories.Linux }, + 'Redis-Insight-linux-amd64.deb': { name: 'Redis Insight for Linux (deb)', category: Categories.Linux }, + 'Redis-Insight-linux-amd64.snap': { name: 'Redis Insight for Linux (snap)', category: Categories.Linux }, + 'Redis-Insight-linux-x86_64.rpm': { name: 'Redis Insight for Linux (rpm)', category: Categories.Linux }, + 'docker/docker-linux-alpine.amd64.tar': { name: 'Redis Insight Docker Image (amd64)', category: Categories.Docker }, + 'docker/docker-linux-alpine.arm64.tar': { name: 'Redis Insight Docker Image (arm64)', category: Categories.Docker }, + } + + const categories = {} + + // Populate categories with existing files + allFiles.forEach((file) => { + const mapping = fileMappings[file] + if (mapping) { + if (!categories[mapping.category]) { + categories[mapping.category] = [] + } + const s3path = `https://s3.${AWS_DEFAULT_REGION}.amazonaws.com/${AWS_BUCKET_NAME_TEST}` + const href = `${s3path}/public/${SUB_PATH}/${file}` + + categories[mapping.category].push(`- [${ mapping.name }](${href})`) + } + }) + + // Prepare the summary markdown document + const markdownLines = ['## Builds:', ''] + + // Append categories to markdown if they have entries + Object.keys(categories).forEach((category) => { + if (categories[category].length) { + markdownLines.push(`### ${category}`, '', ...categories[category], '') + } + }) + + const data = markdownLines.join('\n') + const summaryFilePath = GITHUB_STEP_SUMMARY + + await appendFile(summaryFilePath, data, { encoding: 'utf8' }) + + console.log('Build summary generated successfully.') + + } catch (error) { + console.error(error); + } +} + +generateBuildSummary() diff --git a/.github/itest-results.js b/.github/itest-results.js new file mode 100644 index 0000000000..082254a902 --- /dev/null +++ b/.github/itest-results.js @@ -0,0 +1,51 @@ +const fs = require('fs'); + +const file = 'redisinsight/api/test/test-runs/coverage/test-run-result.json' + +const results = { + message: { + text: `*ITest - ${process.env.ITEST_NAME}* (Branch: *${process.env.GITHUB_REF_NAME}*)` + + `\n`, + attachments: [], + }, +}; + +const result = JSON.parse(fs.readFileSync(file, 'utf-8')) +const testRunResult = { + color: '#36a64f', + title: `Started at: ${result.stats.start}`, + text: `Executed ${result.stats.tests} in ${result.stats.duration / 1000}s`, + fields: [ + { + title: 'Passed', + value: result.stats.passes, + short: true, + }, + { + title: 'Skipped', + value: result.stats.pending, + short: true, + }, + ], +}; + +if (result.stats.failures) { + results.passed = false; + testRunResult.color = '#cc0000'; + testRunResult.fields.push({ + title: 'Failed', + value: result.stats.failures, + short: true, + }); +} + +results.message.attachments.push(testRunResult); + +if (results.passed === false) { + results.message.text = ' ' + results.message.text; +} + +fs.writeFileSync('itests.report.json', JSON.stringify({ + channel: process.env.SLACK_TEST_REPORT_CHANNEL, + ...results.message, +})); diff --git a/.github/lint-report.js b/.github/lint-report.js new file mode 100644 index 0000000000..8e89cb9ea5 --- /dev/null +++ b/.github/lint-report.js @@ -0,0 +1,62 @@ +const fs = require('fs'); + +const FILENAME = process.env.FILENAME || 'lint.audit.json'; +const WORKDIR = process.env.WORKDIR || '.'; +const TARGET = process.env.TARGET || ''; +const file = `${WORKDIR}/${FILENAME}`; +const outputFile = `${WORKDIR}/slack.${FILENAME}`; + +function generateSlackMessage (summary) { + const message = { + text: `CODE SCAN: *${TARGET}* result (Branch: *${process.env.GITHUB_REF_NAME}*)` + + `\n`, + attachments: [], + }; + + if (summary.total) { + if (summary.errors) { + message.attachments.push({ + title: 'Errors', + color: '#C0392B', + text: `${summary.errors}`, + }); + } + if (summary.warnings) { + message.attachments.push({ + title: 'Warnings', + color: '#F5B041', + text: `${summary.warnings}`, + }); + } + } else { + message.attachments.push( + { + title: 'No issues found', + color: 'good' + } + ); + } + + return message; +} + +async function main() { + const summary = { + errors: 0, + warnings: 0, + }; + const scanResult = JSON.parse(fs.readFileSync(file)); + scanResult.forEach(fileResult => { + summary.errors += fileResult.errorCount; + summary.warnings += fileResult.warningCount; + }); + + summary.total = summary.errors + summary.warnings; + + fs.writeFileSync(outputFile, JSON.stringify({ + channel: process.env.SLACK_AUDIT_REPORT_CHANNEL, + ...generateSlackMessage(summary), + })); +} + +main(); diff --git a/.github/redisstack/app-image.repack.sh b/.github/redisstack/app-image.repack.sh new file mode 100755 index 0000000000..a9e183cc06 --- /dev/null +++ b/.github/redisstack/app-image.repack.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +ARCH=${ARCH:-x86_64} +WORKING_DIRECTORY=$(pwd) +SOURCE_APP=${SOURCE_APP:-"Redis-Insight-linux-$ARCH.AppImage"} +RI_APP_FOLDER_NAME="Redis-Insight-linux" +TAR_NAME="Redis-Insight-app-linux.$ARCH.tar.gz" +TMP_FOLDER="/tmp/Redis-Insight-app-$ARCH" + +rm -rf "$TMP_FOLDER" + +mkdir -p "$WORKING_DIRECTORY/release/redisstack" +mkdir -p "$TMP_FOLDER" + +cp "./release/$SOURCE_APP" "$TMP_FOLDER" +cd "$TMP_FOLDER" || exit 1 + +./"$SOURCE_APP" --appimage-extract +mv squashfs-root "$RI_APP_FOLDER_NAME" + +tar -czvf "$TAR_NAME" "$RI_APP_FOLDER_NAME" + +cp "$TAR_NAME" "$WORKING_DIRECTORY/release/redisstack/" +cd "$WORKING_DIRECTORY" || exit 1 diff --git a/.github/redisstack/dmg.repack.sh b/.github/redisstack/dmg.repack.sh new file mode 100755 index 0000000000..023bfd946f --- /dev/null +++ b/.github/redisstack/dmg.repack.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -e + +ARCH=${ARCH:-x64} +WORKING_DIRECTORY=$(pwd) +TAR_NAME="Redis-Insight-app-darwin.$ARCH.tar.gz" +RI_APP_FOLDER_NAME="Redis Insight.app" +TMP_FOLDER="/tmp/$RI_APP_FOLDER_NAME" + +rm -rf "$TMP_FOLDER" + +mkdir -p "$WORKING_DIRECTORY/release/redisstack" +mkdir -p "$TMP_FOLDER" + +hdiutil attach "./release/Redis-Insight-mac-$ARCH.dmg" +rsync -av /Volumes/Redis*/Redis\ Insight.app "/tmp" +cd "/tmp" || exit 1 +tar -czvf "$TAR_NAME" "$RI_APP_FOLDER_NAME" +cp "$TAR_NAME" "$WORKING_DIRECTORY/release/redisstack/" +cd "$WORKING_DIRECTORY" || exit 1 +hdiutil unmount /Volumes/Redis*/ diff --git a/.github/workflows/aws-upload-dev.yml b/.github/workflows/aws-upload-dev.yml new file mode 100644 index 0000000000..70c1ce9362 --- /dev/null +++ b/.github/workflows/aws-upload-dev.yml @@ -0,0 +1,59 @@ +name: AWS Development + +on: + workflow_call: + inputs: + pre-release: + type: boolean + default: false + +env: + AWS_BUCKET_NAME_TEST: ${{ vars.AWS_BUCKET_NAME_TEST }} + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + +jobs: + s3: + name: Upload to s3 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Get current date + id: date + uses: ./.github/actions/get-current-date + + - name: Download builds + uses: actions/download-artifact@v4 + with: + pattern: '*-builds' + path: release + merge-multiple: true + + - run: ls -R ./release + + - name: Upload builds to s3 bucket dev sub folder + if: ${{ !inputs.pre-release }} + run: | + SUB_PATH="dev-builds/${{ steps.date.outputs.date }}/${{ github.run_id }}" + echo "SUB_PATH=${SUB_PATH}" >> $GITHUB_ENV + + aws s3 cp release/ s3://${AWS_BUCKET_NAME_TEST}/public/${SUB_PATH} --recursive + + - name: Upload builds to s3 bucket pre-releasea sub folder + if: inputs.pre-release + run: | + APP_VERSION=$(jq -r '.version' redisinsight/package.json) + SUB_PATH="pre-release/${APP_VERSION}" + + echo "SUB_PATH=${SUB_PATH}" >> $GITHUB_ENV + + aws s3 cp release/ s3://${AWS_BUCKET_NAME_TEST}/public/upgrades --recursive + aws s3 cp release/ s3://${AWS_BUCKET_NAME_TEST}/public/pre-release/${APP_VERSION} --recursive + + - name: Generate job summary + run: | + node ./.github/generate-build-summary.js + diff --git a/.github/workflows/aws.yml b/.github/workflows/aws-upload-prod.yml similarity index 89% rename from .github/workflows/aws.yml rename to .github/workflows/aws-upload-prod.yml index 4ceab7c9c3..515222428b 100644 --- a/.github/workflows/aws.yml +++ b/.github/workflows/aws-upload-prod.yml @@ -1,4 +1,4 @@ -name: AWS +name: AWS production on: workflow_call: @@ -8,7 +8,7 @@ env: AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} jobs: release-private: @@ -17,35 +17,26 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Download All Artifacts + - name: Merge builds by pattern + id: merge-builds + uses: actions/upload-artifact/merge@v4 + with: + name: 'all-builds' + pattern: '*-builds' + delete-merged: true + + - name: Download builds uses: actions/download-artifact@v4 with: - path: ./release + name: 'all-builds' + path: release - run: ls -R ./release - name: Publish private run: | - - # Define array of folders to exclude - exclude=("web" "web-mini" "redisstack" "docker") - - # Iterate through first-level directories in ./release - for dir in ./release/*/; do - dir_name=$(basename "$dir") - - # Check if the directory is not in the exclude list - if [[ ! " ${exclude[@]} " =~ " ${dir_name} " ]]; then - # Move all files from the subdirectory to the release directory - mv "$dir"* ./release/ - - # Remove the now-empty subdirectory - rmdir "$dir" - fi - done - - chmod +x .circleci/build/sum_sha256.sh - .circleci/build/sum_sha256.sh + chmod +x .github/build/sum_sha256.sh + .github/build/sum_sha256.sh applicationVersion=$(jq -r '.version' redisinsight/package.json) aws s3 cp release/ s3://${AWS_BUCKET_NAME}/private/${applicationVersion} --recursive diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 008e24d9b0..2e4071513c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,33 +1,6 @@ name: Build on: - # Manual trigger build - workflow_dispatch: - inputs: - target: - description: Build target - required: false - default: 'all' - type: choice - options: - - all - - docker - - windows:x64 - - macos:x64 - - macos:arm64 - - macos:all - - linux:appimage:x64 - - linux:deb:x64 - - linux:rpm:x64 - - linux:snap:x64 - - linux:all - - environment: - description: Environment to run build - type: environment - default: 'staging' - required: false - # Called for Release workflows workflow_call: inputs: @@ -35,41 +8,47 @@ on: description: Environment to run build type: string default: 'staging' - required: false + target: description: Build target type: string default: 'all' - required: false + + debug: + description: Enable SSH Debug + type: boolean jobs: build-linux: - if: startsWith(inputs.target, 'linux') || endsWith(inputs.target, 'all') - # concurrency: build + if: contains(inputs.target, 'linux') || inputs.target == 'all' uses: ./.github/workflows/pipeline-build-linux.yml secrets: inherit with: environment: ${{ inputs.environment }} - target: ${{ (endsWith(inputs.target, 'all') && 'all') || inputs.target }} + target: ${{ inputs.target }} + debug: ${{ inputs.debug }} build-macos: - if: startsWith(inputs.target, 'macos') || endsWith(inputs.target, 'all') + if: contains(inputs.target, 'macos') || inputs.target == 'all' uses: ./.github/workflows/pipeline-build-macos.yml secrets: inherit with: environment: ${{ inputs.environment }} - target: ${{ (endsWith(inputs.target, 'all') && 'all') || inputs.target }} + target: ${{ inputs.target }} + debug: ${{ inputs.debug }} build-windows: - if: startsWith(inputs.target, 'windows') || endsWith(inputs.target, 'all') + if: contains(inputs.target, 'windows') || inputs.target == 'all' uses: ./.github/workflows/pipeline-build-windows.yml secrets: inherit with: environment: ${{ inputs.environment }} + debug: ${{ inputs.debug }} build-docker: - if: startsWith(inputs.target, 'docker') || endsWith(inputs.target, 'all') + if: contains(inputs.target, 'docker') || inputs.target == 'all' uses: ./.github/workflows/pipeline-build-docker.yml secrets: inherit with: environment: ${{ inputs.environment }} + debug: ${{ inputs.debug }} diff --git a/.github/workflows/clean-deployments.yml b/.github/workflows/clean-deployments.yml new file mode 100644 index 0000000000..96fcb822a9 --- /dev/null +++ b/.github/workflows/clean-deployments.yml @@ -0,0 +1,30 @@ +name: Delete deployments +on: + workflow_call: + +jobs: + clean: + name: Clean deployments + runs-on: ubuntu-latest + steps: + - name: 🗑 Delete deployment (staging) + uses: strumwolf/delete-deployment-environment@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + environment: staging + onlyRemoveDeployments: true + + - name: 🗑 Delete deployment (production) + uses: strumwolf/delete-deployment-environment@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + environment: production + onlyRemoveDeployments: true + + - name: 🗑 Delete deployment (gh-actions) + uses: strumwolf/delete-deployment-environment@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + environment: gh-pages + onlyRemoveDeployments: true + diff --git a/.github/workflows/clean-s3-dev-builds.yml b/.github/workflows/clean-s3-dev-builds.yml new file mode 100644 index 0000000000..44bb6dc3da --- /dev/null +++ b/.github/workflows/clean-s3-dev-builds.yml @@ -0,0 +1,24 @@ +name: Clean AWS S3 development builds +on: + workflow_call: + +env: + AWS_BUCKET_NAME_TEST: ${{ secrets.AWS_BUCKET_NAME_TEST }} + AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + +jobs: + deleting: + runs-on: ubuntu-latest + steps: + - name: Deleting builds and test reports older than 7 days + continue-on-error: true + run: | + DATE=$(date +'%Y-%m-%d') + DATE_EPIRED=$(date -d "$DATE - 7 days" +'%Y-%m-%d') + + aws s3 rm s3://${AWS_BUCKET_NAME_TEST}/public/dev-builds/${DATE_EPIRED} --recursive + aws s3 rm s3://${AWS_BUCKET_NAME_TEST}/public/test-reports/${DATE_EPIRED} --recursive + diff --git a/.github/workflows/compress-images.yml b/.github/workflows/compress-images.yml index f27ce65e3f..ad2211d751 100644 --- a/.github/workflows/compress-images.yml +++ b/.github/workflows/compress-images.yml @@ -16,12 +16,12 @@ jobs: permissions: write-all runs-on: ubuntu-latest steps: - - name: Checkout Repo - uses: actions/checkout@v4 + - name: Checkout Repo + uses: actions/checkout@v4 - - name: Compress Images - uses: calibreapp/image-actions@main - with: - # The `GITHUB_TOKEN` is automatically generated by GitHub and scoped only to the repository that is currently running the action. By default, the action can’t update Pull Requests initiated from forked repositories. - # See https://docs.github.com/en/actions/reference/authentication-in-a-workflow and https://help.github.com/en/articles/virtual-environments-for-github-actions#token-permissions - githubToken: ${{ secrets.GITHUB_TOKEN }} + - name: Compress Images + uses: calibreapp/image-actions@main + with: + # The `GITHUB_TOKEN` is automatically generated by GitHub and scoped only to the repository that is currently running the action. By default, the action can’t update Pull Requests initiated from forked repositories. + # See https://docs.github.com/en/actions/reference/authentication-in-a-workflow and https://help.github.com/en/articles/virtual-environments-for-github-actions#token-permissions + githubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/licenses-check.yml b/.github/workflows/licenses-check.yml index e82705ffb3..dccb2679ba 100644 --- a/.github/workflows/licenses-check.yml +++ b/.github/workflows/licenses-check.yml @@ -25,7 +25,7 @@ jobs: run: | npm i -g license-checker echo "$GOOGLE_ACCOUNT_SERVICE_KEY_BASE64" | base64 -id > gasKey.json - SPREADSHEET_ID=$GOOGLE_SPREADSHEET_DEPENDENCIES_ID node .circleci/deps-licenses-report.js + SPREADSHEET_ID=$GOOGLE_SPREADSHEET_DEPENDENCIES_ID node .github/deps-licenses-report.js - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/manual-build.yml b/.github/workflows/manual-build.yml new file mode 100644 index 0000000000..7ae3d5a2cf --- /dev/null +++ b/.github/workflows/manual-build.yml @@ -0,0 +1,113 @@ +name: 🚀 Manual build + +on: + # Manual trigger build + # No multi-select + # https://github.com/actions/runner/issues/2076 + workflow_dispatch: + inputs: + build_docker: + description: Build Docker + type: boolean + required: false + + build_windows_x64: + description: Build Windows x64 + type: boolean + required: false + + build_macos_x64: + description: Build macOS x64 + type: boolean + required: false + + build_macos_arm64: + description: Build macOS arm64 + type: boolean + required: false + + build_linux_appimage_x64: + description: Build Linux AppImage x64 + type: boolean + required: false + + build_linux_deb_x64: + description: Build Linux deb x64 + type: boolean + required: false + + build_linux_rpm_x64: + description: Build Linux rpm x64 + type: boolean + required: false + + build_linux_snap_x64: + description: Build Linux snap x64 + type: boolean + required: false + + environment: + description: Environment to run build + type: environment + default: 'development' + required: false + + debug: + description: Enable SSH Debug + type: boolean + +# Cancel a previous same workflow +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + get-selected: + runs-on: ubuntu-latest + outputs: # Set this to consume the output on other job + selected: ${{ steps.get-selected.outputs.selected}} + steps: + - uses: actions/checkout@v4 + + - id: get-selected + uses: joao-zanutto/get-selected@v1.1.1 + with: + format: 'list' + + - name: echo selected targets + run: echo ${{ steps.get-selected.outputs.selected }} + + manual-build: + needs: get-selected + uses: ./.github/workflows/build.yml + secrets: inherit + with: + target: ${{ needs.get-selected.outputs.selected }} + debug: ${{ inputs.debug }} + environment: ${{ inputs.environment }} + + aws-upload: + uses: ./.github/workflows/aws-upload-dev.yml + secrets: inherit + needs: [manual-build] + if: always() + + clean: + uses: ./.github/workflows/clean-deployments.yml + # secrets: inherit + needs: [aws-upload] + if: always() + + # Remove artifacts from github actions + remove-artifacts: + name: Remove artifacts + needs: [aws-upload] + if: always() + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Remove all artifacts + uses: ./.github/actions/remove-artifacts + + diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000000..f708d3dbde --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,44 @@ +name: Nightly jobs +on: + schedule: + - cron: 0 0 * * * + +jobs: + # Integration tests + build-docker: + uses: ./.github/workflows/pipeline-build-docker.yml + secrets: inherit + + integration-tests-nightly: + needs: build-docker + uses: ./.github/workflows/tests-integration.yml + secrets: inherit + with: + build: 'docker' + report: true + short_rte_list: false + + e2e-docker-nightly: + uses: ./.github/workflows/tests-e2e-docker.yml + needs: build-docker + secrets: inherit + with: + report: true + + # E2E tests + build-appimage: + uses: ./.github/workflows/pipeline-build-linux.yml + secrets: inherit + with: + target: 'linux:appimage:x64' + + e2e-appimage-nightly: + uses: ./.github/workflows/tests-e2e-appimage.yml + needs: build-appimage + secrets: inherit + with: + report: true + + clean-dev-s3: + uses: ./.github/workflows/clean-s3-dev-builds.yml + secrets: inherit diff --git a/.github/workflows/pipeline-build-docker.yml b/.github/workflows/pipeline-build-docker.yml index f27c42bc8d..d40e948b29 100644 --- a/.github/workflows/pipeline-build-docker.yml +++ b/.github/workflows/pipeline-build-docker.yml @@ -4,10 +4,19 @@ on: inputs: environment: description: Environment for build - required: false default: 'staging' type: string + for_e2e_tests: + description: Build for e2e docker tests + default: false + type: boolean + + debug: + description: SSH Debug + default: false + type: boolean + jobs: build: name: Build docker @@ -16,6 +25,13 @@ jobs: steps: - uses: actions/checkout@v4 + # SSH Debug + - name: Enable SSH + uses: mxschmitt/action-tmate@v3 + if: inputs.debug + with: + detached: true + - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -26,32 +42,39 @@ jobs: sqlite3-host-mirror: ${{ secrets.NPM_CONFIG_NODE_SQLITE3_BINARY_HOST_MIRROR }} - name: Build sources - run: ./.circleci/build/build.sh + run: ./.github/build/build.sh + + # todo: matrix + - name: Build web archives for e2e tests + if: inputs.for_e2e_tests + run: | + unset npm_config_keytar_binary_host_mirror + unset npm_config_node_sqlite3_binary_host_mirror + # Docker sources + PLATFORM=linux ARCH=x64 LIBC=musl .github/build/build_modules.sh - # todo: matrix - name: Build web archives + if: ${{ !inputs.for_e2e_tests }} run: | unset npm_config_keytar_binary_host_mirror unset npm_config_node_sqlite3_binary_host_mirror # Docker sources - PLATFORM=linux ARCH=x64 LIBC=musl .circleci/build/build_modules.sh - PLATFORM=linux ARCH=arm64 LIBC=musl .circleci/build/build_modules.sh + PLATFORM=linux ARCH=x64 LIBC=musl .github/build/build_modules.sh + PLATFORM=linux ARCH=arm64 LIBC=musl .github/build/build_modules.sh # Redis Stack + VSC Linux - PLATFORM=linux ARCH=x64 .circleci/build/build_modules.sh - PLATFORM=linux ARCH=arm64 .circleci/build/build_modules.sh + PLATFORM=linux ARCH=x64 .github/build/build_modules.sh + PLATFORM=linux ARCH=arm64 .github/build/build_modules.sh # VSC Darwin - PLATFORM=darwin ARCH=x64 .circleci/build/build_modules.sh - PLATFORM=darwin ARCH=arm64 .circleci/build/build_modules.sh + PLATFORM=darwin ARCH=x64 .github/build/build_modules.sh + PLATFORM=darwin ARCH=arm64 .github/build/build_modules.sh # VSC Windows - PLATFORM=win32 ARCH=x64 .circleci/build/build_modules.sh - - name: Build Docker (x64, arm64) - env: - ENV: ${{ vars.ENV }} - RI_SEGMENT_WRITE_KEY: ${{ secrets.RI_SEGMENT_WRITE_KEY }} + PLATFORM=win32 ARCH=x64 .github/build/build_modules.sh + + - name: Build Docker (x64) run: | # Build alpine x64 image docker buildx build \ - -f .circleci/build/build.Dockerfile \ + -f .github/build/build.Dockerfile \ --platform linux/amd64 \ --build-arg DIST=release/web/Redis-Insight-web-linux-musl.x64.tar.gz \ --build-arg NODE_ENV="$ENV" \ @@ -59,9 +82,15 @@ jobs: -t redisinsight:amd64 \ . + mkdir -p release/docker + docker image save -o release/docker/docker-linux-alpine.amd64.tar redisinsight:amd64 + + - name: Build Docker (arm64) + if: ${{ !inputs.for_e2e_tests }} + run: | # Build alpine arm64 image docker buildx build \ - -f .circleci/build/build.Dockerfile \ + -f .github/build/build.Dockerfile \ --platform linux/arm64 \ --build-arg DIST=release/web/Redis-Insight-web-linux-musl.arm64.tar.gz \ --build-arg NODE_ENV="$ENV" \ @@ -70,31 +99,20 @@ jobs: . mkdir -p release/docker - docker image save -o release/docker/docker-linux-alpine.amd64.tar redisinsight:amd64 docker image save -o release/docker/docker-linux-alpine.arm64.tar redisinsight:arm64 - uses: actions/upload-artifact@v4 - name: Upload docker images - with: - if-no-files-found: error - name: docker - path: ./release/docker/docker-linux-alpine.*64.tar - - - uses: actions/upload-artifact@v4 - name: Upload web-mini artifacts - with: - if-no-files-found: error - name: web-mini - path: ./release/web-mini - - - uses: actions/upload-artifact@v4 - name: Upload web artifacts + name: Upload docker builds with: - if-no-files-found: error - name: web - path: ./release/web + if-no-files-found: warn + name: docker-builds + path: | + ./release/docker + ./release/web + ./release/web-mini env: + ENV: ${{ vars.ENV }} RI_AI_CONVAI_TOKEN: ${{ secrets.RI_AI_CONVAI_TOKEN }} RI_AI_QUERY_PASS: ${{ secrets.RI_AI_QUERY_PASS }} RI_AI_QUERY_USER: ${{ secrets.RI_AI_QUERY_USER }} diff --git a/.github/workflows/pipeline-build-linux.yml b/.github/workflows/pipeline-build-linux.yml index d7fe413497..554aab00d4 100644 --- a/.github/workflows/pipeline-build-linux.yml +++ b/.github/workflows/pipeline-build-linux.yml @@ -15,6 +15,11 @@ on: default: 'all' type: string + debug: + description: SSH Debug + default: false + type: boolean + jobs: build: name: Build linux @@ -22,20 +27,15 @@ jobs: environment: ${{ inputs.environment }} steps: - #TODO: some debug tools - # - uses: crazy-max/ghaction-dump-context@v2 - # - uses: hmarr/debug-action@v3 - # ssh - # - run: sudo apt-get update -qy && sudo apt-get install -qy tmux; - # - name: Setup upterm session - # - uses: mxschmitt/action-tmate #1 better - # uses: lhotari/action-upterm@v1 #2 - # with: - # limit-access-to-actor: true - # limit-access-to-users: zalenskiSofteq - - uses: actions/checkout@v4 + # SSH Debug + - name: Enable SSH + uses: mxschmitt/action-tmate@v3 + if: inputs.debug + with: + detached: true + - name: Install all libs and dependencies uses: ./.github/actions/install-all-build-libs with: @@ -66,57 +66,41 @@ jobs: if: (vars.ENV == 'staging' || vars.ENV == 'development') && inputs.target == vars.ALL run: yarn package:stage - - name: Build linux packages (development) + - name: Build linux packages (custom) if: inputs.target != vars.ALL run: | - target="" - if [ ${{ startsWith(inputs.target, 'linux:') }} == 'true' ]; then - inputsTarget=${{inputs.target}} - target=${inputsTarget#linux:} - fi + target=$(echo "${{inputs.target}}" | grep -oE 'build_linux_[^_ ]+' | sed 's/build_linux_//' | sort -u | paste -sd ' ' -) - yarn package:stage --linux $target + if [ "${{ vars.ENV == 'production' }}" == "true" ]; then + yarn package:prod --linux $target + else + yarn package:stage --linux $target + fi - uses: actions/upload-artifact@v4 - name: Upload AppImage artifact + name: Upload linux builds with: - name: linux-appimage-build + name: linux-builds path: | + ./release/latest-linux.yml ./release/Redis-Insight*.AppImage - - - uses: actions/upload-artifact@v4 - name: Upload Deb artifact - with: - name: linux-deb-build - path: | ./release/Redis-Insight*.deb - - - uses: actions/upload-artifact@v4 - name: Upload rpm artifacts - with: - name: linux-rpm-build - path: | ./release/Redis-Insight*.rpm - - - uses: actions/upload-artifact@v4 - name: Upload snap artifact - with: - name: linux-snap-builds - path: | ./release/Redis-Insight*.snap - ./release/latest-linux.yml env: RI_AI_CONVAI_TOKEN: ${{ secrets.RI_AI_CONVAI_TOKEN }} RI_AI_QUERY_PASS: ${{ secrets.RI_AI_QUERY_PASS }} RI_AI_QUERY_USER: ${{ secrets.RI_AI_QUERY_USER }} RI_CLOUD_API_URL: ${{ secrets.RI_CLOUD_API_URL }} + RI_CLOUD_API_TOKEN: ${{ secrets.RI_CLOUD_API_TOKEN }} RI_CLOUD_CAPI_URL: ${{ secrets.RI_CLOUD_CAPI_URL }} RI_CLOUD_IDP_AUTHORIZE_URL: ${{ secrets.RI_CLOUD_IDP_AUTHORIZE_URL }} RI_CLOUD_IDP_CLIENT_ID: ${{ secrets.RI_CLOUD_IDP_CLIENT_ID }} RI_CLOUD_IDP_GH_ID: ${{ secrets.RI_CLOUD_IDP_GH_ID }} RI_CLOUD_IDP_GOOGLE_ID: ${{ secrets.RI_CLOUD_IDP_GOOGLE_ID }} RI_CLOUD_IDP_ISSUER: ${{ secrets.RI_CLOUD_IDP_ISSUER }} + RI_CLOUD_IDP_REVOKE_TOKEN_URL: ${{ secrets.RI_CLOUD_IDP_REVOKE_TOKEN_URL }} RI_CLOUD_IDP_REDIRECT_URI: ${{ secrets.RI_CLOUD_IDP_REDIRECT_URI }} RI_CLOUD_IDP_TOKEN_URL: ${{ secrets.RI_CLOUD_IDP_TOKEN_URL }} RI_SEGMENT_WRITE_KEY: ${{ secrets.RI_SEGMENT_WRITE_KEY }} diff --git a/.github/workflows/pipeline-build-macos.yml b/.github/workflows/pipeline-build-macos.yml index 05c1642dc5..eafc375ed4 100644 --- a/.github/workflows/pipeline-build-macos.yml +++ b/.github/workflows/pipeline-build-macos.yml @@ -14,6 +14,11 @@ on: default: 'all' type: string + debug: + description: SSH Debug + default: false + type: boolean + jobs: build: name: Build macos @@ -22,6 +27,13 @@ jobs: steps: - uses: actions/checkout@v4 + # SSH Debug + - name: Enable SSH + uses: mxschmitt/action-tmate@v3 + if: inputs.debug + with: + detached: true + - name: Add certificates to the keychain uses: ./.github/actions/install-apple-certs with: @@ -56,70 +68,48 @@ jobs: run: | unset CSC_LINK - echo "$USE_HARD_LINKS" echo $APP_BUNDLE_VERSION echo $CSC_KEYCHAIN yarn package:stage && yarn package:mas rm -rf release/mac + mv release/mas-universal/Redis-Insight-mac-universal-mas.pkg release/Redis-Insight-mac-universal-mas.pkg # handle manual builds - - name: Build macos dmg (dev) + - name: Build macos dmg (custom) if: inputs.target != vars.ALL run: | unset CSC_LINK - yarn package:stage --mac ${{ inputs.target }} + target=$(echo "${{inputs.target}}" | grep -oE 'build_macos_[^ ]+' | sed 's/build_macos_/dmg:/' | paste -sd ' ' -) + + if [ "${{ vars.ENV == 'production' }}" == "true" ]; then + yarn package:prod --mac $target + else + yarn package:stage --mac $target + fi + rm -rf release/mac - name: Repack dmg to tar - if: vars.ENV == 'production' + if: vars.ENV == 'production' && inputs.target == vars.ALL run: | - ARCH=x64 ./.circleci/redisstack/dmg.repack.sh - ARCH=arm64 ./.circleci/redisstack/dmg.repack.sh + ARCH=x64 ./.github/redisstack/dmg.repack.sh + ARCH=arm64 ./.github/redisstack/dmg.repack.sh - - name: Upload x64 packages + - name: Upload macos packages uses: actions/upload-artifact@v4 - if: inputs.target == vars.ALL || endsWith(inputs.target, 'x64') with: - name: macos-x64-builds + name: macos-builds path: | ./release/Redis-Insight*x64.dmg ./release/Redis-Insight*x64.dmg.blockmap - - - name: Upload zips packages - uses: actions/upload-artifact@v4 - if: inputs.target == vars.ALL || endsWith(inputs.target, 'x64') - with: - name: macos-zip-builds - path: | ./release/Redis-Insight*.zip - - - name: Upload ARM packages - uses: actions/upload-artifact@v4 - if: inputs.target == vars.ALL || endsWith(inputs.target, 'arm64') - with: - name: macos-arm-builds - path: | ./release/Redis-Insight*arm64.dmg ./release/Redis-Insight*arm64.dmg.blockmap - - - name: Upload MAS packages - uses: actions/upload-artifact@v4 - if: inputs.target == vars.ALL - with: - name: macos-mas-builds - path: | ./release/Redis-Insight*.pkg ./release/*-mac.yml - - - name: Upload redis stack packages - uses: actions/upload-artifact@v4 - if: vars.ENV == 'production' - with: - name: 'redisstack' - path: | - ./release/redisstack/Redis-Insight-app-darwin.*.tar.gz + ./release/redisstack env: APPLE_ID: ${{ secrets.APPLE_ID }} @@ -133,10 +123,12 @@ jobs: RI_AI_CONVAI_TOKEN: ${{ secrets.RI_AI_CONVAI_TOKEN }} RI_AI_QUERY_PASS: ${{ secrets.RI_AI_QUERY_PASS }} RI_AI_QUERY_USER: ${{ secrets.RI_AI_QUERY_USER }} + RI_CLOUD_API_TOKEN: ${{ secrets.RI_CLOUD_API_TOKEN }} RI_CLOUD_API_URL: ${{ secrets.RI_CLOUD_API_URL }} RI_CLOUD_CAPI_URL: ${{ secrets.RI_CLOUD_CAPI_URL }} RI_CLOUD_IDP_AUTHORIZE_URL: ${{ secrets.RI_CLOUD_IDP_AUTHORIZE_URL }} RI_CLOUD_IDP_CLIENT_ID: ${{ secrets.RI_CLOUD_IDP_CLIENT_ID }} + RI_CLOUD_IDP_REVOKE_TOKEN_URL: ${{ secrets.RI_CLOUD_IDP_REVOKE_TOKEN_URL }} RI_CLOUD_IDP_GH_ID: ${{ secrets.RI_CLOUD_IDP_GH_ID }} RI_CLOUD_IDP_GOOGLE_ID: ${{ secrets.RI_CLOUD_IDP_GOOGLE_ID }} RI_CLOUD_IDP_ISSUER: ${{ secrets.RI_CLOUD_IDP_ISSUER }} diff --git a/.github/workflows/pipeline-build-windows.yml b/.github/workflows/pipeline-build-windows.yml index 506ab530e7..e708e1dc15 100644 --- a/.github/workflows/pipeline-build-windows.yml +++ b/.github/workflows/pipeline-build-windows.yml @@ -4,10 +4,14 @@ on: inputs: environment: description: Environment for build - required: false default: 'staging' type: string + debug: + description: SSH Debug + default: false + type: boolean + jobs: build: name: Build windows @@ -17,6 +21,13 @@ jobs: steps: - uses: actions/checkout@v4 + # SSH Debug + - name: Enable SSH + uses: mxschmitt/action-tmate@v3 + if: inputs.debug + with: + detached: true + - name: Install all libs and dependencies uses: ./.github/actions/install-all-build-libs @@ -58,12 +69,14 @@ jobs: RI_AI_QUERY_PASS: ${{ secrets.RI_AI_QUERY_PASS }} RI_AI_QUERY_USER: ${{ secrets.RI_AI_QUERY_USER }} RI_CLOUD_API_URL: ${{ secrets.RI_CLOUD_API_URL }} + RI_CLOUD_API_TOKEN: ${{ secrets.RI_CLOUD_API_TOKEN }} RI_CLOUD_CAPI_URL: ${{ secrets.RI_CLOUD_CAPI_URL }} RI_CLOUD_IDP_AUTHORIZE_URL: ${{ secrets.RI_CLOUD_IDP_AUTHORIZE_URL }} RI_CLOUD_IDP_CLIENT_ID: ${{ secrets.RI_CLOUD_IDP_CLIENT_ID }} RI_CLOUD_IDP_GH_ID: ${{ secrets.RI_CLOUD_IDP_GH_ID }} RI_CLOUD_IDP_GOOGLE_ID: ${{ secrets.RI_CLOUD_IDP_GOOGLE_ID }} RI_CLOUD_IDP_ISSUER: ${{ secrets.RI_CLOUD_IDP_ISSUER }} + RI_CLOUD_IDP_REVOKE_TOKEN_URL: ${{ secrets.RI_CLOUD_IDP_REVOKE_TOKEN_URL }} RI_CLOUD_IDP_REDIRECT_URI: ${{ secrets.RI_CLOUD_IDP_REDIRECT_URI }} RI_CLOUD_IDP_TOKEN_URL: ${{ secrets.RI_CLOUD_IDP_TOKEN_URL }} RI_SEGMENT_WRITE_KEY: ${{ secrets.RI_SEGMENT_WRITE_KEY }} diff --git a/.github/workflows/publish-stores.yml b/.github/workflows/publish-stores.yml new file mode 100644 index 0000000000..78665a830c --- /dev/null +++ b/.github/workflows/publish-stores.yml @@ -0,0 +1,70 @@ +name: Publish to stores + +on: + workflow_call: + +env: + AWS_BUCKET_NAME: ${{ secrets.AWS_BUCKET_NAME }} + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + +jobs: + docker: + runs-on: ubuntu-latest + name: Publish to Dockerhub + steps: + - uses: actions/checkout@v4 + + - name: Download Docker images + run: | + mkdir release + aws s3 cp s3://${AWS_BUCKET_NAME}/public/latest/docker ./release/docker --recursive + + - name: Publish docker + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASS: ${{ secrets.DOCKER_PASS }} + DOCKER_REPO: ${{ secrets.DOCKER_REPO }} + DOCKER_V1_USER: ${{ secrets.DOCKER_V1_USER }} + DOCKER_V1_REPO: ${{ secrets.DOCKER_V1_REPO }} + run: | + appVersion=$(jq -r '.version' redisinsight/package.json) + + docker login -u $DOCKER_USER -p $DOCKER_PASS + + ./.github/build/release-docker.sh \ + -d redisinsight \ + -r $DOCKER_REPO \ + -v $appVersion + + docker login -u $DOCKER_V1_USER -p $DOCKER_V1_PASS + + ./.github/build/release-docker.sh \ + -d redisinsight \ + -r $DOCKER_V1_REPO \ + -v $appVersion + + snapcraft: + runs-on: ubuntu-latest + name: Publish to Snapcraft + env: + SNAPCRAFT_FILE_NAME: 'Redis-Insight-linux-amd64.snap' + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + steps: + - uses: actions/checkout@v4 + + - name: Download Snapcraft package + id: snap + run: | + mkdir release + aws s3 cp s3://${AWS_BUCKET_NAME}/public/latest/${SNAPCRAFT_FILE_NAME} ./release + echo "snap-path=$(readlink -e ./release/${SNAPCRAFT_FILE_NAME})" >> "$GITHUB_OUTPUT" + + - uses: snapcore/action-publish@v1 + name: Publish Snapcraft + with: + snap: ${{ steps.snap.outputs.snap-path }} + release: stable + diff --git a/.github/workflows/pull-request-created.yml b/.github/workflows/pull-request-created.yml deleted file mode 100644 index 3d479b82d1..0000000000 --- a/.github/workflows/pull-request-created.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Pull request created - -on: - pull_request_target: - -permissions: - pull-requests: write - -jobs: - assign-author: - runs-on: ubuntu-latest - name: Assign author pr - steps: - - uses: toshimaru/auto-author-assign@v2.1.1 diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml deleted file mode 100644 index 7fc8587c66..0000000000 --- a/.github/workflows/release-docker.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Release docker images - -on: - workflow_call: - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Download Docker artifacts - uses: actions/download-artifact@v4 - with: - name: docker - path: ./release - - - name: Publish docker - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASS: ${{ secrets.DOCKER_PASS }} - DOCKER_REPO: ${{ secrets.DOCKER_REPO }} - DOCKER_V1_USER: ${{ secrets.DOCKER_V1_USER }} - DOCKER_V1_REPO: ${{ secrets.DOCKER_V1_REPO }} - run: | - appVersion=$(jq -r '.version' redisinsight/package.json) - - docker login -u $DOCKER_USER -p $DOCKER_PASS - - ./.github/build/release-docker.sh \ - -d redisinsight \ - -r $DOCKER_REPO \ - -v $appVersion - - docker login -u $DOCKER_V1_USER -p $DOCKER_V1_PASS - - ./.github/build/release-docker.sh \ - -d redisinsight \ - -r $DOCKER_V1_REPO \ - -v $appVersion - diff --git a/.github/workflows/release-prod.yml b/.github/workflows/release-prod.yml index bb24782ff6..00a930b693 100644 --- a/.github/workflows/release-prod.yml +++ b/.github/workflows/release-prod.yml @@ -1,4 +1,4 @@ -name: Release (prod) +name: ❗ Release (prod) on: push: @@ -11,7 +11,9 @@ jobs: uses: ./.github/workflows/tests.yml secrets: inherit with: - all_tests: true + group_tests: 'without_e2e' + short_rte_list: false + pre_release: true builds-prod: name: Create all builds for release @@ -22,6 +24,18 @@ jobs: environment: 'production' target: 'all' + e2e-docker-tests: + name: E2E Docker tests + needs: builds-prod + uses: ./.github/workflows/tests-e2e-docker.yml + secrets: inherit + + e2e-appimage-tests: + name: E2E AppImage tests + needs: builds-prod + uses: ./.github/workflows/tests-e2e-appimage.yml + secrets: inherit + virustotal-prod: name: Virustotal uses: ./.github/workflows/virustotal.yml @@ -30,14 +44,26 @@ jobs: with: skip_report: true - aws-prod: + aws-upload-prod: name: Realse to AWS S3 - uses: ./.github/workflows/aws.yml + uses: ./.github/workflows/aws-upload-prod.yml needs: virustotal-prod secrets: inherit - docker-prod: - name: Release docker images - uses: ./.github/workflows/release-docker.yml - needs: aws-prod + publish-stores: + name: Publish to stores + uses: ./.github/workflows/publish-stores.yml + needs: aws-upload-prod secrets: inherit + + # Remove artifacts from github actions + remove-artifacts: + name: Remove artifacts + needs: [aws-upload-prod, e2e-docker-tests, e2e-appimage-tests] + if: always() + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Remove all artifacts + uses: ./.github/actions/remove-artifacts diff --git a/.github/workflows/release-stage.yml b/.github/workflows/release-stage.yml index d163c5e192..38112438ff 100644 --- a/.github/workflows/release-stage.yml +++ b/.github/workflows/release-stage.yml @@ -1,4 +1,4 @@ -name: Release (stage) +name: 📖 Release (stage) on: push: @@ -11,7 +11,9 @@ jobs: uses: ./.github/workflows/tests.yml secrets: inherit with: - all_tests: true + group_tests: 'without_e2e' + short_rte_list: false + pre_release: true builds: name: Release stage builds @@ -22,17 +24,32 @@ jobs: environment: 'staging' target: 'all' + aws: + uses: ./.github/workflows/aws-upload-dev.yml + needs: [builds] + secrets: inherit + if: always() + with: + pre-release: true - # todo: e2e tests for release stage - # e2e-linux: - # uses: ./.github/workflows/build.yml - # needs: builds - # secrets: inherit - # with: - # environment: 'staging' - # target: 'all' - - - + e2e-docker-tests: + needs: builds + uses: ./.github/workflows/tests-e2e-docker.yml + secrets: inherit + e2e-appimage-tests: + needs: builds + uses: ./.github/workflows/tests-e2e-appimage.yml + secrets: inherit + # Remove artifacts from github actions + remove-artifacts: + name: Remove artifacts + needs: [aws, e2e-docker-tests, e2e-appimage-tests] + if: always() + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Remove all artifacts + uses: ./.github/actions/remove-artifacts # Remove artifacts from github actions diff --git a/.github/workflows/tests-backend.yml b/.github/workflows/tests-backend.yml index ddd7a41005..027c38afef 100644 --- a/.github/workflows/tests-backend.yml +++ b/.github/workflows/tests-backend.yml @@ -9,11 +9,13 @@ on: required: false env: + SLACK_AUDIT_REPORT_CHANNEL: ${{ secrets.SLACK_AUDIT_REPORT_CHANNEL }} SLACK_AUDIT_REPORT_KEY: ${{ secrets.SLACK_AUDIT_REPORT_KEY }} + REPORT_NAME: "report-be" jobs: unit-tests: - name: Backend tests + name: Unit tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -28,7 +30,7 @@ jobs: FILENAME=api.prod.deps.audit.json yarn --cwd redisinsight/api audit --groups dependencies --json > $FILENAME || true && - FILENAME=$FILENAME DEPS="API prod" node .circleci/deps-audit-report.js && + FILENAME=$FILENAME DEPS="API prod" node .github/deps-audit-report.js && curl -H "Content-type: application/json" --data @slack.$FILENAME -H "Authorization: Bearer $SLACK_AUDIT_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage - name: API DEV dependencies scan @@ -36,7 +38,7 @@ jobs: FILENAME=api.dev.deps.audit.json yarn --cwd redisinsight/api audit --groups devDependencies --json > $FILENAME || true && - FILENAME=$FILENAME DEPS="API dev" node .circleci/deps-audit-report.js && + FILENAME=$FILENAME DEPS="API dev" node .github/deps-audit-report.js && curl -H "Content-type: application/json" --data @slack.$FILENAME -H "Authorization: Bearer $SLACK_AUDIT_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage - name: Code analysis @@ -45,8 +47,37 @@ jobs: WORKDIR="./redisinsight/api" yarn lint:api -f json -o $FILENAME || true && - FILENAME=$FILENAME WORKDIR=$WORKDIR TARGET="API" node .circleci/lint-report.js && + FILENAME=$FILENAME WORKDIR=$WORKDIR TARGET="API" node .github/lint-report.js && curl -H "Content-type: application/json" --data @$WORKDIR/slack.$FILENAME -H "Authorization: Bearer $SLACK_AUDIT_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage - name: Unit tests API - run: yarn --cwd redisinsight/api/ test:cov --ci + run: yarn --cwd redisinsight/api/ test:cov --ci --silent + + - name: Upload Test Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ env.REPORT_NAME }} + path: redisinsight/api/report + + - name: Deploy report + uses: ./.github/actions/deploy-test-reports + if: always() + with: + group: 'report' + AWS_BUCKET_NAME_TEST: ${{ vars.AWS_BUCKET_NAME_TEST }} + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Get current date + id: date + if: always() + uses: ./.github/actions/get-current-date + + - name: Add link to report in the workflow summary + if: always() + run: | + link="${{ vars.DEFAULT_TEST_REPORTS_URL }}/${{ steps.date.outputs.date }}/${{ github.run_id }}/${{ env.REPORT_NAME }}/index.html" + echo "[${link}](${link})" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/tests-e2e-appimage.yml b/.github/workflows/tests-e2e-appimage.yml new file mode 100644 index 0000000000..d3c9f7ea3d --- /dev/null +++ b/.github/workflows/tests-e2e-appimage.yml @@ -0,0 +1,128 @@ +name: Tests E2E AppImage +on: + workflow_call: + inputs: + report: + description: Send report to Slack + required: false + default: false + type: boolean + + debug: + description: Send report to Slack + required: false + default: false + type: boolean + +env: + E2E_CLOUD_DATABASE_USERNAME: ${{ secrets.E2E_CLOUD_DATABASE_USERNAME }} + E2E_CLOUD_DATABASE_PASSWORD: ${{ secrets.E2E_CLOUD_DATABASE_PASSWORD }} + E2E_CLOUD_API_ACCESS_KEY: ${{ secrets.E2E_CLOUD_API_ACCESS_KEY }} + E2E_CLOUD_DATABASE_HOST: ${{ secrets.E2E_CLOUD_DATABASE_HOST }} + E2E_CLOUD_DATABASE_PORT: ${{ secrets.E2E_CLOUD_DATABASE_PORT }} + E2E_CLOUD_DATABASE_NAME: ${{ secrets.E2E_CLOUD_DATABASE_NAME }} + E2E_CLOUD_API_SECRET_KEY: ${{ secrets.E2E_CLOUD_API_SECRET_KEY }} + E2E_RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }} + RI_ENCRYPTION_KEY: ${{ secrets.RI_ENCRYPTION_KEY }} + RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }} + RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }} + SLACK_TEST_REPORT_KEY: ${{ secrets.SLACK_TEST_REPORT_KEY }} + TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }} + DBUS_SESSION_BUS_ADDRESS: ${{ vars.DBUS_SESSION_BUS_ADDRESS }} + DISPLAY: ${{ vars.DISPLAY }} + APPIMAGE_PATH: ${{ vars.APPIMAGE_PATH }} + REPORT_NAME: "report-appimage" + +jobs: + e2e-tests-appimage: + runs-on: ubuntu-latest + name: E2E AppImage tests + environment: + name: production + steps: + - uses: actions/checkout@v4 + + # SSH Debug + - name: Enable SSH + uses: mxschmitt/action-tmate@v3 + if: inputs.debug + with: + detached: true + + - name: Install necessary packages + run: | + sudo apt-get update -y + sudo apt-get install kmod libfuse2 xvfb net-tools xdotool desktop-file-utils fluxbox netcat -y + + - name: Install Google Chrome + run: | + wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' + sudo apt-get update + sudo apt-get install -y google-chrome-stable libnss3 libgconf-2-4 libxss1 libasound2 + xdg-settings set default-web-browser google-chrome.desktop + + - name: Download AppImage Artifacts + uses: actions/download-artifact@v4 + with: + name: linux-builds + path: ./release + + - name: Start Xvfb + run: | + if [ -f /tmp/.X99-lock ]; then rm /tmp/.X99-lock; fi + Xvfb :99 -ac -screen 0 1920x1080x24 & + sleep 3 + fluxbox & + + - name: Run tests + run: | + .github/e2e/test.app-image.sh + + - name: Upload Test Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ env.REPORT_NAME }} + path: tests/e2e/report + + - name: Send report to Slack + if: inputs.report && always() + run: | + APP_BUILD_TYPE="Electron (Linux)" node ./.github/e2e-results.js + 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 + + - name: Generate test results + uses: dorny/test-reporter@v1 + if: always() + with: + name: 'Test results: E2E (AppImage)' + path: tests/e2e/results/results.xml + reporter: java-junit + list-tests: 'failed' + list-suites: 'failed' + fail-on-error: 'false' + + # Deploy report to AWS test bucket + - name: Deploy report + uses: ./.github/actions/deploy-test-reports + if: always() + with: + group: 'report' + AWS_BUCKET_NAME_TEST: ${{ vars.AWS_BUCKET_NAME_TEST }} + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Get current date + id: date + if: always() + uses: ./.github/actions/get-current-date + + - name: Add link to report in the workflow summary + if: always() + run: | + link="${{ vars.DEFAULT_TEST_REPORTS_URL }}/${{ steps.date.outputs.date }}/${{ github.run_id }}/${{ env.REPORT_NAME }}/index.html" + echo "[${link}](${link})" >> $GITHUB_STEP_SUMMARY + diff --git a/.github/workflows/tests-e2e-docker.yml b/.github/workflows/tests-e2e-docker.yml new file mode 100644 index 0000000000..21a514b39f --- /dev/null +++ b/.github/workflows/tests-e2e-docker.yml @@ -0,0 +1,129 @@ +name: Tests E2E Docker +on: + workflow_call: + inputs: + report: + description: Send report to Slack + default: false + type: boolean + + debug: + description: SSH Debug + default: false + type: boolean + +env: + E2E_CLOUD_DATABASE_USERNAME: ${{ secrets.E2E_CLOUD_DATABASE_USERNAME }} + E2E_CLOUD_DATABASE_PASSWORD: ${{ secrets.E2E_CLOUD_DATABASE_PASSWORD }} + E2E_CLOUD_API_ACCESS_KEY: ${{ secrets.E2E_CLOUD_API_ACCESS_KEY }} + E2E_CLOUD_DATABASE_HOST: ${{ secrets.E2E_CLOUD_DATABASE_HOST }} + E2E_CLOUD_DATABASE_PORT: ${{ secrets.E2E_CLOUD_DATABASE_PORT }} + E2E_CLOUD_DATABASE_NAME: ${{ secrets.E2E_CLOUD_DATABASE_NAME }} + E2E_CLOUD_API_SECRET_KEY: ${{ secrets.E2E_CLOUD_API_SECRET_KEY }} + E2E_RI_ENCRYPTION_KEY: ${{ secrets.E2E_RI_ENCRYPTION_KEY }} + RI_ENCRYPTION_KEY: ${{ secrets.RI_ENCRYPTION_KEY }} + RI_SERVER_TLS_CERT: ${{ secrets.RI_SERVER_TLS_CERT }} + RI_SERVER_TLS_KEY: ${{ secrets.RI_SERVER_TLS_KEY }} + SLACK_TEST_REPORT_KEY: ${{ secrets.SLACK_TEST_REPORT_KEY }} + TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }} + E2E_VOLUME_PATH: "/usr/src/app" + REPORT_NAME: "report-docker-node" + +jobs: + e2e-docker-tests: + runs-on: ubuntu-latest + name: E2E Docker tests + strategy: + fail-fast: false + matrix: + # Number of threads to run tests + parallel: [0, 1, 2, 3] + + steps: + - uses: actions/checkout@v4 + + # SSH Debug + - name: Enable SSH + uses: mxschmitt/action-tmate@v3 + if: inputs.debug + with: + detached: true + + - name: Download Docker Artifacts + uses: actions/download-artifact@v4 + with: + name: docker-builds + path: ./release + + - name: Load built docker image from workspace + run: | + docker image load -i ./release/docker/docker-linux-alpine.amd64.tar + + - name: Generate short list of the test files + working-directory: ./tests/e2e + run: | + testFiles=$(find tests/web -type f -name '*.e2e.ts' | sort | awk "NR % 4 == ${{ matrix.parallel }}") + + echo $testFiles + + # Multi-Line value + echo "TEST_FILES<> $GITHUB_ENV + echo "$testFiles" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + - name: Run tests + run: | + export NODE_INDEX=${{ matrix.parallel }} + + TEST_BIG_DB_DUMP=$TEST_BIG_DB_DUMP \ + RI_SERVER_TLS_CERT="$RI_SERVER_TLS_CERT" \ + RI_SERVER_TLS_KEY="$RI_SERVER_TLS_KEY" \ + docker compose \ + -f tests/e2e/rte.docker-compose.yml \ + -f tests/e2e/docker.web.docker-compose.yml \ + up --abort-on-container-exit --force-recreate + + - name: Upload Test Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: report-docker-node-${{ matrix.parallel }} + path: /usr/src/app/report + + - name: Send report to Slack + if: inputs.report && always() + run: | + node ./.github/e2e-results.js + 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 + + - name: Generate test results for ${{ matrix.parallel }}th node + uses: dorny/test-reporter@v1 + if: always() + with: + name: 'Test results: E2E (docker) ${{ matrix.parallel }}th node' + path: /usr/src/app/results/results.xml + reporter: java-junit + list-tests: 'failed' + list-suites: 'failed' + fail-on-error: 'false' + + - name: Deploy report + if: always() + uses: ./.github/actions/deploy-test-reports + with: + group: 'report' + AWS_BUCKET_NAME_TEST: ${{ vars.AWS_BUCKET_NAME_TEST }} + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Add link to report in the workflow summary + if: always() + run: | + DATE=$(date +'%Y-%m-%d') + link="${{ vars.DEFAULT_TEST_REPORTS_URL }}/${DATE}/${{ github.run_id }}/${{ env.REPORT_NAME }}-${{ matrix.parallel }}/index.html" + + echo "- [${link}](${link})" >> $GITHUB_STEP_SUMMARY + + diff --git a/.github/workflows/tests-frontend.yml b/.github/workflows/tests-frontend.yml index c8969414d4..2080bacdc8 100644 --- a/.github/workflows/tests-frontend.yml +++ b/.github/workflows/tests-frontend.yml @@ -3,7 +3,9 @@ on: workflow_call: env: + SLACK_AUDIT_REPORT_CHANNEL: ${{ secrets.SLACK_AUDIT_REPORT_CHANNEL }} SLACK_AUDIT_REPORT_KEY: ${{ secrets.SLACK_AUDIT_REPORT_KEY }} + REPORT_NAME: "report-fe" jobs: unit-tests: @@ -22,7 +24,7 @@ jobs: FILENAME=ui.prod.deps.audit.json yarn audit --groups dependencies --json > $FILENAME || true && - FILENAME=$FILENAME DEPS="UI prod" node .circleci/deps-audit-report.js && + FILENAME=$FILENAME DEPS="UI prod" node .github/deps-audit-report.js && curl -H "Content-type: application/json" --data @slack.$FILENAME -H "Authorization: Bearer $SLACK_AUDIT_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage - name: UI DEV dependencies audit @@ -30,7 +32,7 @@ jobs: FILENAME=ui.dev.deps.audit.json yarn audit --groups devDependencies --json > $FILENAME || true && - FILENAME=$FILENAME DEPS="UI dev" node .circleci/deps-audit-report.js && + FILENAME=$FILENAME DEPS="UI dev" node .github/deps-audit-report.js && curl -H "Content-type: application/json" --data @slack.$FILENAME -H "Authorization: Bearer $SLACK_AUDIT_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage - name: Code analysis @@ -39,12 +41,41 @@ jobs: WORKDIR="." yarn lint:ui -f json -o $FILENAME || true && - FILENAME=$FILENAME WORKDIR=$WORKDIR TARGET="UI" node .circleci/lint-report.js && + FILENAME=$FILENAME WORKDIR=$WORKDIR TARGET="UI" node .github/lint-report.js && curl -H "Content-type: application/json" --data @$WORKDIR/slack.$FILENAME -H "Authorization: Bearer $SLACK_AUDIT_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage yarn lint -f json -o $FILENAME || true && - FILENAME=$FILENAME WORKDIR=$WORKDIR TARGET="REST" node .circleci/lint-report.js && + FILENAME=$FILENAME WORKDIR=$WORKDIR TARGET="REST" node .github/lint-report.js && curl -H "Content-type: application/json" --data @$WORKDIR/slack.$FILENAME -H "Authorization: Bearer $SLACK_AUDIT_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage - name: Unit tests UI run: yarn test:cov --ci --silent + + - name: Upload Test Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ env.REPORT_NAME }} + path: report + + - name: Deploy report + uses: ./.github/actions/deploy-test-reports + if: always() + with: + group: 'report' + AWS_BUCKET_NAME_TEST: ${{ vars.AWS_BUCKET_NAME_TEST }} + AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} + AWS_DISTRIBUTION_ID: ${{ secrets.AWS_DISTRIBUTION_ID }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Get current date + id: date + if: always() + uses: ./.github/actions/get-current-date + + - name: Add link to report in the workflow summary + if: always() + run: | + link="${{ vars.DEFAULT_TEST_REPORTS_URL }}/${{ steps.date.outputs.date }}/${{ github.run_id }}/${{ env.REPORT_NAME }}/index.html" + echo "[${link}](${link})" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/tests-integration.yml b/.github/workflows/tests-integration.yml new file mode 100644 index 0000000000..3852cf6170 --- /dev/null +++ b/.github/workflows/tests-integration.yml @@ -0,0 +1,200 @@ +name: Integration tests +on: + workflow_call: + inputs: + build: + description: Backend build to run tests over + type: string + default: 'local' + redis_client: + description: Library to use for redis connection + type: string + default: 'ioredis' + report: + description: Send report for test run to slack + type: boolean + default: false + short_rte_list: + description: Use short rte list + type: boolean + default: false + debug: + description: SSH Debug + type: boolean + default: false + +env: + SLACK_AUDIT_REPORT_KEY: ${{ secrets.SLACK_AUDIT_REPORT_KEY }} + SLACK_AUDIT_REPORT_CHANNEL: ${{ secrets.SLACK_AUDIT_REPORT_CHANNEL }} + TEST_MEDIUM_DB_DUMP: ${{ secrets.TEST_MEDIUM_DB_DUMP }} + TEST_BIG_DB_DUMP: ${{ secrets.TEST_BIG_DB_DUMP }} + REPORT_NAME: "report-it" + ITESTS_NAMES: | + { + "oss-st-5": "OSS Standalone v5", + "oss-st-5-pass": "OSS Standalone v5 with admin pass required", + "oss-st-6": "OSS Standalone v6 and all modules", + "oss-st-big": "OSS Standalone v6 and all modules and predefined amount of data inside (~3-4M)", + "mods-preview": "OSS Standalone and all preview modules", + "oss-st-6-tls": "OSS Standalone v6 with TLS enabled", + "oss-st-6-tls-auth": "OSS Standalone v6 with TLS auth required", + "oss-clu": "OSS Cluster", + "oss-clu-tls": "OSS Cluster with TLS enabled", + "oss-sent": "OSS Sentinel", + "oss-sent-tls-auth": "OSS Sentinel with TLS auth", + "re-st": "Redis Enterprise with Standalone inside", + "re-clu": "Redis Enterprise with Cluster inside", + "re-crdt": "Redis Enterprise with active-active database inside" + } + ITESTS_NAMES_SHORT: | + { + "mods-preview": "OSS Standalone and all preview modules", + "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", + "oss-sent-tls-auth": "OSS Sentinel with TLS auth" + } + +jobs: + set-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.parse-matrix.outputs.matrix }} + steps: + - name: Create JSON array for run-tests matrix + id: parse-matrix + run: | + # Extract the JSON object from the environment variable + MATRIX_JSON="$ITESTS_NAMES_SHORT" + + if [ "${{ inputs.short_rte_list }}" == "false" ]; then + MATRIX_JSON="$ITESTS_NAMES" + fi + + MATRIX_ARRAY=$(echo "$MATRIX_JSON" | jq -c 'keys') + + # Output the formed JSON array for use in other jobs + echo "matrix=$MATRIX_ARRAY" >> $GITHUB_OUTPUT + + - name: Verify the formed matrix array + run: | + echo "Formed matrix array:" + echo "${{ steps.parse-matrix.outputs.matrix }}" + + run-tests: + name: ITest + runs-on: ubuntu-latest + needs: set-matrix + environment: + name: production + strategy: + fail-fast: false + matrix: + rte: ${{ fromJson(needs.set-matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@v4 + + # SSH Debug + - name: Enable SSH + uses: mxschmitt/action-tmate@v3 + if: inputs.debug + with: + detached: true + + - name: Download Docker Artifacts + if: inputs.build == 'docker' + uses: actions/download-artifact@v4 + with: + name: docker + path: ./release + + - name: Load built docker image from workspace + if: inputs.build == 'docker' + run: | + docker image load -i ./release/docker-linux-alpine.amd64.tar + + - name: Run tests + run: | + if [ ${{ inputs.redis_client }} == "node-redis" ]; then + export RI_REDIS_CLIENTS_FORCE_STRATEGY=${{ inputs.redis_client }} + fi + + ./redisinsight/api/test/test-runs/start-test-run.sh -r ${{ matrix.rte }} -t ${{ inputs.build }} + mkdir -p mkdir itest/coverages && mkdir -p itest/results + + cp ./redisinsight/api/test/test-runs/coverage/test-run-result.json ./itest/results/${{ matrix.rte }}.result.json + cp ./redisinsight/api/test/test-runs/coverage/test-run-result.xml ./itest/results/${{ matrix.rte }}.result.xml + cp ./redisinsight/api/test/test-runs/coverage/test-run-coverage.json ./itest/coverages/${{ matrix.rte }}.coverage.json + + - name: Upload coverage files as artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverages-${{ matrix.rte }} + path: itest/coverages + + - name: Send report to Slack + if: inputs.report && always() + run: | + ITEST_NAME=${{ matrix.rte }} node ./.github/itest-results.js + curl -H "Content-type: application/json" --data @itests.report.json -H "Authorization: Bearer $SLACK_TEST_REPORT_KEY" -X POST https://slack.com/api/chat.postMessage + + - name: Generate test results + uses: dorny/test-reporter@v1 + id: test-reporter + if: always() + with: + name: 'Test results: IT (${{ matrix.rte }}) tests' + path: itest/results/*.result.xml + reporter: jest-junit + list-tests: 'failed' + list-suites: 'failed' + fail-on-error: 'false' + + - name: Add link to report in the workflow summary + if: always() + run: | + link="${{ steps.test-reporter.outputs.url_html }}" + echo "- [${link}](${link})" >> $GITHUB_STEP_SUMMARY + + coverage: + runs-on: ubuntu-latest + name: Final coverage + needs: run-tests + if: always() + steps: + - uses: actions/checkout@v4 + + - name: Merge coverage artifacts + id: merge-artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: coverages-artifacts + pattern: coverages-* + delete-merged: true + + - name: Download coverage artifacts + uses: actions/download-artifact@v4 + with: + name: coverages-artifacts + path: ./coverages + + - name: Calculate coverage across all tests runs + run: | + npx nyc report -t ./coverages -r text -r text-summary + sudo mkdir -p /usr/src/app + sudo cp -a ./redisinsight/api/. /usr/src/app/ + sudo cp -R ./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 + + - name: Delete Artifact + uses: actions/github-script@v7 + with: + script: | + github.rest.actions.deleteArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: ${{ steps.merge-artifacts.outputs.artifact-id }} + }); + diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8fae373c51..80e2bb7f03 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,28 +1,64 @@ -name: Tests +name: ✅ Tests on: push: branches: - - 'fe/*' - - 'be/*' - # branches-ignore: - # - main + - 'fe/**' + - 'be/**' + - 'e2e/**' workflow_dispatch: inputs: - all_tests: - description: Run all tests (FE, BE, IT, E2E) + group_tests: + description: Run group of tests + default: 'all' + type: choice + options: + - all + - without_e2e + - only_e2e + + redis_client: + description: Library to use for redis connection + default: 'ioredis' + type: choice + options: + - ioredis + - node-redis + + short_rte_list: + description: Use short RTE list for IT type: boolean - required: false + default: true + + debug: + description: Enable SSH Debug (IT and E2E) default: false + type: boolean workflow_call: inputs: - all_tests: - description: Run all tests (FE, BE, IT, E2E) + group_tests: + description: Run group of tests + type: string + default: 'all' + short_rte_list: + description: Use short rte list type: boolean - required: false + default: true + pre_release: + description: Is pre-release default: false + type: boolean + debug: + description: Enable SSH Debug + default: false + type: boolean + +# Cancel a previous run workflow +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: changes: @@ -49,17 +85,81 @@ jobs: - 'tests/e2e/**' frontend-tests: - # TODO: concurrency - # concurrency: build needs: changes - if: needs.changes.outputs.frontend == 'true' || inputs.all_tests || startsWith(github.ref_name, 'fe/') + if: inputs.group_tests == 'all' || inputs.group_tests == 'without_e2e' || startsWith(github.ref_name, 'fe/') uses: ./.github/workflows/tests-frontend.yml secrets: inherit backend-tests: - # TODO: concurrency - # concurrency: build needs: changes - if: needs.changes.outputs.backend == 'true' || inputs.all_tests || startsWith(github.ref_name, 'be/') + if: inputs.group_tests == 'all' || inputs.group_tests == 'without_e2e' || startsWith(github.ref_name, 'be/') uses: ./.github/workflows/tests-backend.yml secrets: inherit + + integration-tests: + needs: changes + if: inputs.group_tests == 'all' || inputs.group_tests == 'without_e2e' || startsWith(github.ref_name, 'be/') + uses: ./.github/workflows/tests-integration.yml + secrets: inherit + with: + short_rte_list: ${{ inputs.short_rte_list || true }} + redis_client: ${{ inputs.redis_client || '' }} + debug: ${{ inputs.debug || false }} + + # # E2E Approve + e2e-approve: + runs-on: ubuntu-latest + needs: changes + if: inputs.group_tests == 'all' || inputs.group_tests == 'only_e2e' || startsWith(github.ref_name, 'e2e/') + timeout-minutes: 60 + environment: ${{ startsWith(github.ref_name, 'e2e/') && 'e2e-approve' || 'staging' }} + name: Approve E2E tests + steps: + - uses: actions/checkout@v4 + + # E2E Docker + build-docker: + uses: ./.github/workflows/pipeline-build-docker.yml + needs: e2e-approve + secrets: inherit + with: + debug: ${{ inputs.debug || false }} + for_e2e_tests: true + + e2e-docker-tests: + needs: build-docker + uses: ./.github/workflows/tests-e2e-docker.yml + secrets: inherit + with: + debug: ${{ inputs.debug || false }} + + # E2E AppImage + build-appimage: + uses: ./.github/workflows/pipeline-build-linux.yml + needs: e2e-approve + secrets: inherit + with: + target: build_linux_appimage_x64 + debug: ${{ inputs.debug || false }} + + e2e-appimage-tests: + needs: build-appimage + uses: ./.github/workflows/tests-e2e-appimage.yml + secrets: inherit + with: + debug: ${{ inputs.debug || false }} + + clean: + uses: ./.github/workflows/clean-deployments.yml + if: always() + needs: [frontend-tests, backend-tests, integration-tests, e2e-docker-tests, e2e-appimage-tests] + + # Remove artifacts from github actions + remove-artifacts: + name: Remove artifacts + needs: [frontend-tests, backend-tests, integration-tests, e2e-docker-tests, e2e-appimage-tests] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Remove all artifacts + uses: ./.github/actions/remove-artifacts diff --git a/.github/workflows/virustotal.yml b/.github/workflows/virustotal.yml index cb5fe09497..c67f520cae 100644 --- a/.github/workflows/virustotal.yml +++ b/.github/workflows/virustotal.yml @@ -22,39 +22,39 @@ jobs: artifact_names: ${{ steps.list_artifacts.outputs.artifact_names }} artifact_exists: ${{ steps.list_artifacts.outputs.artifact_exists }} steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Download All Artifacts - uses: actions/download-artifact@v4 - with: - path: ./release - # TODO: enable pattern filter after fix: - # https://github.com/nektos/act/issues/2433 - # pattern: '*-build' - # merge-multiple: true - - - run: ls -R ./release - - - name: List Artifact Files - id: list_artifacts - run: | - # If artifacts don't exist put array of app names for url check - if [ ! -d "./release" ]; then - echo "NO REALEASE FOLDER ${VIRUSTOTAL_FILE_NAMES}" - echo "artifact_exists=false" >> $GITHUB_OUTPUT - echo "artifact_names=$VIRUSTOTAL_FILE_NAMES" >> $GITHUB_OUTPUT - exit 0; - fi + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Download All Artifacts + uses: actions/download-artifact@v4 + with: + path: ./release + # TODO: enable pattern filter after fix: + # https://github.com/nektos/act/issues/2433 + # pattern: '*-build' + # merge-multiple: true + + - run: ls -R ./release + + - name: List Artifact Files + id: list_artifacts + run: | + # If artifacts don't exist put array of app names for url check + if [ ! -d "./release" ]; then + echo "NO REALEASE FOLDER ${VIRUSTOTAL_FILE_NAMES}" + echo "artifact_exists=false" >> $GITHUB_OUTPUT + echo "artifact_names=$VIRUSTOTAL_FILE_NAMES" >> $GITHUB_OUTPUT + exit 0; + fi - # Get list of artifacts - ARTIFACTS=$(ls ./release) + # Get list of artifacts + ARTIFACTS=$(ls ./release) - # Conver list to json - ARTIFACTS_JSON=$(echo "$ARTIFACTS" | jq -R -s -c 'split("\n")[:-1]') + # Conver list to json + ARTIFACTS_JSON=$(echo "$ARTIFACTS" | jq -R -s -c 'split("\n")[:-1]') - echo "artifact_exists=true" >> $GITHUB_OUTPUT - echo "artifact_names=$ARTIFACTS_JSON" >> $GITHUB_OUTPUT + echo "artifact_exists=true" >> $GITHUB_OUTPUT + echo "artifact_names=$ARTIFACTS_JSON" >> $GITHUB_OUTPUT analyze: name: Analyze file @@ -62,6 +62,7 @@ jobs: needs: download_artifacts strategy: + fail-fast: false matrix: artifact: ${{ fromJson(needs.download_artifacts.outputs.artifact_names) }} diff --git a/.gitignore b/.gitignore index 232acd8c53..4217dfaa02 100644 --- a/.gitignore +++ b/.gitignore @@ -53,10 +53,14 @@ redisinsight/ui/style.css.map redisinsight/ui/dist redisinsight/api/commands redisinsight/api/guides +redisinsight/api/report +redisinsight/api/reports redisinsight/api/dist-minified redisinsight/api/tutorials redisinsight/api/content redisinsight/ui/dist-stats.html +report +reports dist distWeb dll diff --git a/Dockerfile b/Dockerfile index a2247ba379..b7b90e0dd8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,7 +39,7 @@ RUN yarn --cwd ./redisinsight/api install --production COPY ./redisinsight/api/.yarnclean.prod ./redisinsight/api/.yarnclean RUN yarn --cwd ./redisinsight/api autoclean --force -FROM 20.14-alpine +FROM node:20.14-alpine # runtime args and environment variables ARG NODE_ENV=production diff --git a/electron-builder.json b/electron-builder.json index fad7cbfd3e..d0aa82c16d 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -11,8 +11,7 @@ "compression": "normal", "asarUnpack": [ "node_modules/keytar", - "node_modules/sqlite3", - "node_modules/cpu-features" + "node_modules/sqlite3" ], "protocols": [{ "name": "RedisInsight", diff --git a/jest.config.cjs b/jest.config.cjs index a6817a1303..8e87ed4717 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,7 +1,7 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ module.exports = { testEnvironmentOptions: { - url: 'http://localhost/' + url: 'http://localhost/', }, moduleNameMapper: { '\\.(jpg|jpeg|png|ico|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': @@ -18,7 +18,8 @@ module.exports = { 'remark-rehype': '/redisinsight/__mocks__/remarkRehype.js', 'rehype-stringify': '/redisinsight/__mocks__/rehypeStringify.js', 'unist-util-visit': '/redisinsight/__mocks__/unistUtilsVisit.js', - 'react-children-utilities': '/redisinsight/__mocks__/react-children-utilities.js', + 'react-children-utilities': + '/redisinsight/__mocks__/react-children-utilities.js', d3: '/node_modules/d3/dist/d3.min.js', '^uuid$': require.resolve('uuid'), msgpackr: require.resolve('msgpackr'), @@ -27,20 +28,9 @@ module.exports = { 'construct-style-sheets-polyfill', '/redisinsight/ui/src/setup-env.ts', ], - setupFilesAfterEnv: [ - '/redisinsight/ui/src/setup-tests.ts', - ], - moduleDirectories: [ - 'node_modules', - 'redisinsight/node_modules', - ], - moduleFileExtensions: [ - 'js', - 'jsx', - 'ts', - 'tsx', - 'json', - ], + setupFilesAfterEnv: ['/redisinsight/ui/src/setup-tests.ts'], + moduleDirectories: ['node_modules', 'redisinsight/node_modules'], + moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'], testEnvironment: 'jest-environment-jsdom', transformIgnorePatterns: [ 'node_modules/(?!(monaco-editor|react-monaco-editor)/)', @@ -50,13 +40,23 @@ module.exports = { '/redisinsight/ui/src/packages', '/redisinsight/ui/src/mocks', ], - coverageDirectory: './coverage', + coverageDirectory: './report/coverage', coveragePathIgnorePatterns: [ '/node_modules/', '/redisinsight/api', '/redisinsight/ui/src/packages', ], resolver: '/jest-resolver.js', + reporters: [ + 'default', + [ + 'jest-html-reporters', + { + publicPath: './report', + filename: 'index.html', + }, + ], + ], coverageThreshold: { global: { statements: 80, @@ -65,4 +65,4 @@ module.exports = { lines: 80, }, }, -} +}; diff --git a/package.json b/package.json index 73ece58055..83fdb79b1b 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "@babel/preset-env": "^7.23.2", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.2", - "@electron/rebuild": "^3.3.0", + "@electron/rebuild": "^3.7.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "@svgr/webpack": "^8.1.0", "@teamsupercell/typings-for-css-modules-loader": "^2.4.0", @@ -139,7 +139,7 @@ "css-minimizer-webpack-plugin": "^6.0.0", "csv-parser": "^3.0.0", "csv-stringify": "^6.4.0", - "electron": "31.0.2", + "electron": "33.2.0", "electron-builder": "^24.13.3", "electron-builder-notarize": "^1.5.2", "electron-debug": "^3.2.0", @@ -165,6 +165,7 @@ "ioredis-mock": "^5.5.4", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jest-html-reporters": "^3.1.7", "jest-runner-groups": "^2.2.0", "jest-when": "^3.2.1", "license-checker": "^25.0.1", @@ -233,7 +234,7 @@ "gzip-js": "^0.3.2", "html-entities": "^2.3.2", "html-react-parser": "^1.2.4", - "java-object-serialization": "^0.1.1", + "java-object-serialization": "^0.1.2", "js-yaml": "^4.1.0", "json-bigint": "^1.0.0", "jsonpath": "^1.1.1", diff --git a/redisinsight/api/config/default.ts b/redisinsight/api/config/default.ts index eb82353947..ce82d6b23f 100644 --- a/redisinsight/api/config/default.ts +++ b/redisinsight/api/config/default.ts @@ -80,13 +80,15 @@ export default { migrateOldFolders: process.env.RI_MIGRATE_OLD_FOLDERS ? process.env.RI_MIGRATE_OLD_FOLDERS === 'true' : true, autoBootstrap: process.env.RI_AUTO_BOOTSTRAP ? process.env.RI_AUTO_BOOTSTRAP === 'true' : true, buildType: process.env.RI_BUILD_TYPE || 'DOCKER_ON_PREMISE', - appVersion: process.env.RI_APP_VERSION || '2.60.0', + appVersion: process.env.RI_APP_VERSION || '2.62.0', requestTimeout: parseInt(process.env.RI_REQUEST_TIMEOUT, 10) || 25000, excludeRoutes: [], excludeAuthRoutes: [], }, encryption: { keytar: process.env.RI_ENCRYPTION_KEYTAR ? process.env.RI_ENCRYPTION_KEYTAR === 'true' : true, // enabled by default + // !!! DO NOT CHANGE THIS VARIABLE FOR REDIS INSIGHT!!! MUST BE "redisinsight"!!! It's only for vscode extension + keytarService: process.env.RI_ENCRYPTION_KEYTAR_SERVICE || 'redisinsight', encryptionIV: process.env.RI_ENCRYPTION_IV || Buffer.alloc(16, 0), encryptionAlgorithm: process.env.RI_ENCRYPTION_ALGORYTHM || 'aes-256-cbc', }, diff --git a/redisinsight/api/config/ormconfig.ts b/redisinsight/api/config/ormconfig.ts index e53b470879..294e85c5a8 100644 --- a/redisinsight/api/config/ormconfig.ts +++ b/redisinsight/api/config/ormconfig.ts @@ -21,6 +21,7 @@ import { CloudDatabaseDetailsEntity } from 'src/modules/cloud/database/entities/ import { CloudCapiKeyEntity } from 'src/modules/cloud/capi-key/entity/cloud-capi-key.entity'; import { RdiEntity } from 'src/modules/rdi/entities/rdi.entity'; import { AiQueryMessageEntity } from 'src/modules/ai/query/entities/ai-query.message.entity'; +import { CloudSessionEntity } from 'src/modules/cloud/session/entities/cloud.session.entity'; import migrations from '../migration'; import * as config from '../src/utils/config'; @@ -52,6 +53,7 @@ const ormConfig = { CloudCapiKeyEntity, RdiEntity, AiQueryMessageEntity, + CloudSessionEntity, ], migrations, }; diff --git a/redisinsight/api/config/swagger.ts b/redisinsight/api/config/swagger.ts index 5a88b97c66..8c7ae3728f 100644 --- a/redisinsight/api/config/swagger.ts +++ b/redisinsight/api/config/swagger.ts @@ -5,7 +5,7 @@ const SWAGGER_CONFIG: Omit = { info: { title: 'Redis Insight Backend API', description: 'Redis Insight Backend API', - version: '2.60.0', + version: '2.62.0', }, tags: [], }; diff --git a/redisinsight/api/migration/1729085495444-cloud-session.ts b/redisinsight/api/migration/1729085495444-cloud-session.ts new file mode 100644 index 0000000000..2b3bf4b936 --- /dev/null +++ b/redisinsight/api/migration/1729085495444-cloud-session.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CloudSession1729085495444 implements MigrationInterface { + name = 'CloudSession1729085495444' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "cloud_session" ("id" varchar PRIMARY KEY NOT NULL, "data" varchar, "encryption" varchar)`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "cloud_session"`); + } + +} diff --git a/redisinsight/api/migration/index.ts b/redisinsight/api/migration/index.ts index 60f4bda910..8d18dff288 100644 --- a/redisinsight/api/migration/index.ts +++ b/redisinsight/api/migration/index.ts @@ -42,6 +42,7 @@ import { AiHistory1713515657364 } from './1713515657364-ai-history'; import { AiHistorySteps1714501203616 } from './1714501203616-ai-history-steps'; import { Rdi1716370509836 } from './1716370509836-rdi'; import { AiHistory1718260230164 } from './1718260230164-ai-history'; +import { CloudSession1729085495444 } from './1729085495444-cloud-session'; import { CommandExecution1726058563737 } from './1726058563737-command-execution'; export default [ @@ -89,5 +90,6 @@ export default [ AiHistorySteps1714501203616, Rdi1716370509836, AiHistory1718260230164, + CloudSession1729085495444, CommandExecution1726058563737, ]; diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json index a6270bb378..5e44cebeaa 100644 --- a/redisinsight/api/package.json +++ b/redisinsight/api/package.json @@ -1,6 +1,6 @@ { "name": "redisinsight-api", - "version": "2.60.0", + "version": "2.62.0", "description": "Redis Insight API", "private": true, "author": { @@ -48,6 +48,7 @@ "@nestjs/platform-socket.io/socket.io": "^4.8.0", "@nestjs/cli/**/braces": "^3.0.3", "**/semver": "^7.5.2", + "**/cpu-features": "file:./stubs/cpu-features", "winston-daily-rotate-file/**/file-stream-rotator": "^1.0.0" }, "dependencies": { @@ -110,8 +111,8 @@ "@types/node": "14.14.10", "@types/ssh2": "^1.11.6", "@types/supertest": "^2.0.8", - "@typescript-eslint/eslint-plugin": "^4.8.1", - "@typescript-eslint/parser": "^4.8.1", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", "chai": "^4.3.4", "concurrently": "^5.3.0", "cross-env": "^7.0.3", @@ -123,6 +124,8 @@ "eslint-plugin-sonarjs": "^0.9.1", "ioredis-mock": "^8.2.2", "jest": "^29.7.0", + "jest-html-reporters": "^3.1.7", + "jest-junit": "^16.0.0", "jest-when": "^3.2.1", "joi": "^17.4.0", "mocha": "^8.4.0", @@ -153,7 +156,7 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, - "coverageDirectory": "../coverage", + "coverageDirectory": "../report/coverage", "coveragePathIgnorePatterns": [ "/node_modules/", ".entity.ts$", @@ -167,6 +170,13 @@ "src/(.*)": "/$1", "apiSrc/(.*)": "/$1", "tests/(.*)": "/__tests__/$1" - } + }, + "reporters": [ + "default", + ["jest-html-reporters", { + "publicPath": "./report", + "filename": "index.html" + }] + ] } } diff --git a/redisinsight/api/src/__mocks__/cloud-session.ts b/redisinsight/api/src/__mocks__/cloud-session.ts index 4a70f7ff96..da3d103b0f 100644 --- a/redisinsight/api/src/__mocks__/cloud-session.ts +++ b/redisinsight/api/src/__mocks__/cloud-session.ts @@ -1,6 +1,9 @@ import { ICloudApiCredentials } from 'src/modules/cloud/common/models'; import { CloudAuthIdpType } from 'src/modules/cloud/auth/models'; import { ICloudApiCsrfToken } from 'src/modules/cloud/user/models'; +import { CloudSessionData } from 'src/modules/cloud/session/models/cloud-session'; +import { CloudSessionEntity } from 'src/modules/cloud/session/entities/cloud.session.entity'; +import { EncryptionStrategy } from 'src/modules/encryption/models'; export const mockCloudApiCsrfToken: ICloudApiCsrfToken = { csrf_token: 'csrf_p6vA6A5tF36Jf6twH2cBOqtt7n', @@ -14,6 +17,17 @@ export const mockCloudApiAuthDto: ICloudApiCredentials = { apiSessionId: 'asid_p6v-A6A5tF36J-f6twH2cB!@#$_^&*()Oqtt7n', }; +export const mockCloudSessionData = Object.assign(new CloudSessionData(), { + id: '1', + data: { idpType: CloudAuthIdpType.Google }, +}); + +export const mockCloudSessionEntity = Object.assign(new CloudSessionEntity(), { + ...mockCloudSessionData, + data: 'encryptedCloudSessionData', + encryption: EncryptionStrategy.KEYTAR, +}); + export const mockCloudSession = { ...mockCloudApiAuthDto, user: { @@ -32,6 +46,11 @@ export const mockCloudSession = { }, }; +export const mockCloudSessionRepository = jest.fn(() => ({ + get: jest.fn().mockResolvedValue(null), + save: jest.fn(), +})); + export const mockCloudSessionService = jest.fn(() => ({ getSession: jest.fn().mockResolvedValue(mockCloudSession), updateSessionData: jest.fn().mockResolvedValue(mockCloudSession), diff --git a/redisinsight/api/src/__mocks__/custom-tutorial.ts b/redisinsight/api/src/__mocks__/custom-tutorial.ts index 15990b1ba0..11ee2a8f9c 100644 --- a/redisinsight/api/src/__mocks__/custom-tutorial.ts +++ b/redisinsight/api/src/__mocks__/custom-tutorial.ts @@ -143,7 +143,7 @@ export const globalCustomTutorialManifest = { _actions: [CustomTutorialActions.CREATE], args: { withBorder: true, - initialIsOpen: true, + initialIsOpen: false, }, children: [ mockCustomTutorialManifest, diff --git a/redisinsight/api/src/__mocks__/rdi.ts b/redisinsight/api/src/__mocks__/rdi.ts index 6f47a64613..826f90e972 100644 --- a/redisinsight/api/src/__mocks__/rdi.ts +++ b/redisinsight/api/src/__mocks__/rdi.ts @@ -311,13 +311,13 @@ export const mockRdiConfigSchema = { default: 1000, }, target_data_type: { - title: 'Target data type: hash/json - RedisJSON module must be in use in the target DB', + title: 'Target data type: hash/json - JSON module must be in use in the target DB', type: 'string', pattern: '^\\${.*}$|hash|json', default: 'hash', }, json_update_strategy: { - title: 'Target update strategy: replace/merge - RedisJSON module must be in use in the target DB', + title: 'Target update strategy: replace/merge - JSON module must be in use in the target DB', type: 'string', pattern: '^\\${.*}$|replace|merge', default: 'replace', diff --git a/redisinsight/api/src/main.ts b/redisinsight/api/src/main.ts index 3abc61a446..5093770c5d 100644 --- a/redisinsight/api/src/main.ts +++ b/redisinsight/api/src/main.ts @@ -29,7 +29,10 @@ export default async function bootstrap(apiPort?: number): Promise { await migrateHomeFolder() && await removeOldFolders(); } - const { port, host } = serverConfig; + if (apiPort) { + serverConfig.port = apiPort; + } + const logger = WinstonModule.createLogger(LOGGER_CONFIG); const options: NestApplicationOptions = { @@ -75,7 +78,9 @@ export default async function bootstrap(apiPort?: number): Promise { const logFileProvider = app.get(LogFileProvider); - await app.listen(apiPort || port, host); + const { port, host } = serverConfig; + + await app.listen(port, host); logger.log({ message: `Server is running on http(s)://${host}:${port}`, context: 'bootstrap', diff --git a/redisinsight/api/src/modules/browser/rejson-rl/rejson-rl.service.spec.ts b/redisinsight/api/src/modules/browser/rejson-rl/rejson-rl.service.spec.ts index 5c09c32042..3e58d12277 100644 --- a/redisinsight/api/src/modules/browser/rejson-rl/rejson-rl.service.spec.ts +++ b/redisinsight/api/src/modules/browser/rejson-rl/rejson-rl.service.spec.ts @@ -181,7 +181,7 @@ describe('JsonService', () => { } catch (err) { expect(err).toBeInstanceOf(BadRequestException); expect(err.message).toEqual( - ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('RedisJSON'), + ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'), ); } }); @@ -215,7 +215,7 @@ describe('JsonService', () => { expect(result).toEqual({ downloaded: true, path: testPath, - data: testData, + data: JSON.stringify(testData), }); }); it('should return data (number)', async () => { @@ -236,7 +236,7 @@ describe('JsonService', () => { expect(result).toEqual({ downloaded: true, path: testPath, - data: testData, + data: JSON.stringify(testData), }); }); it('should return data (integer)', async () => { @@ -257,7 +257,7 @@ describe('JsonService', () => { expect(result).toEqual({ downloaded: true, path: testPath, - data: testData, + data: JSON.stringify(testData), }); }); it('should return data (boolean)', async () => { @@ -278,7 +278,7 @@ describe('JsonService', () => { expect(result).toEqual({ downloaded: true, path: testPath, - data: testData, + data: JSON.stringify(testData), }); }); it('should return data (null)', async () => { @@ -299,7 +299,7 @@ describe('JsonService', () => { expect(result).toEqual({ downloaded: true, path: testPath, - data: testData, + data: JSON.stringify(testData), }); }); it('should return data (array)', async () => { @@ -328,7 +328,7 @@ describe('JsonService', () => { expect(result).toEqual({ downloaded: true, path: testPath, - data: testData, + data: JSON.stringify(testData), }); }); it('should return data (object)', async () => { @@ -355,7 +355,7 @@ describe('JsonService', () => { expect(result).toEqual({ downloaded: true, path: testPath, - data: testData, + data: JSON.stringify(testData), }); }); it('should return full json data when forceRetrieve is true', async () => { @@ -393,7 +393,7 @@ describe('JsonService', () => { expect(result).toEqual({ downloaded: true, path: testPath, - data: testData, + data: JSON.stringify(testData), }); }); }); @@ -432,7 +432,7 @@ describe('JsonService', () => { expect(result).toEqual({ downloaded: true, path: testPath, - data: testData, + data: JSON.stringify(testData), }); }); @@ -462,7 +462,7 @@ describe('JsonService', () => { expect(result).toEqual({ downloaded: true, path: testPath, - data: testData, + data: JSON.stringify(testData), }); }); }); @@ -503,11 +503,11 @@ describe('JsonService', () => { expect(result).toEqual({ downloaded: false, path: testPath, - data: testData, + data: JSON.stringify(testData), type: 'string', }); }); - it('should return array with scalar values and safe struct types descriptions', async () => { + it('should return array with scalar values as strings and safe struct types descriptions', async () => { const testData = [ 12, 3.14, @@ -555,35 +555,35 @@ describe('JsonService', () => { path: '[0]', cardinality: 1, type: 'integer', - value: testData[0], + value: String(testData[0]), }, { key: 1, path: '[1]', cardinality: 1, type: 'number', - value: testData[1], + value: String(testData[1]), }, { key: 2, path: '[2]', cardinality: 1, type: 'string', - value: testData[2], + value: `"${testData[2]}"`, }, { key: 3, path: '[3]', cardinality: 1, type: 'boolean', - value: testData[3], + value: String(testData[3]), }, { key: 4, path: '[4]', cardinality: 1, type: 'null', - value: testData[4], + value: String(testData[4]), }, { key: 5, @@ -654,19 +654,19 @@ describe('JsonService', () => { path: `${path}[0]`, cardinality: 1, type: 'integer', - value: testData[0], + value: String(testData[0]), }, { key: 1, path: `${path}[1]`, cardinality: 1, type: 'string', - value: testData[1], + value: `"${testData[1]}"`, }, ], }); }); - it('should return object with scalar values and safe struct types descriptions', async () => { + it('should return object with scalar values as strings and safe struct types descriptions', async () => { const testData = { fInt: 12, fNum: 3.14, @@ -752,35 +752,35 @@ describe('JsonService', () => { path: '["fInt"]', cardinality: 1, type: 'integer', - value: testData.fInt, + value: String(testData.fInt), }, { key: 'fNum', path: '["fNum"]', cardinality: 1, type: 'number', - value: testData.fNum, + value: String(testData.fNum), }, { key: 'fStr', path: '["fStr"]', cardinality: 1, type: 'string', - value: testData.fStr, + value: `"${testData.fStr}"`, }, { key: 'fBool', path: '["fBool"]', cardinality: 1, type: 'boolean', - value: testData.fBool, + value: String(testData.fBool), }, { key: 'fNull', path: '["fNull"]', cardinality: 1, type: 'null', - value: testData.fNull, + value: String(testData.fNull), }, { key: 'fArr', @@ -797,7 +797,7 @@ describe('JsonService', () => { ], }); }); - it('should return object with scalar values in a custom path', async () => { + it('should return object with scalar values as strings in a custom path', async () => { const path = '["customPath"]'; const testData = { fInt: 12, @@ -855,14 +855,14 @@ describe('JsonService', () => { path: `${path}["fInt"]`, cardinality: 1, type: 'integer', - value: testData.fInt, + value: String(testData.fInt), }, { key: 'fStr', path: `${path}["fStr"]`, cardinality: 1, type: 'string', - value: testData.fStr, + value: `"${testData.fStr}"`, }, ], }); @@ -923,7 +923,7 @@ describe('JsonService', () => { } catch (err) { expect(err).toBeInstanceOf(BadRequestException); expect(err.message).toEqual( - ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('RedisJSON'), + ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'), ); } }); @@ -1004,7 +1004,7 @@ describe('JsonService', () => { } catch (err) { expect(err).toBeInstanceOf(BadRequestException); expect(err.message).toEqual( - ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('RedisJSON'), + ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'), ); } }); @@ -1097,7 +1097,7 @@ describe('JsonService', () => { } catch (err) { expect(err).toBeInstanceOf(BadRequestException); expect(err.message).toEqual( - ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('RedisJSON'), + ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'), ); } }); @@ -1170,7 +1170,7 @@ describe('JsonService', () => { } catch (err) { expect(err).toBeInstanceOf(BadRequestException); expect(err.message).toEqual( - ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('RedisJSON'), + ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON'), ); } }); diff --git a/redisinsight/api/src/modules/browser/rejson-rl/rejson-rl.service.ts b/redisinsight/api/src/modules/browser/rejson-rl/rejson-rl.service.ts index 9fe4060c39..66045afc64 100644 --- a/redisinsight/api/src/modules/browser/rejson-rl/rejson-rl.service.ts +++ b/redisinsight/api/src/modules/browser/rejson-rl/rejson-rl.service.ts @@ -51,7 +51,7 @@ export class RejsonRlService { ); } - return JSON.parse(data); + return data } private async estimateSize( @@ -253,9 +253,9 @@ export class RejsonRlService { } if (error.message.includes(RedisErrorCodes.UnknownCommand)) { - throw new BadRequestException( - ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('RedisJSON'), - ); + throw new BadRequestException({ + message: ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON') + }); } throw catchAclError(error); @@ -307,9 +307,9 @@ export class RejsonRlService { } if (error.message.includes(RedisErrorCodes.UnknownCommand)) { - throw new BadRequestException( - ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('RedisJSON'), - ); + throw new BadRequestException({ + message: ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON') + }); } // todo: refactor error handling across the project @@ -361,9 +361,9 @@ export class RejsonRlService { } if (error.message.includes(RedisErrorCodes.UnknownCommand)) { - throw new BadRequestException( - ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('RedisJSON'), - ); + throw new BadRequestException({ + message: ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON') + }); } throw catchAclError(error); @@ -402,9 +402,9 @@ export class RejsonRlService { } if (error.message.includes(RedisErrorCodes.UnknownCommand)) { - throw new BadRequestException( - ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('RedisJSON'), - ); + throw new BadRequestException({ + message: ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON') + }); } throw catchAclError(error); @@ -443,9 +443,9 @@ export class RejsonRlService { } if (error.message.includes(RedisErrorCodes.UnknownCommand)) { - throw new BadRequestException( - ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('RedisJSON'), - ); + throw new BadRequestException({ + message: ERROR_MESSAGES.REDIS_MODULE_IS_REQUIRED('JSON') + }); } throw catchAclError(error); diff --git a/redisinsight/api/src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy.spec.ts b/redisinsight/api/src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy.spec.ts index 97fa5aa275..53b229a3c1 100644 --- a/redisinsight/api/src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy.spec.ts +++ b/redisinsight/api/src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy.spec.ts @@ -7,7 +7,7 @@ import { OktaAuth } from '@okta/okta-auth-js'; import { SsoIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy'; import { CloudAuthIdpType } from 'src/modules/cloud/auth/models'; import { - CloudOauthSsoUnsupportedEmailException + CloudOauthSsoUnsupportedEmailException, } from 'src/modules/cloud/auth/exceptions/cloud-oauth.sso-unsupported-email.exception'; jest.mock('@okta/okta-auth-js'); diff --git a/redisinsight/api/src/modules/cloud/auth/cloud-auth.service.spec.ts b/redisinsight/api/src/modules/cloud/auth/cloud-auth.service.spec.ts index b61752709a..39f03b2003 100644 --- a/redisinsight/api/src/modules/cloud/auth/cloud-auth.service.spec.ts +++ b/redisinsight/api/src/modules/cloud/auth/cloud-auth.service.spec.ts @@ -41,7 +41,7 @@ import { CloudSsoFeatureStrategy } from 'src/modules/cloud/cloud-sso.feature.fla import { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptions'; import { SsoIdpCloudAuthStrategy } from 'src/modules/cloud/auth/auth-strategy/sso-idp.cloud.auth-strategy'; import { - CloudOauthSsoUnsupportedEmailException + CloudOauthSsoUnsupportedEmailException, } from 'src/modules/cloud/auth/exceptions/cloud-oauth.sso-unsupported-email.exception'; const mockedAxios = axios as jest.Mocked; @@ -255,7 +255,9 @@ describe('CloudAuthService', () => { error: 'access_denied', error_description: 'Some required properties are missing: email and lastName', }, - )).rejects.toThrow(new CloudOauthMissedRequiredDataException('Some required properties are missing: email and lastName')); + )).rejects.toThrow( + new CloudOauthMissedRequiredDataException('Some required properties are missing: email and lastName'), + ); }); it('should throw an error if request not found', async () => { expect(service['authRequests'].size).toEqual(1); @@ -343,6 +345,7 @@ describe('CloudAuthService', () => { { accessToken: mockCloudAccessTokenNew, refreshToken: mockCloudRefreshTokenNew, + idpType: mockCloudApiAuthDto.idpType, csrf: null, apiSessionId: null, }, diff --git a/redisinsight/api/src/modules/cloud/auth/cloud-auth.service.ts b/redisinsight/api/src/modules/cloud/auth/cloud-auth.service.ts index a82fae6dd5..d1196abdaa 100644 --- a/redisinsight/api/src/modules/cloud/auth/cloud-auth.service.ts +++ b/redisinsight/api/src/modules/cloud/auth/cloud-auth.service.ts @@ -204,7 +204,6 @@ export class CloudAuthService { private async revokeRefreshToken(sessionMetadata: SessionMetadata): Promise { try { const session = await this.sessionService.getSession(sessionMetadata.sessionId); - if (!session?.refreshToken) { return; } @@ -274,6 +273,7 @@ export class CloudAuthService { await this.sessionService.updateSessionData(sessionMetadata.sessionId, { accessToken: data.access_token, refreshToken: data.refresh_token, + idpType, csrf: null, apiSessionId: null, }); diff --git a/redisinsight/api/src/modules/cloud/cloud.module.ts b/redisinsight/api/src/modules/cloud/cloud.module.ts index a0844eda8a..7a2afeea0e 100644 --- a/redisinsight/api/src/modules/cloud/cloud.module.ts +++ b/redisinsight/api/src/modules/cloud/cloud.module.ts @@ -5,6 +5,7 @@ import { CloudUserModule } from 'src/modules/cloud/user/cloud-user.module'; import { CloudTaskModule } from 'src/modules/cloud/task/cloud-task.module'; import { CloudJobModule } from 'src/modules/cloud/job/cloud-job.module'; import { CloudCapiKeyModule } from 'src/modules/cloud/capi-key/cloud-capi-key.module'; +import { CloudSessionModule } from './session/cloud-session.module'; @Module({}) export class CloudModule { @@ -12,6 +13,7 @@ export class CloudModule { return { module: CloudModule, imports: [ + CloudSessionModule.register(), CloudAuthModule, CloudUserModule, CloudAutodiscoveryModule, diff --git a/redisinsight/api/src/modules/cloud/session/cloud-session.module.ts b/redisinsight/api/src/modules/cloud/session/cloud-session.module.ts index 0aaaafe47d..336e4d881d 100644 --- a/redisinsight/api/src/modules/cloud/session/cloud-session.module.ts +++ b/redisinsight/api/src/modules/cloud/session/cloud-session.module.ts @@ -1,8 +1,23 @@ -import { Module } from '@nestjs/common'; +import { Global, Type } from '@nestjs/common'; import { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service'; +import { CloudSessionRepository } from './repositories/cloud.session.repository'; +import { LocalCloudSessionRepository } from './repositories/local.cloud.session.repository'; -@Module({ - providers: [CloudSessionService], - exports: [CloudSessionService], -}) -export class CloudSessionModule {} +@Global() +export class CloudSessionModule { + static register( + cloudSessionRepository: Type = LocalCloudSessionRepository, + ) { + return { + module: CloudSessionModule, + providers: [ + CloudSessionService, + { + provide: CloudSessionRepository, + useClass: cloudSessionRepository, + }, + ], + exports: [CloudSessionService], + }; + } +} diff --git a/redisinsight/api/src/modules/cloud/session/cloud-session.service.spec.ts b/redisinsight/api/src/modules/cloud/session/cloud-session.service.spec.ts index 7e822adabd..b82b473234 100644 --- a/redisinsight/api/src/modules/cloud/session/cloud-session.service.spec.ts +++ b/redisinsight/api/src/modules/cloud/session/cloud-session.service.spec.ts @@ -4,11 +4,13 @@ import { } from 'src/__mocks__'; import { SessionService } from 'src/modules/session/session.service'; import { CloudSessionService } from 'src/modules/cloud/session/cloud-session.service'; -import { mockCloudSession } from 'src/__mocks__/cloud-session'; +import { mockCloudSession, mockCloudSessionRepository } from 'src/__mocks__/cloud-session'; +import { CloudSessionRepository } from './repositories/cloud.session.repository'; describe('CloudSessionService', () => { let service: CloudSessionService; let sessionService: MockType; + let cloudSessionRepository: MockType; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -18,11 +20,16 @@ describe('CloudSessionService', () => { provide: SessionService, useFactory: mockSessionService, }, + { + provide: CloudSessionRepository, + useFactory: mockCloudSessionRepository, + }, ], }).compile(); service = module.get(CloudSessionService); sessionService = module.get(SessionService); + cloudSessionRepository = module.get(CloudSessionRepository); }); describe('getSession', () => { @@ -32,6 +39,18 @@ describe('CloudSessionService', () => { expect(result).toEqual(null); }); + it('should take some additional data in repository if it is not in session', async () => { + cloudSessionRepository.get.mockResolvedValueOnce({ id: 1, data: { idpType: 'test' } }); + const result = await service.getSession(mockInitSession.id); + expect(result.idpType).toBe('test'); + }); + + it('should not fail if data in repository is null', async () => { + cloudSessionRepository.get.mockResolvedValueOnce({ id: 1, data: null }); + const result = await service.getSession(mockInitSession.id); + expect(result).toEqual(null); + }); + it('Should return cloud data only', async () => { sessionService.getSession.mockResolvedValueOnce({ data: { @@ -60,6 +79,14 @@ describe('CloudSessionService', () => { cloud: mockCloudSession, }); }); + + it('Should update data in cloud sesssion repository when necessary fields included', async () => { + sessionService.updateSessionData.mockResolvedValue({ data: { cloud: mockCloudSession } }); + await service.updateSessionData(mockInitSession.id, mockCloudSession); + + expect(cloudSessionRepository.save).toHaveBeenCalled(); + }); + it('Should merge and update cloud data', async () => { sessionService.getSession.mockResolvedValueOnce({ data: { @@ -93,9 +120,10 @@ describe('CloudSessionService', () => { }); describe('deleteSession', () => { - it('should delete cloud session data by id', async () => { + it('should delete cloud session data by id and clear cloud session repository data', async () => { await service.deleteSessionData(mockInitSession.id); expect(sessionService.updateSessionData).toHaveBeenCalledWith(mockInitSession.id, { cloud: null }); + expect(cloudSessionRepository.save).toHaveBeenCalledWith({ data: null }); }); }); diff --git a/redisinsight/api/src/modules/cloud/session/cloud-session.service.ts b/redisinsight/api/src/modules/cloud/session/cloud-session.service.ts index 877eb8fb72..327571fc53 100644 --- a/redisinsight/api/src/modules/cloud/session/cloud-session.service.ts +++ b/redisinsight/api/src/modules/cloud/session/cloud-session.service.ts @@ -3,31 +3,71 @@ import { SessionService } from 'src/modules/session/session.service'; import { CloudSession } from 'src/modules/cloud/session/models/cloud-session'; import { classToPlain, plainToClass } from 'class-transformer'; import { TransformGroup } from 'src/common/constants'; +import { CloudSessionRepository } from './repositories/cloud.session.repository'; @Injectable() export class CloudSessionService { constructor( private readonly sessionService: SessionService, + private readonly cloudSessionRepository: CloudSessionRepository, ) {} async getSession(id: string): Promise { const session = await this.sessionService.getSession(id); - return session?.data?.cloud || null; + const cloud = session?.data?.cloud; + if (!cloud?.refreshToken) { + try { + const cloudSessionData = await this.cloudSessionRepository.get(); + if (cloudSessionData?.data) { + const { data } = cloudSessionData; + + return { + ...cloud, + refreshToken: data.refreshToken, + idpType: data.idpType, + }; + } + } catch (e) { + // ignore + } + } + return cloud || null; } async updateSessionData(id: string, cloud: any): Promise { const session = await this.getSession(id); - return (await this.sessionService.updateSessionData(id, { + const cloudSession = (await this.sessionService.updateSessionData(id, { cloud: plainToClass(CloudSession, { ...classToPlain(session, { groups: [TransformGroup.Secure] }), ...classToPlain(cloud, { groups: [TransformGroup.Secure] }), }, { groups: [TransformGroup.Secure] }), }))?.data?.cloud || null; + + if (cloudSession && cloud?.refreshToken && cloud?.idpType) { + try { + this.cloudSessionRepository.save({ + data: { + refreshToken: cloud.refreshToken, + idpType: cloud.idpType, + }, + }); + } catch (e) { + // ignore + } + } + + return cloudSession; } async deleteSessionData(id: string): Promise { await this.sessionService.updateSessionData(id, { cloud: null }); + + try { + await this.cloudSessionRepository.save({ data: null }); + } catch (e) { + // ignore + } } async invalidateApiSession(id: string): Promise { diff --git a/redisinsight/api/src/modules/cloud/session/entities/cloud.session.entity.ts b/redisinsight/api/src/modules/cloud/session/entities/cloud.session.entity.ts new file mode 100644 index 0000000000..09d031df10 --- /dev/null +++ b/redisinsight/api/src/modules/cloud/session/entities/cloud.session.entity.ts @@ -0,0 +1,18 @@ +import { Expose } from 'class-transformer'; +import { DataAsJsonString } from 'src/common/decorators'; +import { Column, Entity } from 'typeorm'; + +@Entity('cloud_session') +export class CloudSessionEntity { + @Column({ nullable: false, primary: true }) + @Expose() + id: string; + + @Column({ nullable: true }) + @DataAsJsonString() + @Expose() + data: string; + + @Column({ nullable: true }) + encryption: string; +} diff --git a/redisinsight/api/src/modules/cloud/session/models/cloud-session.ts b/redisinsight/api/src/modules/cloud/session/models/cloud-session.ts index 0c5ddfd4c6..7563984a91 100644 --- a/redisinsight/api/src/modules/cloud/session/models/cloud-session.ts +++ b/redisinsight/api/src/modules/cloud/session/models/cloud-session.ts @@ -1,16 +1,32 @@ import { CloudUser } from 'src/modules/cloud/user/models'; import { CloudAuthIdpType } from 'src/modules/cloud/auth/models'; +import { Expose, Type } from 'class-transformer'; export class CloudSession { + @Expose() accessToken?: string; + @Expose() refreshToken?: string; + @Expose() idpType?: CloudAuthIdpType; + @Expose() csrf?: string; + @Expose() apiSessionId?: string; + @Expose() user?: CloudUser; } + +export class CloudSessionData { + @Expose() + id: number; + + @Expose() + @Type(() => CloudSession) + data: CloudSession; +} diff --git a/redisinsight/api/src/modules/cloud/session/repositories/cloud.session.repository.ts b/redisinsight/api/src/modules/cloud/session/repositories/cloud.session.repository.ts new file mode 100644 index 0000000000..4c95ec6fe7 --- /dev/null +++ b/redisinsight/api/src/modules/cloud/session/repositories/cloud.session.repository.ts @@ -0,0 +1,7 @@ +// import { SessionMetadata } from 'src/common/models'; +import { CloudSessionData } from '../models/cloud-session'; + +export abstract class CloudSessionRepository { + abstract get(): Promise; + abstract save(data: Partial): Promise; +} diff --git a/redisinsight/api/src/modules/cloud/session/repositories/local.cloud.session.repository.spec.ts b/redisinsight/api/src/modules/cloud/session/repositories/local.cloud.session.repository.spec.ts new file mode 100644 index 0000000000..3042ee2820 --- /dev/null +++ b/redisinsight/api/src/modules/cloud/session/repositories/local.cloud.session.repository.spec.ts @@ -0,0 +1,78 @@ +import { + MockType, mockCloudSessionData, mockCloudSessionEntity, mockEncryptionService, mockRepository, +} from 'src/__mocks__'; +import { Repository } from 'typeorm'; +import { EncryptionService } from 'src/modules/encryption/encryption.service'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { when } from 'jest-when'; +import { LocalCloudSessionRepository } from './local.cloud.session.repository'; +import { CloudSessionEntity } from '../entities/cloud.session.entity'; +import { CloudAuthIdpType } from '../../auth/models'; + +describe('LocalCloudSessionRepository', () => { + let service: LocalCloudSessionRepository; + let repository: MockType>; + let encryptionService: MockType; + + beforeEach(async () => { + jest.clearAllMocks(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + LocalCloudSessionRepository, + { + provide: getRepositoryToken(CloudSessionEntity), + useFactory: mockRepository, + }, + { + provide: EncryptionService, + useFactory: mockEncryptionService, + }, + ], + }).compile(); + + service = module.get(LocalCloudSessionRepository); + encryptionService = module.get(EncryptionService); + repository = module.get(getRepositoryToken(CloudSessionEntity)); + + encryptionService.decrypt.mockImplementation((value) => value); + when(encryptionService.decrypt) + .calledWith(mockCloudSessionEntity.data, expect.anything()) + .mockResolvedValue(JSON.stringify(mockCloudSessionData.data)); + + encryptionService.encrypt.mockImplementation((value) => value); + when(encryptionService.encrypt) + .calledWith(JSON.stringify(mockCloudSessionData.data)) + .mockResolvedValue({ data: mockCloudSessionEntity.data, encryption: mockCloudSessionEntity.encryption }); + }); + + describe('get', () => { + it('should return null if no cloud session data in the repository', async () => { + await expect(service.get()).resolves.toEqual(null); + }); + + it('should return cloudSession data if it is in the repository', async () => { + repository.findOneBy.mockResolvedValue(mockCloudSessionEntity); + await expect(service.get()).resolves.toEqual(mockCloudSessionData); + }); + }); + + describe('save', () => { + it('should upsert data into repository', async () => { + repository.upsert.mockResolvedValue(mockCloudSessionEntity); + + await expect(service.save({ data: { idpType: CloudAuthIdpType.Google } })).resolves.toEqual(undefined); + }); + + it('encrypts the data before upsertion', async () => { + await service.save({ data: { idpType: CloudAuthIdpType.Google } }); + + expect(repository.upsert).toHaveBeenCalledWith(mockCloudSessionEntity, ['id']); + }); + + it('not fail when null data is stored', async () => { + await expect(service.save({ data: null })).resolves.toEqual(undefined); + }); + }); +}); diff --git a/redisinsight/api/src/modules/cloud/session/repositories/local.cloud.session.repository.ts b/redisinsight/api/src/modules/cloud/session/repositories/local.cloud.session.repository.ts new file mode 100644 index 0000000000..a78aa4ee9a --- /dev/null +++ b/redisinsight/api/src/modules/cloud/session/repositories/local.cloud.session.repository.ts @@ -0,0 +1,44 @@ +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { classToClass } from 'src/utils'; +import { EncryptionService } from 'src/modules/encryption/encryption.service'; +import { ModelEncryptor } from 'src/modules/encryption/model.encryptor'; +import { plainToClass } from 'class-transformer'; +import { CloudSessionRepository } from './cloud.session.repository'; +import { CloudSessionEntity } from '../entities/cloud.session.entity'; +import { CloudSessionData } from '../models/cloud-session'; + +const SESSION_ID = '1'; + +export class LocalCloudSessionRepository extends CloudSessionRepository { + private readonly modelEncryptor: ModelEncryptor; + + constructor( + @InjectRepository(CloudSessionEntity) + private readonly repository: Repository, + private readonly encryptionService: EncryptionService, + ) { + super(); + this.modelEncryptor = new ModelEncryptor(this.encryptionService, ['data']); + } + + async get(): Promise { + const entity = await this.repository.findOneBy({ id: SESSION_ID }); + + if (!entity) { + return null; + } + + const decrypted = await this.modelEncryptor.decryptEntity(entity, false); + + return classToClass(CloudSessionData, decrypted); + } + + async save(cloudAuth: Partial): Promise { + const entity = await this.modelEncryptor.encryptEntity( + plainToClass(CloudSessionEntity, { ...cloudAuth, id: SESSION_ID }), + ); + + await this.repository.upsert(entity, ['id']); + } +} diff --git a/redisinsight/api/src/modules/cloud/user/cloud-user.api.service.ts b/redisinsight/api/src/modules/cloud/user/cloud-user.api.service.ts index 6ab007e172..a30848a727 100644 --- a/redisinsight/api/src/modules/cloud/user/cloud-user.api.service.ts +++ b/redisinsight/api/src/modules/cloud/user/cloud-user.api.service.ts @@ -1,5 +1,4 @@ import { find } from 'lodash'; -import { decode } from 'jsonwebtoken'; import { Injectable, Logger } from '@nestjs/common'; import { SessionMetadata } from 'src/common/models'; import { CloudUserRepository } from 'src/modules/cloud/user/repositories/cloud-user.repository'; @@ -10,11 +9,9 @@ import { CloudApiUnauthorizedException } from 'src/modules/cloud/common/exceptio import { CloudUserApiProvider } from 'src/modules/cloud/user/providers/cloud-user.api.provider'; import { CloudRequestUtm } from 'src/modules/cloud/common/models'; import { CloudAuthService } from 'src/modules/cloud/auth/cloud-auth.service'; -import config from 'src/utils/config'; import { CloudSession } from 'src/modules/cloud/session/models/cloud-session'; import { ServerService } from 'src/modules/server/server.service'; - -const cloudConfig = config.get('cloud'); +import { isValidToken } from './utils'; @Injectable() export class CloudUserApiService { @@ -70,20 +67,15 @@ export class CloudUserApiService { try { const session = await this.sessionService.getSession(sessionMetadata.sessionId); - if (!session?.accessToken) { - throw new CloudApiUnauthorizedException(); - } - - const { exp } = JSON.parse(Buffer.from(session.accessToken.split('.')[1], 'base64').toString()); - - const expiresIn = exp * 1_000 - Date.now(); + if (!isValidToken(session?.accessToken)) { + if (!session?.refreshToken) { + throw new CloudApiUnauthorizedException(); + } - if (expiresIn < cloudConfig.renewTokensBeforeExpire) { - // need to renew - await this.cloudAuthService.renewTokens(sessionMetadata, session.idpType, session.refreshToken); + await this.cloudAuthService.renewTokens(sessionMetadata, session?.idpType, session?.refreshToken); } } catch (e) { - throw new CloudApiUnauthorizedException(); + throw new CloudApiUnauthorizedException(e.message); } } diff --git a/redisinsight/api/src/modules/cloud/user/utils/index.ts b/redisinsight/api/src/modules/cloud/user/utils/index.ts index d1049fa8df..b64902a81d 100644 --- a/redisinsight/api/src/modules/cloud/user/utils/index.ts +++ b/redisinsight/api/src/modules/cloud/user/utils/index.ts @@ -1 +1,2 @@ export * from './cloud-data-converter'; +export * from './token'; diff --git a/redisinsight/api/src/modules/cloud/user/utils/token.spec.ts b/redisinsight/api/src/modules/cloud/user/utils/token.spec.ts new file mode 100644 index 0000000000..01d2ae5682 --- /dev/null +++ b/redisinsight/api/src/modules/cloud/user/utils/token.spec.ts @@ -0,0 +1,18 @@ +import { sign } from 'jsonwebtoken'; +import { isValidToken } from './token'; + +describe('isValidToken', () => { + it('should return false if no token has been provided', () => { + expect(isValidToken()).toEqual(false); + }); + + it('should be falsy if token has been expired', () => { + const expired = sign({ exp: Math.trunc(Date.now() / 1000) - 3600 }, 'test'); + expect(isValidToken(expired)).toBe(false); + }); + + it('should return true if token has not been expired', () => { + const valid = sign({ exp: Math.trunc(Date.now() / 1000) + 3600 }, 'test'); + expect(isValidToken(valid)).toBe(true); + }); +}); diff --git a/redisinsight/api/src/modules/cloud/user/utils/token.ts b/redisinsight/api/src/modules/cloud/user/utils/token.ts new file mode 100644 index 0000000000..b8f8de50a3 --- /dev/null +++ b/redisinsight/api/src/modules/cloud/user/utils/token.ts @@ -0,0 +1,15 @@ +import config from 'src/utils/config'; + +const cloudConfig = config.get('cloud'); + +export const isValidToken = (token?: string) => { + if (!token) { + return false; + } + + const { exp } = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()); + + const expiresIn = exp * 1_000 - Date.now(); + + return expiresIn > cloudConfig.renewTokensBeforeExpire; +}; diff --git a/redisinsight/api/src/modules/custom-tutorial/custom-tutorial.service.ts b/redisinsight/api/src/modules/custom-tutorial/custom-tutorial.service.ts index e5367dfc96..03710c13e7 100644 --- a/redisinsight/api/src/modules/custom-tutorial/custom-tutorial.service.ts +++ b/redisinsight/api/src/modules/custom-tutorial/custom-tutorial.service.ts @@ -145,7 +145,7 @@ export class CustomTutorialService { _actions: [CustomTutorialActions.CREATE], args: { withBorder: true, - initialIsOpen: true, + initialIsOpen: false, }, children, }; diff --git a/redisinsight/api/src/modules/database/providers/database-overview.provider.ts b/redisinsight/api/src/modules/database/providers/database-overview.provider.ts index c8c4e8268b..8756e3d161 100644 --- a/redisinsight/api/src/modules/database/providers/database-overview.provider.ts +++ b/redisinsight/api/src/modules/database/providers/database-overview.provider.ts @@ -219,7 +219,7 @@ export class DatabaseOverviewProvider { return [undefined, undefined]; } - const totalKeysPerDb = {}; + const totalKeysPerDb: Record = {}; masterNodes.forEach((node) => { map( diff --git a/redisinsight/api/src/modules/encryption/strategies/keytar-encryption.strategy.ts b/redisinsight/api/src/modules/encryption/strategies/keytar-encryption.strategy.ts index ea3b4968d9..3dedea1f46 100644 --- a/redisinsight/api/src/modules/encryption/strategies/keytar-encryption.strategy.ts +++ b/redisinsight/api/src/modules/encryption/strategies/keytar-encryption.strategy.ts @@ -11,7 +11,6 @@ import { } from 'src/modules/encryption/exceptions'; import config, { Config } from 'src/utils/config'; -const SERVICE = 'redisinsight'; const ACCOUNT = 'app'; const SERVER_CONFIG = config.get('server') as Config['server']; const ENCRYPTION_CONFIG = config.get('encryption') as Config['encryption']; @@ -54,7 +53,7 @@ export class KeytarEncryptionStrategy implements IEncryptionStrategy { */ private async getPassword(): Promise { try { - return await this.keytar.getPassword(SERVICE, ACCOUNT); + return await this.keytar.getPassword(ENCRYPTION_CONFIG.keytarService, ACCOUNT); } catch (error) { this.logger.error('Unable to get password'); throw new KeytarUnavailableException(); @@ -68,7 +67,7 @@ export class KeytarEncryptionStrategy implements IEncryptionStrategy { */ private async setPassword(password: string): Promise { try { - await this.keytar.setPassword(SERVICE, ACCOUNT, password); + await this.keytar.setPassword(ENCRYPTION_CONFIG.keytarService, ACCOUNT, password); } catch (error) { this.logger.error('Unable to set password'); throw new KeytarUnavailableException(); @@ -109,7 +108,7 @@ export class KeytarEncryptionStrategy implements IEncryptionStrategy { } try { - await this.keytar.getPassword(SERVICE, ACCOUNT); + await this.keytar.getPassword(ENCRYPTION_CONFIG.keytarService, ACCOUNT); return true; } catch (e) { return false; diff --git a/redisinsight/api/src/modules/rdi/rdi.service.ts b/redisinsight/api/src/modules/rdi/rdi.service.ts index 9b03f4c0b0..ee84bf3fb0 100644 --- a/redisinsight/api/src/modules/rdi/rdi.service.ts +++ b/redisinsight/api/src/modules/rdi/rdi.service.ts @@ -16,6 +16,7 @@ import { RdiPipelineNotFoundException, wrapRdiPipelineError } from 'src/modules/ import { isUndefined, omitBy } from 'lodash'; import { deepMerge } from 'src/common/utils'; import { RdiAnalytics } from './rdi.analytics'; +import { CloudAuthService } from '../cloud/auth/cloud-auth.service'; @Injectable() export class RdiService { diff --git a/redisinsight/api/stubs/cpu-features/index.js b/redisinsight/api/stubs/cpu-features/index.js new file mode 100644 index 0000000000..f053ebf797 --- /dev/null +++ b/redisinsight/api/stubs/cpu-features/index.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/redisinsight/api/stubs/cpu-features/package.json b/redisinsight/api/stubs/cpu-features/package.json new file mode 100644 index 0000000000..95b97743c0 --- /dev/null +++ b/redisinsight/api/stubs/cpu-features/package.json @@ -0,0 +1,5 @@ +{ + "name": "cpu-features", + "version": "1.0.0", + "main": "index.js" +} diff --git a/redisinsight/api/test/api/custom-tutorials/POST-custom-tutorials.test.ts b/redisinsight/api/test/api/custom-tutorials/POST-custom-tutorials.test.ts index 5d3a8ff4c5..41732a1d88 100644 --- a/redisinsight/api/test/api/custom-tutorials/POST-custom-tutorials.test.ts +++ b/redisinsight/api/test/api/custom-tutorials/POST-custom-tutorials.test.ts @@ -101,7 +101,7 @@ const globalManifest = { 'create', ], args: { - initialIsOpen: true, + initialIsOpen: false, withBorder: true, }, children: [], diff --git a/redisinsight/api/test/api/rejson-rl/POST-databases-id-rejson_rl-get.test.ts b/redisinsight/api/test/api/rejson-rl/POST-databases-id-rejson_rl-get.test.ts index 4746c7bdb6..e9393bb7b3 100644 --- a/redisinsight/api/test/api/rejson-rl/POST-databases-id-rejson_rl-get.test.ts +++ b/redisinsight/api/test/api/rejson-rl/POST-databases-id-rejson_rl-get.test.ts @@ -64,7 +64,7 @@ describe('POST /databases/:instanceId/rejson-rl/get', () => { responseBody: { downloaded: true, path: '.', - data: constants.TEST_REJSON_VALUE_3, + data: JSON.stringify(constants.TEST_REJSON_VALUE_3), }, }, ].map(mainCheckFn); @@ -83,7 +83,7 @@ describe('POST /databases/:instanceId/rejson-rl/get', () => { responseBody: { downloaded: true, path: '.', - data: constants.TEST_REJSON_VALUE_3, + data: JSON.stringify(constants.TEST_REJSON_VALUE_3), }, }, { @@ -97,7 +97,7 @@ describe('POST /databases/:instanceId/rejson-rl/get', () => { responseBody: { downloaded: true, path: '.object.field', - data: 'value', + data: `"${'value'}"`, }, }, { @@ -111,7 +111,7 @@ describe('POST /databases/:instanceId/rejson-rl/get', () => { responseBody: { downloaded: true, path: '["array"][1]', - data: 2, + data: String(2), }, }, { @@ -175,7 +175,7 @@ describe('POST /databases/:instanceId/rejson-rl/get', () => { responseBody: { downloaded: false, path: '["object"]["some"]', - data: constants.TEST_REJSON_VALUE_3.object.some, // full value right now + data: `"${constants.TEST_REJSON_VALUE_3.object.some}"`, // full value right now type: 'string', }, }, diff --git a/redisinsight/api/test/test-runs/start-test-run.sh b/redisinsight/api/test/test-runs/start-test-run.sh index 49724ba3b6..4da56042f5 100755 --- a/redisinsight/api/test/test-runs/start-test-run.sh +++ b/redisinsight/api/test/test-runs/start-test-run.sh @@ -43,31 +43,31 @@ if test -f "$PRESTART"; then fi echo "Pulling RTE... ${RTE}" -eval "ID=$ID RTE=$RTE docker-compose \ +eval "ID=$ID RTE=$RTE docker compose \ -f $BASEDIR/$BUILD.build.yml \ -f $BASEDIR/$RTE/docker-compose.yml \ --env-file $BASEDIR/$BUILD.build.env pull redis" echo "Building RTE... ${RTE}" -eval "ID=$ID RTE=$RTE docker-compose \ +eval "ID=$ID RTE=$RTE docker compose \ -f $BASEDIR/$BUILD.build.yml \ -f $BASEDIR/$RTE/docker-compose.yml \ --env-file $BASEDIR/$BUILD.build.env build --no-cache redis" echo "Test run is starting... ${RTE}" -eval "ID=$ID RTE=$RTE docker-compose -p $ID \ +eval "ID=$ID RTE=$RTE docker compose -p $ID \ -f $BASEDIR/$BUILD.build.yml \ -f $BASEDIR/$RTE/docker-compose.yml \ --env-file $BASEDIR/$BUILD.build.env run --use-aliases test" echo "Stop all containers... ${RTE}" -eval "ID=$ID RTE=$RTE docker-compose -p $ID \ +eval "ID=$ID RTE=$RTE docker compose -p $ID \ -f $BASEDIR/$BUILD.build.yml \ -f $BASEDIR/$RTE/docker-compose.yml \ --env-file $BASEDIR/$BUILD.build.env stop" echo "Remove containers with anonymous volumes... ${RTE}" -eval "ID=$ID RTE=$RTE docker-compose -p $ID \ +eval "ID=$ID RTE=$RTE docker compose -p $ID \ -f $BASEDIR/$BUILD.build.yml \ -f $BASEDIR/$RTE/docker-compose.yml \ --env-file $BASEDIR/$BUILD.build.env rm -v -f" diff --git a/redisinsight/api/test/test-runs/test.Dockerfile b/redisinsight/api/test/test-runs/test.Dockerfile index 0821d14c7c..ec5e45d825 100644 --- a/redisinsight/api/test/test-runs/test.Dockerfile +++ b/redisinsight/api/test/test-runs/test.Dockerfile @@ -6,6 +6,7 @@ RUN dbus-uuidgen > /var/lib/dbus/machine-id WORKDIR /usr/src/app COPY package.json yarn.lock ./ +COPY stubs ./stubs RUN yarn install COPY . . diff --git a/redisinsight/api/yarn.lock b/redisinsight/api/yarn.lock index 548556ca5f..ce2390fa2c 100644 --- a/redisinsight/api/yarn.lock +++ b/redisinsight/api/yarn.lock @@ -695,6 +695,18 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.0.tgz#db44a6a08520b5f25bbe409f34a59f2d4bcc7ced" integrity sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g== +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz#d1145bf2c20132d6400495d6df4bf59362fd9d56" + integrity sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.4.0": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + "@eslint/eslintrc@^0.4.3": version "0.4.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" @@ -1617,11 +1629,16 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" -"@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8": +"@types/json-schema@^7.0.8": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -1669,6 +1686,11 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== +"@types/semver@^7.3.12": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + "@types/send@*": version "0.17.1" resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301" @@ -1746,33 +1768,23 @@ dependencies: "@types/yargs-parser" "*" -"@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" - integrity sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg== +"@typescript-eslint/eslint-plugin@^5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" + integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== dependencies: - "@typescript-eslint/experimental-utils" "4.33.0" - "@typescript-eslint/scope-manager" "4.33.0" - debug "^4.3.1" - functional-red-black-tree "^1.0.1" - ignore "^5.1.8" - regexpp "^3.1.0" - semver "^7.3.5" + "@eslint-community/regexpp" "^4.4.0" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/type-utils" "5.62.0" + "@typescript-eslint/utils" "5.62.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/experimental-utils@4.33.0": - version "4.33.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd" - integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q== - dependencies: - "@types/json-schema" "^7.0.7" - "@typescript-eslint/scope-manager" "4.33.0" - "@typescript-eslint/types" "4.33.0" - "@typescript-eslint/typescript-estree" "4.33.0" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - -"@typescript-eslint/parser@^4.4.1", "@typescript-eslint/parser@^4.8.1": +"@typescript-eslint/parser@^4.4.1": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.33.0.tgz#dfe797570d9694e560528d18eecad86c8c744899" integrity sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA== @@ -1782,6 +1794,16 @@ "@typescript-eslint/typescript-estree" "4.33.0" debug "^4.3.1" +"@typescript-eslint/parser@^5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" + integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== + dependencies: + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + debug "^4.3.4" + "@typescript-eslint/scope-manager@4.33.0": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz#d38e49280d983e8772e29121cf8c6e9221f280a3" @@ -1790,11 +1812,34 @@ "@typescript-eslint/types" "4.33.0" "@typescript-eslint/visitor-keys" "4.33.0" +"@typescript-eslint/scope-manager@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + +"@typescript-eslint/type-utils@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" + integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew== + dependencies: + "@typescript-eslint/typescript-estree" "5.62.0" + "@typescript-eslint/utils" "5.62.0" + debug "^4.3.4" + tsutils "^3.21.0" + "@typescript-eslint/types@4.33.0": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72" integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ== +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== + "@typescript-eslint/typescript-estree@4.33.0": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz#0dfb51c2908f68c5c08d82aefeaf166a17c24609" @@ -1808,6 +1853,33 @@ semver "^7.3.5" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + eslint-scope "^5.1.1" + semver "^7.3.7" + "@typescript-eslint/visitor-keys@4.33.0": version "4.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd" @@ -1816,6 +1888,14 @@ "@typescript-eslint/types" "4.33.0" eslint-visitor-keys "^2.0.0" +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + dependencies: + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" + "@ungap/promise-all-settled@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" @@ -2546,11 +2626,6 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -buildcheck@~0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238" - integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A== - busboy@^1.0.0, busboy@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" @@ -3133,13 +3208,8 @@ cosmiconfig@^8.2.0: parse-json "^5.2.0" path-type "^4.0.0" -cpu-features@~0.0.9: - version "0.0.9" - resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.9.tgz#5226b92f0f1c63122b0a3eb84cb8335a4de499fc" - integrity sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ== - dependencies: - buildcheck "~0.0.6" - nan "^2.17.0" +"cpu-features@file:./stubs/cpu-features", cpu-features@~0.0.9: + version "1.0.0" create-jest@^29.7.0: version "29.7.0" @@ -3308,6 +3378,11 @@ define-data-property@^1.1.4: es-errors "^1.3.0" gopd "^1.0.1" +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" @@ -3834,13 +3909,6 @@ eslint-utils@^2.1.0: dependencies: eslint-visitor-keys "^1.1.0" -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" @@ -3851,6 +3919,11 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + eslint@^7.1.0: version "7.32.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" @@ -4546,7 +4619,7 @@ globalthis@^1.0.3: dependencies: define-properties "^1.1.3" -globby@^11.0.3: +globby@^11.0.3, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -4570,6 +4643,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.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + growl@1.10.5: version "1.10.5" resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" @@ -4750,7 +4828,7 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.8, ignore@^5.2.0: +ignore@^5.2.0: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== @@ -4971,6 +5049,11 @@ is-date-object@^1.0.1: dependencies: has-tostringtag "^1.0.0" +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -5102,6 +5185,13 @@ is-windows@^1.0.2: resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + 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" @@ -5390,6 +5480,24 @@ jest-haste-map@^29.7.0: optionalDependencies: fsevents "^2.3.2" +jest-html-reporters@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/jest-html-reporters/-/jest-html-reporters-3.1.7.tgz#d8cb6f5d15fd518e601841f90165f37765e7ff34" + integrity sha512-GTmjqK6muQ0S0Mnksf9QkL9X9z2FGIpNSxC52E0PHDzjPQ1XDu2+XTI3B3FS43ZiUzD1f354/5FfwbNIBzT7ew== + dependencies: + fs-extra "^10.0.0" + open "^8.0.3" + +jest-junit@^16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/jest-junit/-/jest-junit-16.0.0.tgz#d838e8c561cf9fdd7eb54f63020777eee4136785" + integrity sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ== + dependencies: + mkdirp "^1.0.4" + strip-ansi "^6.0.1" + uuid "^8.3.2" + xml "^1.0.1" + jest-leak-detector@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" @@ -6328,11 +6436,6 @@ mz@^2.4.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nan@^2.17.0: - version "2.17.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" - integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== - nan@^2.18.0: version "2.18.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" @@ -6348,6 +6451,11 @@ 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== +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -6673,6 +6781,15 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +open@^8.0.3: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -7492,7 +7609,7 @@ schema-utils@^3.2.0: ajv "^6.12.5" ajv-keywords "^3.5.2" -"semver@2 || 3 || 4 || 5", semver@^6.0.0, semver@^6.3.0, semver@^6.3.1, semver@^7.2.1, semver@^7.3.5, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: +"semver@2 || 3 || 4 || 5", semver@^6.0.0, semver@^6.3.0, semver@^6.3.1, semver@^7.2.1, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== diff --git a/redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts b/redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts index ba343769c5..24ffa394fc 100644 --- a/redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts +++ b/redisinsight/desktop/src/lib/aboutPanel/aboutPanel.ts @@ -8,7 +8,7 @@ const ICON_PATH = app.isPackaged export const AboutPanelOptions = { applicationName: 'Redis Insight', - applicationVersion: `${app.getVersion() || '2.60.0'}${ + applicationVersion: `${app.getVersion() || '2.62.0'}${ !config.isProduction ? `-dev-${process.getCreationTime()}` : '' }`, copyright: `Copyright © ${new Date().getFullYear()} Redis Ltd.`, diff --git a/redisinsight/desktop/views/cloud_outh_callback/callback.html b/redisinsight/desktop/views/cloud_outh_callback/callback.html index b4cbc26f23..d825816bbe 100644 --- a/redisinsight/desktop/views/cloud_outh_callback/callback.html +++ b/redisinsight/desktop/views/cloud_outh_callback/callback.html @@ -26,17 +26,17 @@

Thank you

- You may close this window and navigate back to Redis Insight. + To complete the authentication, click "Open Redis Insight"

- Click open Redis Insight below, if you don’t see a dialog. + Click open Redis Insight below, if you don’t see the dialog.
- In the case you are not redirected, check that your package supports
deep linking and try again. + In case you are not redirected, check that your package supports deep linking and try again.
- If the issue persists, report the issue. + If the issue persists, manually add your database from Redis Cloud.
diff --git a/redisinsight/package.json b/redisinsight/package.json index 69059207fe..cc4683bb79 100644 --- a/redisinsight/package.json +++ b/redisinsight/package.json @@ -3,7 +3,7 @@ "appName": "Redis Insight", "productName": "RedisInsight", "private": true, - "version": "2.60.0", + "version": "2.62.0", "description": "Redis Insight", "main": "./dist/main/main.js", "author": { @@ -16,7 +16,8 @@ }, "resolutions": { "**/semver": "^7.5.2", - "sqlite3/**/tar": "^6.2.1" + "sqlite3/**/tar": "^6.2.1", + "**/cpu-features": "file:./api/stubs/cpu-features" }, "dependencies": { "keytar": "^7.9.0", diff --git a/redisinsight/ui/index.tsx b/redisinsight/ui/index.tsx index b51dda7731..0ed8fb6ef3 100644 --- a/redisinsight/ui/index.tsx +++ b/redisinsight/ui/index.tsx @@ -3,9 +3,11 @@ import { createRoot } from 'react-dom/client' import App from 'uiSrc/App' import Router from 'uiSrc/Router' import { listenPluginsEvents } from 'uiSrc/plugins/pluginEvents' +import { migrateLocalStorageData } from 'uiSrc/services' import 'uiSrc/styles/base/_fonts.scss' import 'uiSrc/styles/main.scss' +migrateLocalStorageData() listenPluginsEvents() const rootEl = document.getElementById('root') diff --git a/redisinsight/ui/indexElectron.tsx b/redisinsight/ui/indexElectron.tsx index 641a01f9a4..f18b813246 100644 --- a/redisinsight/ui/indexElectron.tsx +++ b/redisinsight/ui/indexElectron.tsx @@ -2,12 +2,14 @@ import React from 'react' import { createRoot } from 'react-dom/client' import AppElectron from 'uiSrc/electron/AppElectron' import { listenPluginsEvents } from 'uiSrc/plugins/pluginEvents' +import { migrateLocalStorageData } from 'uiSrc/services' import 'uiSrc/styles/base/_fonts.scss' import 'uiSrc/styles/main.scss' window.app.sendWindowId((_e: any, windowId: string = '') => { window.windowId = windowId || window.windowId + migrateLocalStorageData() listenPluginsEvents() const rootEl = document.getElementById('root') diff --git a/redisinsight/ui/package.json b/redisinsight/ui/package.json index 7054878391..5f19941761 100644 --- a/redisinsight/ui/package.json +++ b/redisinsight/ui/package.json @@ -2,7 +2,7 @@ "name": "redisinsight", "appName": "Redis Insight", "productName": "RedisInsight", - "version": "2.60.0", + "version": "2.62.0", "description": "Redis Insight", "author": { "name": "Redis Ltd.", diff --git a/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/constants.ts b/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/constants.ts index ed333376b5..79cbbd1b74 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/constants.ts +++ b/redisinsight/ui/src/components/messages/module-not-loaded-minimalized/constants.ts @@ -9,11 +9,11 @@ export const MODULE_CAPABILITY_TEXT_NOT_AVAILABLE: { [key in RedisDefaultModules text: 'Create a free Redis Stack database with probabilistic data structures that extend the core capabilities of your Redis.' }, [RedisDefaultModules.ReJSON]: { - title: 'JSON capability is not available', + title: 'JSON data structure is not available', text: 'Create a free Redis Stack database with JSON capability that extends the core capabilities of your Redis.' }, [RedisDefaultModules.Search]: { - title: 'Search and query capability is not available', + title: 'Redis Query Engine capability is not available', text: 'Create a free Redis Stack database with search and query features that extend the core capabilities of your Redis.' }, [RedisDefaultModules.TimeSeries]: { diff --git a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx index 88dae8fd93..6511ec6353 100644 --- a/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx +++ b/redisinsight/ui/src/components/messages/module-not-loaded/ModuleNotLoaded.tsx @@ -18,6 +18,13 @@ import { getDbWithModuleLoaded } from 'uiSrc/utils' import { useCapability } from 'uiSrc/services' import styles from './styles.module.scss' +export const MODULE_OAUTH_SOURCE_MAP: { [key in RedisDefaultModules]?: String } = { + [RedisDefaultModules.Bloom]: 'RedisBloom', + [RedisDefaultModules.ReJSON]: 'RedisJSON', + [RedisDefaultModules.Search]: 'RediSearch', + [RedisDefaultModules.TimeSeries]: 'RedisTimeSeries', +} + export interface IProps { moduleName: RedisDefaultModules id: string @@ -31,7 +38,7 @@ const MAX_ELEMENT_WIDTH = 1440 const renderTitle = (width: number, moduleName?: string) => (

- {`${moduleName} ${moduleName === MODULE_TEXT_VIEW.redisgears ? 'are' : 'is'} not available `} + {`${moduleName} ${[MODULE_TEXT_VIEW.redisgears, MODULE_TEXT_VIEW.bf].includes(moduleName) ? 'are' : 'is'} not available `} {width > MAX_ELEMENT_WIDTH &&
} for this database

@@ -51,7 +58,8 @@ const ModuleNotLoaded = ({ moduleName, id, type = 'workbench', onClose }: IProps const [width, setWidth] = useState(0) const freeInstances = useSelector(freeInstancesSelector) || [] - const module = MODULE_TEXT_VIEW[moduleName] + const module = MODULE_OAUTH_SOURCE_MAP[moduleName] + const freeDbWithModule = getDbWithModuleLoaded(freeInstances, moduleName) const source = type === 'browser' ? OAuthSocialSource.BrowserSearch : OAuthSocialSource[module] @@ -104,7 +112,7 @@ const ModuleNotLoaded = ({ moduleName, id, type = 'workbench', onClose }: IProps )}
- {renderTitle(width, module)} + {renderTitle(width, MODULE_TEXT_VIEW[moduleName])} {CONTENT[moduleName]?.text.map((item: string) => ( width > MIN_ELEMENT_WIDTH ? <>{item}
: item @@ -122,7 +130,7 @@ const ModuleNotLoaded = ({ moduleName, id, type = 'workbench', onClose }: IProps ))}
)} - {renderText(module)} + {renderText(MODULE_TEXT_VIEW[moduleName])}
diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx index 3e964542a5..e3fd904d75 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx @@ -6,6 +6,7 @@ import { appInfoSelector } from 'uiSrc/slices/app/info' import { cleanup, mockedStore, render, screen, fireEvent } from 'uiSrc/utils/test-utils' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { appContextSelector } from 'uiSrc/slices/app/context' import NavigationMenu from './NavigationMenu' let store: typeof mockedStore @@ -17,6 +18,13 @@ beforeEach(() => { const mockAppInfoSelector = jest.requireActual('uiSrc/slices/app/info') +jest.mock('uiSrc/slices/app/context', () => ({ + ...jest.requireActual('uiSrc/slices/app/context'), + appContextSelector: jest.fn().mockReturnValue({ + workspace: 'database', + }), +})) + jest.mock('uiSrc/slices/app/info', () => ({ ...jest.requireActual('uiSrc/slices/app/info'), appInfoSelector: jest.fn().mockReturnValue({ @@ -31,6 +39,13 @@ jest.mock('uiSrc/slices/instances/instances', () => ({ }), })) +jest.mock('uiSrc/slices/rdi/instances', () => ({ + ...jest.requireActual('uiSrc/slices/rdi/instances'), + connectedInstanceSelector: jest.fn().mockReturnValue({ + id: 'mockRdiId', + }), +})) + jest.mock('uiSrc/slices/app/features', () => ({ ...jest.requireActual('uiSrc/slices/app/features'), appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({ @@ -157,4 +172,16 @@ describe('NavigationMenu', () => { expect(githubBtn?.getAttribute('href')).toEqual(EXTERNAL_LINKS.githubRepo) }) }) + + it('should render private routes with connectedRdiInstanceId', () => { + (appContextSelector as jest.Mock).mockImplementation(() => ({ + ...appContextSelector, + workspace: 'redisDataIntegration' + })) + + render() + + expect(screen.getByTestId('pipeline-status-page-btn')).toBeTruthy() + expect(screen.getByTestId('pipeline-management-page-btn')).toBeTruthy() + }) }) diff --git a/redisinsight/ui/src/components/navigation-menu/components/notifications-center/NotificationMenu.spec.tsx b/redisinsight/ui/src/components/navigation-menu/components/notifications-center/NotificationMenu.spec.tsx index 3cfd903d12..f7f66403c3 100644 --- a/redisinsight/ui/src/components/navigation-menu/components/notifications-center/NotificationMenu.spec.tsx +++ b/redisinsight/ui/src/components/navigation-menu/components/notifications-center/NotificationMenu.spec.tsx @@ -1,7 +1,7 @@ import { fireEvent } from '@testing-library/react' import { cloneDeep } from 'lodash' import React from 'react' -import { setIsCenterOpen } from 'uiSrc/slices/app/notifications' +import { notificationCenterSelector, setIsCenterOpen } from 'uiSrc/slices/app/notifications' import { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils' import NotificationMenu from './NotificationMenu' @@ -41,4 +41,15 @@ describe('NotificationMenu', () => { expect(screen.getByTestId('total-unread-badge')).toBeInTheDocument() expect(screen.getByTestId('total-unread-badge')).toHaveTextContent('1') }) + + it('should show badge with count 9+ of unread messages', async () => { + (notificationCenterSelector as jest.Mock).mockReturnValueOnce({ + notifications: [], + totalUnread: 13, + isCenterOpen: false, + }) + render() + + expect(screen.getByTestId('total-unread-badge')).toHaveTextContent('9+') + }) }) diff --git a/redisinsight/ui/src/components/navigation-menu/components/notifications-center/NotificationMenu.tsx b/redisinsight/ui/src/components/navigation-menu/components/notifications-center/NotificationMenu.tsx index 123b7f9b0f..335d6cf660 100644 --- a/redisinsight/ui/src/components/navigation-menu/components/notifications-center/NotificationMenu.tsx +++ b/redisinsight/ui/src/components/navigation-menu/components/notifications-center/NotificationMenu.tsx @@ -33,7 +33,11 @@ const NavButton = () => { return (
{!isCenterOpen && !isNotificationOpen ? ({Btn}) : Btn} - {(totalUnread > 0 && !isCenterOpen) && (
{totalUnread}
)} + {(totalUnread > 0 && !isCenterOpen) && ( +
+ {totalUnread > 9 ? '9+' : totalUnread} +
+ )}
) } 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 a782da8186..235170e15d 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 @@ -12,18 +12,18 @@ .badgeUnreadCount { position: absolute; - top: 8px; - right: 8px; - width: 24px; + top: 12px; + right: 12px; + width: 16px; height: 16px; border-radius: 22px; - background: #9E2A28; + background: #8BA2FF; text-align: center; - line-height: 14px; - font-size: 12px; - color: #fff; + line-height: 15px; + font-size: 10px; + color: #000; } } diff --git a/redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.spec.tsx b/redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.spec.tsx index e8e604f54b..6a55c43574 100644 --- a/redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.spec.tsx +++ b/redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.spec.tsx @@ -7,7 +7,7 @@ import { act, cleanup, mockedStore, render, screen, mockedStoreFn, } from 'uiSrc/utils/test-utils' -import { getUserInfo, logoutUser, oauthCloudUserSelector } from 'uiSrc/slices/oauth/cloud' +import { getUserInfo, logoutUser, oauthCloudUserSelector, setInitialLoadingState } from 'uiSrc/slices/oauth/cloud' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { loadSubscriptionsRedisCloud, setSSOFlow } from 'uiSrc/slices/instances/cloud' import { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces' @@ -21,7 +21,10 @@ const mockedProps = mock() jest.mock('uiSrc/slices/oauth/cloud', () => ({ ...jest.requireActual('uiSrc/slices/oauth/cloud'), oauthCloudUserSelector: jest.fn().mockReturnValue({ - data: null + loading: false, + data: null, + error: '', + initialLoading: false }), })) @@ -50,7 +53,26 @@ describe('OAuthUserProfile', () => { expect(render()).toBeTruthy() }) + it('should render loading spinner initially', () => { + (oauthCloudUserSelector as jest.Mock).mockReturnValueOnce({ + loading: false, + data: null, + error: '', + initialLoading: true + }) + render() + + expect(screen.getByTestId('oath-user-profile-spinner')).toBeInTheDocument() + expect(screen.queryByTestId('cloud-sign-in-btn')).not.toBeInTheDocument() + expect(screen.queryByTestId('user-profile-btn')).not.toBeInTheDocument() + }) + it('should render sign in button if no profile', () => { + (oauthCloudUserSelector as jest.Mock).mockReturnValue({ + loading: false, + data: null, + error: 'Some error', + }) render() expect(screen.getByTestId('cloud-sign-in-btn')).toBeInTheDocument() @@ -117,7 +139,11 @@ describe('OAuthUserProfile', () => { } }) - expect(store.getActions()).toEqual([setSSOFlow(OAuthSocialAction.Import), loadSubscriptionsRedisCloud()]); + expect(store.getActions()).toEqual([ + setInitialLoadingState(false), + setSSOFlow(OAuthSocialAction.Import), + loadSubscriptionsRedisCloud() + ]); (sendEventTelemetry as jest.Mock).mockRestore() }) @@ -139,7 +165,7 @@ describe('OAuthUserProfile', () => { fireEvent.click(screen.getByTestId('profile-account-2')) - expect(store.getActions()).toEqual([getUserInfo()]); + expect(store.getActions()).toEqual([setInitialLoadingState(false), getUserInfo()]); (sendEventTelemetry as jest.Mock).mockRestore() }) @@ -180,6 +206,6 @@ describe('OAuthUserProfile', () => { fireEvent.click(screen.getByTestId('profile-logout')) - expect(store.getActions()).toEqual([logoutUser(), setSSOFlow()]) + expect(store.getActions()).toEqual([setInitialLoadingState(false), logoutUser(), setSSOFlow()]) }) }) diff --git a/redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.tsx b/redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.tsx index 9bb20b69f1..865caab2ff 100644 --- a/redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.tsx +++ b/redisinsight/ui/src/components/oauth/oauth-user-profile/OAuthUserProfile.tsx @@ -1,11 +1,16 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { EuiIcon, EuiLink, EuiLoadingSpinner, EuiPopover, EuiText } from '@elastic/eui' import cx from 'classnames' import { useHistory } from 'react-router-dom' import OAuthSignInButton from 'uiSrc/components/oauth/oauth-sign-in-button' -import { activateAccount, logoutUserAction, oauthCloudUserSelector } from 'uiSrc/slices/oauth/cloud' +import { + activateAccount, + logoutUserAction, + oauthCloudUserSelector, + setInitialLoadingState +} from 'uiSrc/slices/oauth/cloud' import CloudIcon from 'uiSrc/assets/img/oauth/cloud.svg?react' import { getUtmExternalLink } from 'uiSrc/utils/links' @@ -27,7 +32,7 @@ export interface Props { const OAuthUserProfile = (props: Props) => { const { source } = props const [selectingAccountId, setSelectingAccountId] = useState() - const { data } = useSelector(oauthCloudUserSelector) + const { error, data, initialLoading } = useSelector(oauthCloudUserSelector) const { server } = useSelector(appInfoSelector) const [isProfileOpen, setIsProfileOpen] = useState(false) @@ -36,9 +41,27 @@ const OAuthUserProfile = (props: Props) => { const dispatch = useDispatch() const history = useHistory() + useEffect(() => { + if (data || error) { + dispatch(setInitialLoadingState(false)) + } + }, [data, error]) + if (!data) { if (server?.packageType === PackageType.Mas) return null + if (initialLoading) { + return ( +
+ +
+ ) + } + return (
diff --git a/redisinsight/ui/src/components/oauth/oauth-user-profile/styles.module.scss b/redisinsight/ui/src/components/oauth/oauth-user-profile/styles.module.scss index 70078b7eb2..1da073773e 100644 --- a/redisinsight/ui/src/components/oauth/oauth-user-profile/styles.module.scss +++ b/redisinsight/ui/src/components/oauth/oauth-user-profile/styles.module.scss @@ -135,3 +135,12 @@ } } } + +.loadingContainer { + display: flex; + align-items: center; + + .loading { + border-top-color: var(--euiColorGhost) !important; + } +} diff --git a/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/ExpertChat.tsx b/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/ExpertChat.tsx index 0dd2cf4048..27f7509cfe 100644 --- a/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/ExpertChat.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/ExpertChat.tsx @@ -173,16 +173,16 @@ const ExpertChat = () => { if (!instanceId) { return { title: 'Open a database', - content: 'Open your Redis database with search & query, or create a new database to get started.' + content: 'Open your Redis database with Redis Query Engine, or create a new database to get started.' } } if (!isRedisearchAvailable(modules)) { return { - title: 'Search & query capability is not available', + title: 'Redis Query Engine capability is not available', content: freeInstances?.length ? 'Use your free all-in-one Redis Cloud database to start exploring these capabilities.' - : 'Create a free Redis Stack database with search & query capability that extends the core capabilities of open-source Redis.', + : 'Create a free Redis Stack database with Redis Query Engine capability that extends the core capabilities of open-source Redis.', icon: ( { } const handleOpen = (isOpen: boolean) => { + if (forceState === 'open') return + setIsGroupOpen(isOpen) onToggle?.(isOpen) } const actionsContent = ( <> - {actions?.includes(EAItemActions.Create) && ( + {actions?.includes(EAItemActions.Create) && (isGroupOpen || forceState === 'open') && ( { const { tutorials, customTutorials, isInternalPageVisible } = props + const { currentStep, isActive } = useSelector(appFeatureOnboardingSelector) const [isCreateOpen, setIsCreateOpen] = useState(false) const dispatch = useDispatch() const { instanceId = '' } = useParams<{ instanceId: string }>() + const isCustomTutorialsOnboarding = currentStep === OnboardingSteps.CustomTutorials && isActive + + useEffect(() => () => { + dispatch(setWbCustomTutorialsState()) + }, []) + const submitCreate = ({ file, link }: FormValues) => { const formData = new FormData() @@ -66,7 +79,10 @@ const Navigation = (props: Props) => { dispatch(uploadCustomTutorial( formData, - () => setIsCreateOpen(false), + () => { + setIsCreateOpen(false) + dispatch(setWbCustomTutorialsState(true)) + }, )) } @@ -111,6 +127,7 @@ const Navigation = (props: Props) => { onCreate={() => setIsCreateOpen((v) => !v)} onDelete={onDeleteCustomTutorial} isPageOpened={isInternalPageVisible} + forceState={isCustomTutorials && isCustomTutorialsOnboarding ? 'open' : undefined} {...args} > {isCustomTutorials && actions?.includes(EAItemActions.Create) && ( diff --git a/redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/LiveTimeRecommendations.tsx b/redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/LiveTimeRecommendations.tsx index 7bd5d6371e..781fea4e59 100644 --- a/redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/LiveTimeRecommendations.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/LiveTimeRecommendations.tsx @@ -12,7 +12,7 @@ import { } from '@elastic/eui' import { remove } from 'lodash' -import { Pages } from 'uiSrc/constants' +import { DEFAULT_DELIMITER, Pages } from 'uiSrc/constants' import { ANALYZE_CLUSTER_TOOLTIP_MESSAGE, ANALYZE_TOOLTIP_MESSAGE } from 'uiSrc/constants/recommendations' import { recommendationsSelector, @@ -26,6 +26,7 @@ import { IRecommendation } from 'uiSrc/slices/interfaces/recommendations' import { appContextDbConfig, setRecommendationsShowHidden } from 'uiSrc/slices/app/context' import { ConnectionType } from 'uiSrc/slices/interfaces' import { createNewAnalysis } from 'uiSrc/slices/analytics/dbAnalysis' +import { comboBoxToArray } from 'uiSrc/utils' import InfoIcon from 'uiSrc/assets/img/icons/help_illus.svg' @@ -45,7 +46,7 @@ const LiveTimeRecommendations = () => { } = useSelector(recommendationsSelector) const { showHiddenRecommendations: isShowHidden, - treeViewDelimiter: delimiter = '', + treeViewDelimiter = [DEFAULT_DELIMITER], } = useSelector(appContextDbConfig) const { instanceId } = useParams<{ instanceId: string }>() @@ -68,7 +69,7 @@ const LiveTimeRecommendations = () => { }, []) const handleClickDbAnalysisLink = () => { - dispatch(createNewAnalysis(instanceId, delimiter)) + dispatch(createNewAnalysis(instanceId, comboBoxToArray(treeViewDelimiter))) history.push(Pages.databaseAnalysis(instanceId)) sendEventTelemetry({ event: TelemetryEvent.INSIGHTS_TIPS_DATABASE_ANALYSIS_CLICKED, diff --git a/redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/welcome-screen/WelcomeScreen.tsx b/redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/welcome-screen/WelcomeScreen.tsx index eaaafd7c3c..d8650fe3ae 100644 --- a/redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/welcome-screen/WelcomeScreen.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/welcome-screen/WelcomeScreen.tsx @@ -4,7 +4,7 @@ import { useHistory, useParams } from 'react-router-dom' import cx from 'classnames' import { EuiText, EuiButton } from '@elastic/eui' -import { Pages } from 'uiSrc/constants' +import { DEFAULT_DELIMITER, Pages } from 'uiSrc/constants' import { recommendationsSelector } from 'uiSrc/slices/recommendations/recommendations' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' @@ -12,6 +12,7 @@ import WelcomeIcon from 'uiSrc/assets/img/icons/welcome.svg?react' import { appContextDbConfig } from 'uiSrc/slices/app/context' import { createNewAnalysis } from 'uiSrc/slices/analytics/dbAnalysis' import { ConnectionType } from 'uiSrc/slices/interfaces' +import { comboBoxToArray } from 'uiSrc/utils' import { ANALYZE_CLUSTER_TOOLTIP_MESSAGE, ANALYZE_TOOLTIP_MESSAGE } from 'uiSrc/constants/recommendations' import PopoverRunAnalyze from '../popover-run-analyze' @@ -20,7 +21,7 @@ import styles from './styles.module.scss' const NoRecommendationsScreen = () => { const { provider, connectionType } = useSelector(connectedInstanceSelector) const { data: { recommendations } } = useSelector(recommendationsSelector) - const { treeViewDelimiter: delimiter = '' } = useSelector(appContextDbConfig) + const { treeViewDelimiter = [DEFAULT_DELIMITER] } = useSelector(appContextDbConfig) const [isShowInfo, setIsShowInfo] = useState(false) @@ -29,7 +30,7 @@ const NoRecommendationsScreen = () => { const history = useHistory() const handleClickDbAnalysisLink = () => { - dispatch(createNewAnalysis(instanceId, delimiter)) + dispatch(createNewAnalysis(instanceId, comboBoxToArray(treeViewDelimiter))) history.push(Pages.databaseAnalysis(instanceId)) sendEventTelemetry({ event: TelemetryEvent.INSIGHTS_TIPS_DATABASE_ANALYSIS_CLICKED, diff --git a/redisinsight/ui/src/constants/browser.ts b/redisinsight/ui/src/constants/browser.ts index 453f28dd97..ab4b077c6a 100644 --- a/redisinsight/ui/src/constants/browser.ts +++ b/redisinsight/ui/src/constants/browser.ts @@ -1,6 +1,7 @@ +import { EuiComboBoxOptionOption } from '@elastic/eui' import { KeyValueFormat, SortOrder } from './keys' -export const DEFAULT_DELIMITER = ':' +export const DEFAULT_DELIMITER: EuiComboBoxOptionOption = { label: ':' } export const DEFAULT_TREE_SORTING = SortOrder.ASC export const DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS = false diff --git a/redisinsight/ui/src/constants/help-texts.tsx b/redisinsight/ui/src/constants/help-texts.tsx index 35d5eec54e..8ec40bee2d 100644 --- a/redisinsight/ui/src/constants/help-texts.tsx +++ b/redisinsight/ui/src/constants/help-texts.tsx @@ -7,22 +7,15 @@ import styles from 'uiSrc/pages/browser/components/popover-delete/styles.module. export default { REJSON_SHOULD_BE_LOADED: ( <> - RedisJSON module should be loaded to add this key. Find  - - more information - -   - about RedisJSON or create your  - - free Redis database - -   - with RedisJSON on Redis Cloud. + This database does not support the JSON data structure. Learn more about JSON support + {' '} + here. + {' '} + You can also create a + {' '} + free Redis Cloud database + {' '} + with built-in JSON support. ), REMOVE_LAST_ELEMENT: (fieldType: string) => ( diff --git a/redisinsight/ui/src/constants/mocks/mock-custom-tutorials.ts b/redisinsight/ui/src/constants/mocks/mock-custom-tutorials.ts index 256e32b0cd..6c161ddb26 100644 --- a/redisinsight/ui/src/constants/mocks/mock-custom-tutorials.ts +++ b/redisinsight/ui/src/constants/mocks/mock-custom-tutorials.ts @@ -6,6 +6,9 @@ export const MOCK_CUSTOM_TUTORIALS_ITEMS: IEnablementAreaItem[] = [ label: 'MY TUTORIALS', type: EnablementAreaComponent.Group, _actions: ['create'], + args: { + initialIsOpen: true + }, children: [ { id: '12mfp-rem', diff --git a/redisinsight/ui/src/constants/mocks/mock-recommendations.ts b/redisinsight/ui/src/constants/mocks/mock-recommendations.ts index 8dfd17b7e4..f5979368b5 100644 --- a/redisinsight/ui/src/constants/mocks/mock-recommendations.ts +++ b/redisinsight/ui/src/constants/mocks/mock-recommendations.ts @@ -875,7 +875,7 @@ export const MOCK_RECOMMENDATIONS: IRecommendationsStatic = { type: 'link', value: { href: 'https://redis.io/docs/interact/search-and-query/', - name: 'RediSearch' + name: 'Redis Query Engine' } }, { diff --git a/redisinsight/ui/src/constants/workbenchResults.ts b/redisinsight/ui/src/constants/workbenchResults.ts index fcba84cd7b..db67184ab2 100644 --- a/redisinsight/ui/src/constants/workbenchResults.ts +++ b/redisinsight/ui/src/constants/workbenchResults.ts @@ -6,7 +6,7 @@ export const EMPTY_COMMAND = 'Encrypted data' export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = { [RedisDefaultModules.TimeSeries]: { - text: ['RedisTimeSeries adds a Time Series data structure to Redis. ', 'With this capability you can:'], + text: ['Time series data structure adds the capability to:'], improvements: [ 'Add sample data', 'Perform cross-time-series range and aggregation queries', @@ -15,7 +15,7 @@ export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = link: 'https://redis.io/docs/latest/develop/data-types/timeseries/' }, [RedisDefaultModules.Search]: { - text: ['RediSearch adds the capability to:'], + text: ['Redis Query Engine allows to:'], improvements: [ 'Query', 'Secondary index', @@ -25,17 +25,17 @@ export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = link: 'https://redis.io/docs/interact/search-and-query/' }, [RedisDefaultModules.ReJSON]: { - text: ['RedisJSON adds the capability to:'], + text: ['JSON adds the capability to:'], improvements: [ 'Store JSON documents', 'Update JSON documents', 'Retrieve JSON documents' ], - additionalText: ['RedisJSON also works seamlessly with RediSearch to let you index and query JSON documents.'], + additionalText: ['JSON data structure also works seamlessly with Redis Query Engine to let you index and query JSON documents.'], link: 'https://redis.io/docs/latest/develop/data-types/json/' }, [RedisDefaultModules.Bloom]: { - text: ['RedisBloom adds a set of probabilistic data structures to Redis, including:'], + text: ['Probabilistic data structures include:'], improvements: [ 'Bloom filter', 'Cuckoo filter', @@ -43,14 +43,14 @@ export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = 'Top-K', 'T-digest' ], - additionalText: ['With this capability you can query streaming data without needing to store all the elements of the stream.'], + additionalText: ['With these data structures, you can query streaming data without needing to store all the elements of the stream.'], link: 'https://redis.io/docs/latest/develop/data-types/probabilistic/bloom-filter/' }, } export const MODULE_TEXT_VIEW: { [key in RedisDefaultModules]?: string } = { - [RedisDefaultModules.Bloom]: 'RedisBloom', - [RedisDefaultModules.ReJSON]: 'RedisJSON', - [RedisDefaultModules.Search]: 'RediSearch', - [RedisDefaultModules.TimeSeries]: 'RedisTimeSeries', + [RedisDefaultModules.Bloom]: 'Probabilistic data structures', + [RedisDefaultModules.ReJSON]: 'JSON data structure', + [RedisDefaultModules.Search]: 'Redis Query Engine', + [RedisDefaultModules.TimeSeries]: 'Time series data structure', } diff --git a/redisinsight/ui/src/contexts/themeContext.tsx b/redisinsight/ui/src/contexts/themeContext.tsx index 4beccdaa06..e3f5632c31 100644 --- a/redisinsight/ui/src/contexts/themeContext.tsx +++ b/redisinsight/ui/src/contexts/themeContext.tsx @@ -1,9 +1,10 @@ import React from 'react' +import { ipcThemeChange } from 'uiSrc/electron/utils' import { BrowserStorageItem, Theme, THEMES, THEME_MATCH_MEDIA_DARK } from '../constants' import { localStorageService, themeService } from '../services' interface Props { - children: React.ReactNode; + children: React.ReactNode } const THEME_NAMES = THEMES.map(({ value }) => value) @@ -36,14 +37,17 @@ export class ThemeProvider extends React.Component { } } - getSystemTheme = () => (window.matchMedia && window.matchMedia(THEME_MATCH_MEDIA_DARK).matches ? Theme.Dark : Theme.Light) + getSystemTheme = () => (window.matchMedia?.(THEME_MATCH_MEDIA_DARK)?.matches ? Theme.Dark : Theme.Light) - changeTheme = (themeValue: any) => { + changeTheme = async (themeValue: any) => { let actualTheme = themeValue + + // since change theme is async need to wait to have a proper prefers-color-scheme + await ipcThemeChange(themeValue) + if (themeValue === Theme.System) { actualTheme = this.getSystemTheme() } - window.app?.ipc?.invoke?.('theme:change', themeValue) this.setState({ theme: actualTheme, usingSystemTheme: themeValue === Theme.System }, () => { themeService.applyTheme(themeValue) diff --git a/redisinsight/ui/src/electron/utils/index.ts b/redisinsight/ui/src/electron/utils/index.ts index 88cd6888b6..30c2ce2318 100644 --- a/redisinsight/ui/src/electron/utils/index.ts +++ b/redisinsight/ui/src/electron/utils/index.ts @@ -2,5 +2,6 @@ import { ipcCheckUpdates, ipcSendEvents } from './ipcCheckUpdates' export * from './ipcAuth' export * from './ipcAppRestart' +export * from './ipcThemeChange' export { ipcCheckUpdates, ipcSendEvents } diff --git a/redisinsight/ui/src/electron/utils/ipcThemeChange.ts b/redisinsight/ui/src/electron/utils/ipcThemeChange.ts new file mode 100644 index 0000000000..1cd1be46b3 --- /dev/null +++ b/redisinsight/ui/src/electron/utils/ipcThemeChange.ts @@ -0,0 +1,8 @@ +import { IpcInvokeEvent } from '../constants' + +export const ipcThemeChange = async (value: string) => { + await window.app?.ipc?.invoke?.( + IpcInvokeEvent.themeChange, + value, + ) +} diff --git a/redisinsight/ui/src/helpers/constructKeysToTree.ts b/redisinsight/ui/src/helpers/constructKeysToTree.ts index 52cb959f6d..67c8a5bcca 100644 --- a/redisinsight/ui/src/helpers/constructKeysToTree.ts +++ b/redisinsight/ui/src/helpers/constructKeysToTree.ts @@ -3,20 +3,21 @@ import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' interface Props { items: IKeyPropTypes[] - delimiter?: string + delimiterPattern?: string + delimiters?: string[] sorting?: SortOrder } export const constructKeysToTree = (props: Props): any[] => { - const { items: keys, delimiter = ':', sorting = 'ASC' } = props - const keysSymbol = `keys${delimiter}keys` + const { items: keys, delimiterPattern = ':', delimiters = [], sorting = 'ASC' } = props + const keysSymbol = `keys${delimiterPattern}keys` const tree: any = {} keys.forEach((key: any) => { // eslint-disable-next-line prefer-object-spread let currentNode: any = tree const { nameString: name = '' } = key - const nameSplitted = name.split(delimiter) + const nameSplitted = name.split(new RegExp(delimiterPattern, 'g')) const lastIndex = nameSplitted.length - 1 nameSplitted.forEach((value:any, index: number) => { @@ -78,33 +79,34 @@ export const constructKeysToTree = (props: Props): any[] => { return treeNodes.map((key, index) => { const name = key?.toString() const node: any = { nameString: name } - const tillNowKeyName = previousKey + name + delimiter const path = prevIndex ? `${prevIndex}.${index}` : `${index}` // populate node with children nodes if (!tree[key].isLeaf && Object.keys(tree[key]).length > 0) { + const delimiterView = delimiters.length === 1 ? delimiters[0] : '-' node.children = formatTreeData( tree[key], - tillNowKeyName, + `${previousKey + name + delimiterView}`, delimiter, path, ) node.keyCount = node.children.reduce((a: any, b:any) => a + (b.keyCount || 1), 0) node.keyApproximate = (node.keyCount / keys.length) * 100 + node.fullName = previousKey + name } else { // populate leaf node.isLeaf = true node.children = [] node.nameString = name.slice(0, -keysSymbol.length) node.nameBuffer = tree[key]?.name + node.fullName = previousKey + name + delimiter } node.path = path - node.fullName = tillNowKeyName node.id = getUniqueId() return node }) } - return formatTreeData(tree, '', delimiter) + return formatTreeData(tree, '', delimiterPattern) } diff --git a/redisinsight/ui/src/helpers/tests/constructKeysToTree.spec.ts b/redisinsight/ui/src/helpers/tests/constructKeysToTree.spec.ts index c2c413f1e2..88e846dbca 100644 --- a/redisinsight/ui/src/helpers/tests/constructKeysToTree.spec.ts +++ b/redisinsight/ui/src/helpers/tests/constructKeysToTree.spec.ts @@ -1,12 +1,10 @@ -import { DEFAULT_DELIMITER } from 'uiSrc/constants' -import { constructKeysToTreeMockResult } from './constructKeysToTreeMockResult' +import { constructKeysToTreeMockResult, delimiterMock } from './constructKeysToTreeMockResult' import { constructKeysToTree } from '../constructKeysToTree' const constructKeysToTreeTests: any[] = [ [{ items: [ { nameString: 'keys:1:2', type: 'hash', ttl: -1, size: 71 }, - { nameString: 'keys2', type: 'hash', ttl: -1, size: 71 }, { nameString: 'keys:1:1', type: 'hash', ttl: -1, size: 71 }, { nameString: 'empty::test', type: 'hash', ttl: -1, size: 71 }, { nameString: 'test1', type: 'hash', ttl: -1, size: 71 }, @@ -15,8 +13,9 @@ const constructKeysToTreeTests: any[] = [ { nameString: 'keys1', type: 'hash', ttl: -1, size: 71 }, { nameString: 'keys:3', type: 'hash', ttl: -1, size: 71 }, { nameString: 'keys:2', type: 'hash', ttl: -1, size: 71 }, + { nameString: 'keys_2', type: 'hash', ttl: -1, size: 71 }, ], - delimiter: DEFAULT_DELIMITER + delimiterPattern: delimiterMock }, constructKeysToTreeMockResult ] diff --git a/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts b/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts index 0bce8a266c..7dcd4dc32c 100644 --- a/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts +++ b/redisinsight/ui/src/helpers/tests/constructKeysToTreeMockResult.ts @@ -1,3 +1,4 @@ +export const delimiterMock = ':|_' export const constructKeysToTreeMockResult = [ { nameString: 'empty', @@ -10,19 +11,19 @@ export const constructKeysToTreeMockResult = [ isLeaf: true, children: [], path: '0.0.0', - fullName: 'empty::empty::testkeys:keys:', + fullName: `empty--empty::testkeys${delimiterMock}keys${delimiterMock}`, } ], keyCount: 1, keyApproximate: 10, path: '0.0', - fullName: 'empty::', + fullName: 'empty-', } ], keyCount: 1, keyApproximate: 10, path: '0', - fullName: 'empty:', + fullName: 'empty', }, { nameString: 'keys', @@ -35,74 +36,74 @@ export const constructKeysToTreeMockResult = [ isLeaf: true, children: [], path: '1.0.0', - fullName: 'keys:1:keys:1:1keys:keys:', + fullName: `keys-1-keys:1:1keys${delimiterMock}keys${delimiterMock}`, }, { nameString: 'keys:1:2', isLeaf: true, children: [], path: '1.0.1', - fullName: 'keys:1:keys:1:2keys:keys:', + fullName: `keys-1-keys:1:2keys${delimiterMock}keys${delimiterMock}`, } ], keyCount: 2, keyApproximate: 20, path: '1.0', - fullName: 'keys:1:', + fullName: 'keys-1', }, { - nameString: 'keys:1', + nameString: 'keys_2', isLeaf: true, children: [], path: '1.1', - fullName: 'keys:keys:1keys:keys:', + fullName: `keys-keys_2keys${delimiterMock}keys${delimiterMock}`, }, { - nameString: 'keys:2', + nameString: 'keys:1', isLeaf: true, children: [], path: '1.2', - fullName: 'keys:keys:2keys:keys:', + fullName: `keys-keys:1keys${delimiterMock}keys${delimiterMock}`, }, { - nameString: 'keys:3', + nameString: 'keys:2', isLeaf: true, children: [], path: '1.3', - fullName: 'keys:keys:3keys:keys:', + fullName: `keys-keys:2keys${delimiterMock}keys${delimiterMock}`, + }, + { + nameString: 'keys:3', + isLeaf: true, + children: [], + path: '1.4', + fullName: `keys-keys:3keys${delimiterMock}keys${delimiterMock}`, } ], - keyCount: 5, - keyApproximate: 50, + keyCount: 6, + keyApproximate: 60, path: '1', - fullName: 'keys:', + fullName: 'keys', }, { nameString: 'keys1', isLeaf: true, children: [], path: '2', - fullName: 'keys1keys:keys:', - }, - { - nameString: 'keys2', - isLeaf: true, - children: [], - path: '3', - fullName: 'keys2keys:keys:', + fullName: `keys1keys${delimiterMock}keys${delimiterMock}`, }, { nameString: 'test1', isLeaf: true, children: [], - path: '4', - fullName: 'test1keys:keys:', + path: '3', + fullName: `test1keys${delimiterMock}keys${delimiterMock}`, }, { nameString: 'test2', isLeaf: true, children: [], - path: '5', - fullName: 'test2keys:keys:', + path: '4', + fullName: `test2keys${delimiterMock}keys${delimiterMock}`, } ] diff --git a/redisinsight/ui/src/packages/redisearch/src/index.html b/redisinsight/ui/src/packages/redisearch/src/index.html index beaf8a57ef..344485e21a 100644 --- a/redisinsight/ui/src/packages/redisearch/src/index.html +++ b/redisinsight/ui/src/packages/redisearch/src/index.html @@ -4,7 +4,7 @@ - RediSearch plugin + Redis Query Engine plugin diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResultPage.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResultPage.tsx index 723a4981b4..eafba8d925 100644 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResultPage.tsx +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases-result/RedisCloudDatabasesResultPage.tsx @@ -173,7 +173,7 @@ const RedisCloudDatabasesResultPage = () => { { field: 'modules', className: 'column_modules', - name: 'Modules', + name: 'Capabilities', dataType: 'auto', align: 'left', width: '200px', diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabasesPage.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabasesPage.tsx index 3414b6a2d7..0cb1308dbd 100644 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabasesPage.tsx +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-databases/RedisCloudDatabasesPage.tsx @@ -48,6 +48,7 @@ const RedisCloudDatabasesPage = () => { } = useSelector(cloudSelector) const { data: userOAuthProfile } = useSelector(oauthCloudUserSelector) const currentAccountIdRef = useRef(userOAuthProfile?.id) + const ssoFlowRef = useRef(ssoFlow) setTitle('Redis Cloud Databases') @@ -60,7 +61,7 @@ const RedisCloudDatabasesPage = () => { }, []) useEffect(() => { - if (ssoFlow !== OAuthSocialAction.Import) return + if (ssoFlowRef.current !== OAuthSocialAction.Import) return if (!userOAuthProfile) { dispatch(resetDataRedisCloud()) @@ -73,7 +74,7 @@ const RedisCloudDatabasesPage = () => { history.push(Pages.redisCloudSubscriptions) })) } - }, [ssoFlow, userOAuthProfile]) + }, [userOAuthProfile]) useEffect(() => { if (instancesAdded.length) { @@ -221,7 +222,7 @@ const RedisCloudDatabasesPage = () => { { field: 'modules', className: 'column_modules', - name: 'Modules', + name: 'Capabilities', dataType: 'auto', align: 'left', width: '200px', diff --git a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptionsPage.tsx b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptionsPage.tsx index 8cc6ba271f..e89d91aa65 100644 --- a/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptionsPage.tsx +++ b/redisinsight/ui/src/pages/autodiscover-cloud/redis-cloud-subscriptions/RedisCloudSubscriptionsPage.tsx @@ -48,6 +48,7 @@ const RedisCloudSubscriptionsPage = () => { } = useSelector(cloudSelector) const { data: userOAuthProfile } = useSelector(oauthCloudUserSelector) const currentAccountIdRef = useRef(userOAuthProfile?.id) + const ssoFlowRef = useRef(ssoFlow) setTitle('Redis Cloud Subscriptions') @@ -58,7 +59,7 @@ const RedisCloudSubscriptionsPage = () => { }, []) useEffect(() => { - if (ssoFlow !== OAuthSocialAction.Import) return + if (ssoFlowRef.current !== OAuthSocialAction.Import) return if (!userOAuthProfile) { history.push(Pages.home) @@ -69,7 +70,7 @@ const RedisCloudSubscriptionsPage = () => { dispatch(fetchSubscriptionsRedisCloud(null, true)) currentAccountIdRef.current = userOAuthProfile?.id } - }, [ssoFlow, userOAuthProfile]) + }, [userOAuthProfile]) useEffect(() => { if (instancesLoaded) { 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 ab955fb949..61da55dbd3 100644 --- a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx @@ -279,7 +279,7 @@ const KeyList = forwardRef((props: Props, ref) => { minWidth: 94, truncateText: true, render: (cellData: string) => ( - + ) }, { diff --git a/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.spec.tsx index 8ee2f086ea..6e2a14ecc4 100644 --- a/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.spec.tsx @@ -13,15 +13,15 @@ describe('KeyRowName', () => { expect(render()).toBeTruthy() }) - it('should render Loading if no nameString', () => { - const { queryByTestId } = render() + it('should render Loading if no nameString and shortName', () => { + const { queryByTestId } = render() expect(queryByTestId(loadingTestId)).toBeInTheDocument() }) it('content should be no more than 200 symbols', () => { const longName = Array.from({ length: 250 }, () => '1').join('') - const { queryByTestId } = render() + const { queryByTestId } = render() expect(queryByTestId(loadingTestId)).not.toBeInTheDocument() expect(queryByTestId(`key-${longName}`)).toHaveTextContent(longName.slice(0, 200)) diff --git a/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx b/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx index 5dfc1df372..3e18f9bd87 100644 --- a/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-row-name/KeyRowName.tsx @@ -11,12 +11,13 @@ import styles from './styles.module.scss' export interface Props { nameString: Maybe + shortName: Maybe } const KeyRowName = (props: Props) => { - const { nameString } = props + const { nameString, shortName } = props - if (isUndefined(nameString)) { + if (isUndefined(shortName)) { return ( { } // Better to cut the long string, because it could affect virtual scroll performance - const nameContent = replaceSpaces(nameString?.substring?.(0, 200)) + const nameContent = replaceSpaces(shortName?.substring?.(0, 200)) const nameTooltipContent = formatLongName(nameString) return (
-
+
{ const { instanceId } = useParams<{ instanceId: string }>() const { openNodes } = useSelector(appContextBrowserTree) - const { treeViewDelimiter: delimiter = '', treeViewSort: sorting } = useSelector(appContextDbConfig) + const { treeViewDelimiter, treeViewSort: sorting } = useSelector(appContextDbConfig) const { nameString: selectedKeyName = null } = useSelector(selectedKeyDataSelector) ?? {} const [statusOpen, setStatusOpen] = useState(openNodes) @@ -67,6 +68,12 @@ const KeyTree = forwardRef((props: Props, ref) => { const [firstDataLoaded, setFirstDataLoaded] = useState(!!keysState.keys.length) const [items, setItems] = useState(parseKeyNames(keysState.keys ?? [])) + // escape regexp symbols and join and transform to regexp + const delimiters = comboBoxToArray(treeViewDelimiter) + const delimiterPattern = delimiters + .map(escapeRegExp) + .join('|') + const dispatch = useDispatch() useImperativeHandle(ref, () => ({ @@ -86,8 +93,8 @@ const KeyTree = forwardRef((props: Props, ref) => { // open all parents for selected key const openSelectedKey = (selectedKeyName: Nullable = '') => { if (selectedKeyName) { - const parts = selectedKeyName.split(delimiter) - const parents = parts.map((_, index) => parts.slice(0, index + 1).join(delimiter) + delimiter) + const parts = selectedKeyName.split(delimiterPattern) + const parents = parts.map((_, index) => parts.slice(0, index + 1).join(delimiterPattern) + delimiterPattern) // remove key name from parents parents.pop() @@ -110,7 +117,7 @@ const KeyTree = forwardRef((props: Props, ref) => { } setItems(parseKeyNames(keysState.keys)) - }, [keysState.lastRefreshTime, delimiter, sorting]) + }, [keysState.lastRefreshTime, delimiterPattern, sorting]) useEffect(() => { openSelectedKey(selectedKeyName) @@ -188,7 +195,8 @@ const KeyTree = forwardRef((props: Props, ref) => { () @@ -23,7 +24,6 @@ let store: typeof mockedStore const APPLY_BTN = 'tree-view-apply-btn' const TREE_SETTINGS_TRIGGER_BTN = 'tree-view-settings-btn' const SORTING_SELECT = 'tree-view-sorting-select' -const DELIMITER_INPUT = 'tree-view-delimiter-input' const SORTING_DESC_ITEM = 'tree-view-sorting-item-DESC' beforeEach(() => { @@ -63,7 +63,10 @@ describe('KeyTreeDelimiter', () => { }) await waitForEuiPopoverVisible() - expect(screen.getByTestId(DELIMITER_INPUT)).toBeInTheDocument() + const comboboxInput = document + .querySelector('[data-testid="delimiter-combobox"] [data-test-subj="comboBoxSearchInput"]') as HTMLInputElement + + expect(comboboxInput).toBeInTheDocument() expect(screen.getByTestId(SORTING_SELECT)).toBeInTheDocument() }) @@ -79,7 +82,21 @@ describe('KeyTreeDelimiter', () => { await waitForEuiPopoverVisible() - fireEvent.change(screen.getByTestId(DELIMITER_INPUT), { target: { value } }) + const comboboxInput = document + .querySelector('[data-testid="delimiter-combobox"] [data-test-subj="comboBoxSearchInput"]') as HTMLInputElement + + fireEvent.change( + comboboxInput, + { target: { value } } + ) + + fireEvent.keyDown(comboboxInput, { key: 'Enter', code: 13, charCode: 13 }) + + const containerLabels = document.querySelector('[data-test-subj="comboBoxInput"]')! + expect(containerLabels.querySelector(`[title="${value}"]`)).toBeInTheDocument() + + fireEvent.click(containerLabels.querySelector('[title^="Remove :"]')!) + expect(containerLabels.querySelector('[title=":"]')).not.toBeInTheDocument() await act(() => { fireEvent.click(screen.getByTestId(SORTING_SELECT)) @@ -89,14 +106,16 @@ describe('KeyTreeDelimiter', () => { await act(() => { fireEvent.click(screen.getByTestId(SORTING_DESC_ITEM)) - }) + }); + + (sendEventTelemetry as jest.Mock).mockRestore() await act(() => { fireEvent.click(screen.getByTestId(APPLY_BTN)) }) const expectedActions = [ - setBrowserTreeDelimiter(value), + setBrowserTreeDelimiter([{ label: value }]), resetBrowserTree(), setBrowserTreeSort(SortOrder.DESC), resetBrowserTree(), @@ -110,8 +129,8 @@ describe('KeyTreeDelimiter', () => { event: TelemetryEvent.TREE_VIEW_DELIMITER_CHANGED, eventData: { databaseId: INSTANCE_ID_MOCK, - from: DEFAULT_DELIMITER, - to: value, + from: comboBoxToArray([DEFAULT_DELIMITER]), + to: [value], } }) @@ -127,7 +146,6 @@ describe('KeyTreeDelimiter', () => { }) it('"setBrowserTreeDelimiter" should be called with DEFAULT_DELIMITER after Apply change with empty input', async () => { - const value = '' render() await act(() => { @@ -136,14 +154,16 @@ describe('KeyTreeDelimiter', () => { await waitForEuiPopoverVisible() - fireEvent.change(screen.getByTestId(DELIMITER_INPUT), { target: { value } }) + const containerLabels = document.querySelector('[data-test-subj="comboBoxInput"]')! + fireEvent.click(containerLabels.querySelector('[title^="Remove :"]')!) + expect(containerLabels.querySelector('[title=":"]')).not.toBeInTheDocument() await act(() => { fireEvent.click(screen.getByTestId(APPLY_BTN)) }) const expectedActions = [ - setBrowserTreeDelimiter(DEFAULT_DELIMITER), + setBrowserTreeDelimiter([DEFAULT_DELIMITER]), resetBrowserTree(), ] diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx index 78bfce85e3..58a000e626 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/KeyTreeSettings.tsx @@ -2,7 +2,19 @@ import React, { useCallback, useEffect, useState } from 'react' import cx from 'classnames' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' -import { EuiButton, EuiButtonIcon, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPopover, EuiSuperSelect, EuiText } from '@elastic/eui' +import { + EuiButton, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPopover, + EuiSuperSelect, + EuiText, + EuiComboBox, + EuiComboBoxOptionOption, +} from '@elastic/eui' +import { isEqual } from 'lodash' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { DEFAULT_DELIMITER, DEFAULT_TREE_SORTING, SortOrder } from 'uiSrc/constants' @@ -13,13 +25,13 @@ import { setBrowserTreeSort, } from 'uiSrc/slices/app/context' import TreeViewSort from 'uiSrc/assets/img/browser/treeViewSort.svg?react' +import { comboBoxToArray } from 'uiSrc/utils' import styles from './styles.module.scss' export interface Props { loading: boolean } -const MAX_DELIMITER_LENGTH = 5 const sortOptions = [SortOrder.ASC, SortOrder.DESC].map((value) => ({ value, inputDisplay: ( @@ -29,9 +41,12 @@ const sortOptions = [SortOrder.ASC, SortOrder.DESC].map((value) => ({ const KeyTreeSettings = ({ loading }: Props) => { const { instanceId = '' } = useParams<{ instanceId: string }>() - const { treeViewDelimiter = '', treeViewSort = DEFAULT_TREE_SORTING } = useSelector(appContextDbConfig) + const { + treeViewDelimiter = [DEFAULT_DELIMITER], + treeViewSort = DEFAULT_TREE_SORTING, + } = useSelector(appContextDbConfig) const [sorting, setSorting] = useState(treeViewSort) - const [delimiter, setDelimiter] = useState(treeViewDelimiter) + const [delimiters, setDelimiters] = useState(treeViewDelimiter) const [isPopoverOpen, setIsPopoverOpen] = useState(false) @@ -42,7 +57,7 @@ const KeyTreeSettings = ({ loading }: Props) => { }, [treeViewSort]) useEffect(() => { - setDelimiter(treeViewDelimiter) + setDelimiters(treeViewDelimiter) }, [treeViewDelimiter]) const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen) @@ -55,7 +70,7 @@ const KeyTreeSettings = ({ loading }: Props) => { const resetStates = useCallback(() => { setSorting(treeViewSort) - setDelimiter(treeViewDelimiter) + setDelimiters(treeViewDelimiter) }, [treeViewSort, treeViewDelimiter]) const button = ( @@ -70,14 +85,16 @@ const KeyTreeSettings = ({ loading }: Props) => { ) const handleApply = () => { - if (delimiter !== treeViewDelimiter) { - dispatch(setBrowserTreeDelimiter(delimiter || DEFAULT_DELIMITER)) + if (!isEqual(delimiters, treeViewDelimiter)) { + const delimitersValue = delimiters.length ? delimiters : [DEFAULT_DELIMITER] + + dispatch(setBrowserTreeDelimiter(delimitersValue)) sendEventTelemetry({ event: TelemetryEvent.TREE_VIEW_DELIMITER_CHANGED, eventData: { databaseId: instanceId, - from: treeViewDelimiter, - to: delimiter || DEFAULT_DELIMITER + from: comboBoxToArray(treeViewDelimiter), + to: comboBoxToArray(delimitersValue) } }) @@ -122,14 +139,16 @@ const KeyTreeSettings = ({ loading }: Props) => {
Delimiter
- setDelimiter(e.target.value)} - aria-label="Title" - maxLength={MAX_DELIMITER_LENGTH} - data-testid="tree-view-delimiter-input" + delimiter=" " + selectedOptions={delimiters} + onCreateOption={(del) => setDelimiters([...delimiters, { label: del }])} + onChange={(selectedOptions) => setDelimiters(selectedOptions)} + className={styles.combobox} + data-testid="delimiter-combobox" />
diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss index 9118c31517..6d730a7be9 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeSettings/styles.module.scss @@ -2,7 +2,8 @@ margin-left: 8px; margin-top: 1px; - :global(.euiPopover), .anchorWrapper { + :global(.euiPopover), + .anchorWrapper { height: 100%; } } @@ -18,7 +19,6 @@ } .popoverWrapper { - height: 162px; width: 300px; padding: 14px 16px !important; border: none !important; @@ -32,17 +32,18 @@ } } .euiFormControlLayout { - height: 26px !important; + min-height: 26px !important; + height: auto !important; width: auto; } } - .input, .select { + .select { width: 188px; height: 24px !important; font-size: 12px; border-radius: 4px; - background-color: var(--browserViewTypePassive) !important; + background-color: var(--euiColorEmptyShade) !important; } } @@ -50,7 +51,7 @@ display: flex; width: 66px; font-size: 12px; - color: var(--euiTextSubduedColor)!important; + color: var(--euiTextSubduedColor) !important; } .title { @@ -82,3 +83,23 @@ margin-left: 8px; } } + +.combobox { + padding-bottom: 6px; + + :global(.euiComboBox__input) { + height: 30px !important; + } + :global(.euiComboBox__inputWrap) { + max-height: 200px; + overflow: auto; + max-width: 210px !important; + padding: 2px 4px !important; + min-height: 34px !important; + border-radius: 4px !important; + } + :global(.euiComboBoxPlaceholder) { + height: 30px !important; + line-height: 30px; + } +} diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx index b4f5947fbb..b31520116c 100644 --- a/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/VirtualTree.tsx @@ -13,12 +13,12 @@ import { bufferToString, Maybe, Nullable } from 'uiSrc/utils' import { useDisposableWebworker } from 'uiSrc/services' import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' import { ThemeContext } from 'uiSrc/contexts/themeContext' -import { DEFAULT_DELIMITER, DEFAULT_TREE_SORTING, KeyTypes, ModulesKeyTypes, SortOrder, Theme } from 'uiSrc/constants' +import { DEFAULT_TREE_SORTING, KeyTypes, ModulesKeyTypes, SortOrder, Theme } from 'uiSrc/constants' import KeyLightSVG from 'uiSrc/assets/img/sidebar/browser.svg' import KeyDarkSVG from 'uiSrc/assets/img/sidebar/browser_active.svg' import { RedisResponseBuffer, RedisString } from 'uiSrc/slices/interfaces' import { fetchKeysMetadataTree } from 'uiSrc/slices/browser/keys' -import { GetKeyInfoResponse } from 'apiSrc/modules/browser/dto' +import { GetKeyInfoResponse } from 'apiSrc/modules/browser/keys/dto' import { Node } from './components/Node' import { NodeMeta, TreeData, TreeNode } from './interfaces' @@ -27,7 +27,8 @@ import styles from './styles.module.scss' export interface Props { items: IKeyPropTypes[] - delimiter?: string + delimiterPattern: string + delimiters: string[] loadingIcon?: string loading: boolean deleting: boolean @@ -52,7 +53,8 @@ export const KEYS = 'keys' const VirtualTree = (props: Props) => { const { items, - delimiter = DEFAULT_DELIMITER, + delimiterPattern, + delimiters, loadingIcon = 'empty', statusOpen = {}, statusSelected, @@ -103,13 +105,13 @@ const VirtualTree = (props: Props) => { nodes.current = [] elements.current = {} rerender({}) - runWebworker?.({ items: [], delimiter, sorting }) + runWebworker?.({ items: [], delimiterPattern, delimiters, sorting }) return } setConstructingTree(true) - runWebworker?.({ items, delimiter, sorting }) - }, [items, delimiter]) + runWebworker?.({ items, delimiterPattern, delimiters, sorting }) + }, [items, delimiterPattern]) const handleUpdateSelected = useCallback((name: RedisString) => { onStatusSelected?.(name) @@ -154,7 +156,7 @@ const VirtualTree = (props: Props) => { ) => { const items = loadedItems.map(formatItem) - items.forEach((item) => updateNodeByPath(item.path, item)) + items.forEach((item: any) => updateNodeByPath(item.path, item)) rerender({}) } @@ -190,7 +192,8 @@ const VirtualTree = (props: Props) => { size: node.size, type: node.type, fullName: node.fullName, - shortName: node.nameString?.split(delimiter).pop(), + shortName: node.nameString?.split(new RegExp(delimiterPattern, 'g')).pop(), + delimiters, nestingLevel, deleting, path: node.path, diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx index 597fe86359..15f96473ad 100644 --- a/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/Node.tsx @@ -47,6 +47,7 @@ const Node = ({ nameString, keyApproximate, isSelected, + delimiters = [], getMetadata, onDelete, onDeleteClicked, @@ -54,6 +55,8 @@ const Node = ({ updateStatusSelected, } = data + const delimiterView = delimiters.length === 1 ? delimiters[0] : '-' + const [deletePopoverId, setDeletePopoverId] = useState>(undefined) useEffect(() => { @@ -127,7 +130,7 @@ const Node = ({ const Leaf = () => ( <> - + - {`${fullName}*`} -
+
+ {`${fullName + delimiterView}*`} + {delimiters.length > 1 && ( + + {delimiters.map((delimiter) => ( + {delimiter} + ))} + + )} +
{`${keyCount} key(s) (${Math.round(keyApproximate * 100) / 100}%)`} ) diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss index dd3be8344d..f9093b0fa6 100644 --- a/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/components/Node/styles.module.scss @@ -140,3 +140,29 @@ .deletePopover { max-width: 400px !important; } + +.folderTooltipHeader { + display: flex; + flex-wrap: wrap; + align-items: center; + word-break: break-all; +} + +.delimiters { + display: inline-flex; + flex-wrap: wrap; +} + +.folderPattern { + font-weight: bold; + margin-right: 4px; + white-space: normal; +} + +.delimiter { + margin-bottom: 2px; + padding: 2px 5px; + margin-right: 4px; + border-radius: 2px; + background-color: var(--euiColorLightestShade); +} diff --git a/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts b/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts index 119c843d29..0f54655e27 100644 --- a/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts +++ b/redisinsight/ui/src/pages/browser/components/virtual-tree/interfaces.ts @@ -50,6 +50,7 @@ export interface TreeData extends FixedSizeNodeData { nestingLevel: number deleting: boolean isSelected: boolean + delimiters: string[] children?: TreeData[] updateStatusOpen: (fullName: string, value: boolean) => void updateStatusSelected: (key: RedisString) => void diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.spec.tsx index 1976583580..c651dc1861 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.spec.tsx @@ -1,11 +1,20 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { cloneDeep } from 'lodash' -import { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils' +import { cleanup, mockedStore, render, screen, act } from 'uiSrc/utils/test-utils' import { defaultSelectedKeyAction, setSelectedKeyRefreshDisabled } from 'uiSrc/slices/browser/keys' +import { stringToBuffer } from 'uiSrc/utils' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { apiService } from 'uiSrc/services' +import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/instances/instancesHandlers' import KeyDetails, { Props as KeyDetailsProps } from './KeyDetails' +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + const mockedProps = mock() let store: typeof mockedStore @@ -47,4 +56,23 @@ describe('KeyDetails', () => { expect(screen.getByTestId('select-key-message')).toBeInTheDocument() }) + + it('should call proper telemetry after open key details', async () => { + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock) + apiService.post = jest.fn().mockResolvedValueOnce({ status: 200, data: { length: 1, type: 'hash' } }) + + await act(async () => { + render() + }) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.BROWSER_KEY_VALUE_VIEWED, + eventData: { + databaseId: INSTANCE_ID_MOCK, + length: 1, + keyType: 'hash' + } + }) + }) }) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.tsx index 8df9b9eac8..db99595ce4 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/KeyDetails.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react' -import { isNull, isUndefined } from 'lodash' +import { isNull } from 'lodash' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import cx from 'classnames' @@ -45,33 +45,34 @@ const KeyDetails = (props: Props) => { const { viewType } = useSelector(keysSelector) const { loading, error = '', data } = useSelector(selectedKeySelector) const isKeySelected = !isNull(useSelector(selectedKeyDataSelector)) - const { type: keyType, length: keyLength } = useSelector(selectedKeyDataSelector) ?? { - type: KeyTypes.String, - } + const { type: keyType } = useSelector(selectedKeyDataSelector) ?? { type: KeyTypes.String } const dispatch = useDispatch() useEffect(() => { - if (keyProp === null) { - return - } - if (keyProp?.data) { - sendEventTelemetry({ - event: getBasedOnViewTypeEvent( - viewType, - TelemetryEvent.BROWSER_KEY_VALUE_VIEWED, - TelemetryEvent.TREE_VIEW_KEY_VALUE_VIEWED - ), - eventData: { - keyType, - databaseId: instanceId, - length: keyLength, - } - }) - } - // Restore key details from context in future - // (selectedKey.data?.name !== keyProp) - dispatch(fetchKeyInfo(keyProp)) + if (keyProp === null) return + + dispatch(fetchKeyInfo( + keyProp, + undefined, + (data) => { + if (!data) return + + sendEventTelemetry({ + event: getBasedOnViewTypeEvent( + viewType, + TelemetryEvent.BROWSER_KEY_VALUE_VIEWED, + TelemetryEvent.TREE_VIEW_KEY_VALUE_VIEWED + ), + eventData: { + keyType: data.type, + databaseId: instanceId, + length: data.length, + } + }) + } + )) + dispatch(setSelectedKeyRefreshDisabled(false)) }, [keyProp]) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.tsx index dce38fe0ac..d9601fcd69 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/hash-details/HashDetails.tsx @@ -68,7 +68,7 @@ const HashDetails = (props: Props) => { className={styles.showTtlCheckbox} checked={showTtl} onChange={(e) => handleSelectShow(e.target.checked)} - data-testId="test-check-ttl" + data-testid="test-check-ttl" /> { const [expandedRows, setExpandedRows] = useState>(new Set()) + const updatedData = parseJsonData(data) + useEffect(() => { setExpandedRows(new Set()) }, [nameString]) @@ -96,11 +99,11 @@ const RejsonDetailsWrapper = (props: Props) => { data-testid="progress-key-json" /> )} - {!isUndefined(data) && ( + {!isUndefined(updatedData) && ( { aria-label="Cancel add" className={styles.declineBtn} onClick={onCancel} + data-testid="cancel-edit-btn" /> { const data = { ...item, parentPath } if (['array', 'object'].includes(item.type)) return renderJSONObject(data, item.type) + return renderScalar(data) } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-object/RejsonObject.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-object/RejsonObject.tsx index 3ecd1daca9..ef8935bd6c 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-object/RejsonObject.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-object/RejsonObject.tsx @@ -79,7 +79,11 @@ const RejsonObject = (props: JSONObjectProps) => { const onClickEditEntireObject = () => { handleFetchVisualisationResults(path, true).then((data: REJSONResponse) => { setEditEntireObject(true) - setValueOfEntireObject(typeof data.data === 'object' ? JSON.stringify(data.data, undefined, 4) : data.data) + setValueOfEntireObject(typeof data.data === 'object' ? JSON.stringify(data.data, (_key, value) => ( + typeof value === 'bigint' + ? value.toString() + : value + ), 4) : data.data) }) } diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-scalar/RejsonScalar.spec.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-scalar/RejsonScalar.spec.tsx index 55d6a2ed32..fd9184f135 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-scalar/RejsonScalar.spec.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-scalar/RejsonScalar.spec.tsx @@ -101,4 +101,44 @@ describe('JSONScalar', () => { expect(handleEdit).not.toBeCalled() }) + + it('should render BigInt value when root', () => { + render() + + expect(screen.getByText('1188950299261208742')).toBeInTheDocument() + }) + + it('should render BigInt value when not root', () => { + render() + + expect(screen.getByTestId('json-scalar-value')).toHaveTextContent('1188950299261208742') + }) + + it('should render regular number without n suffix', () => { + render() + + expect(screen.getByText('123')).toBeInTheDocument() + }) + + it('should render string value with quotes', () => { + render() + + expect(screen.getByText('"test"')).toBeInTheDocument() + }) }) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-scalar/RejsonScalar.tsx b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-scalar/RejsonScalar.tsx index 43994fcbf7..e0b4c7aefe 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-scalar/RejsonScalar.tsx +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/rejson-scalar/RejsonScalar.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react' import { useDispatch } from 'react-redux' import cx from 'classnames' -import { isNull, isString } from 'lodash' import { setReJSONDataAction } from 'uiSrc/slices/browser/rejson' import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor' import PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete' @@ -10,7 +9,7 @@ import { bufferToString, createDeleteFieldHeader, createDeleteFieldMessage, Null import FieldMessage from 'uiSrc/components/field-message/FieldMessage' import { JSONScalarProps } from '../interfaces' -import { generatePath, getClassNameByValue, isValidJSON } from '../utils' +import { generatePath, getClassNameByValue, isValidJSON, stringifyScalarValue } from '../utils' import { JSONErrors } from '../constants' import styles from '../styles.module.scss' @@ -36,7 +35,7 @@ const RejsonScalar = (props: JSONScalarProps) => { const dispatch = useDispatch() useEffect(() => { - setChangedValue(isString(value) ? `"${value}"` : isNull(value) ? 'null' : value) + setChangedValue(stringifyScalarValue(value)) }, [value]) const onDeclineChanges = () => { diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/utils/utils.spec.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/utils/utils.spec.ts index eb600195d0..2c2522d003 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/utils/utils.spec.ts +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/utils/utils.spec.ts @@ -1,4 +1,4 @@ -import { generatePath, getBrackets, isRealArray, isRealObject, isScalar, isValidKey, wrapPath } from './utils' +import { generatePath, getBrackets, isRealArray, isRealObject, isScalar, isValidKey, parseJsonData, parseValue, stringifyScalarValue, wrapPath } from './utils' import { ObjectTypes } from '../interfaces' describe('JSONUtils', () => { @@ -79,4 +79,129 @@ describe('JSONUtils', () => { expect(isValidKey('"')).toBeFalsy() }) }) + + describe('JSON Parsing Utils', () => { + const bigintAsString = '1188950299261208742' + const scientificNotation = 1.2345678901234568e+29 + + describe('parseValue', () => { + it('should handle non-string values', () => { + expect(parseValue(123)).toBe(123) + expect(parseValue(null)).toBe(null) + expect(parseValue(undefined)).toBe(undefined) + }) + + it('should parse typed integer values', () => { + const result = parseValue(bigintAsString, 'integer') + expect(typeof result).toBe('bigint') + expect(result.toString()).toBe(bigintAsString) + }) + + it('should parse regular numbers as numbers, not bigints', () => { + const result = parseValue('42', 'integer') + expect(typeof result).toBe('number') + expect(result).toBe(42) + }) + + it('should handle string type with quotes', () => { + expect(parseValue('"test"', 'string')).toBe('test') + expect(parseValue('test', 'string')).toBe('test') + }) + + it('should parse boolean values', () => { + expect(parseValue('true', 'boolean')).toBe(true) + expect(parseValue('false', 'boolean')).toBe(false) + }) + + it('should parse null values', () => { + expect(parseValue('null', 'null')).toBe(null) + }) + + it('should parse JSON objects without type', () => { + const input = `{"value": ${bigintAsString}, "text": "test"}` + const result = parseValue(input) + expect(typeof result.value).toBe('bigint') + expect(result.value.toString()).toBe(bigintAsString) + expect(result.text).toBe('test') + }) + + it('should parse JSON arrays without type', () => { + const input = `[${bigintAsString}, "test"]` + const result = parseValue(input) + expect(typeof result[0]).toBe('bigint') + expect(result[0].toString()).toBe(bigintAsString) + expect(result[1]).toBe('test') + }) + + it('should handle extremely large integers and maintain scientific notation', () => { + const resultFromString = parseValue(`'${scientificNotation}'`, 'integer') + expect(resultFromString).toBe(`'${scientificNotation}'`) + + const resultFromInt = parseValue(scientificNotation, 'integer') + expect(resultFromInt).toBe(scientificNotation) + + // Also test parsing as part of JSON + const jsonWithLargeInt = `{"value": ${scientificNotation}}` + const parsedJson = parseValue(jsonWithLargeInt) + expect(parsedJson.value).toBe(scientificNotation) + }) + }) + + describe('parseJsonData', () => { + it('should handle null or undefined data', () => { + expect(parseJsonData(null)).toBe(null) + expect(parseJsonData(undefined)).toBe(undefined) + }) + + it('should parse array of typed values', () => { + const input = [ + { type: 'string', value: '"John"' }, + { type: 'integer', value: bigintAsString } + ] + const result = parseJsonData(input) + + expect(result[0].value).toBe('John') + expect(typeof result[1].value).toBe('bigint') + expect(result[1].value.toString()).toBe(bigintAsString) + }) + + it('should preserve non-typed array items', () => { + const input = [ + { value: '"John"' }, + { someOtherProp: 'test' } + ] + const result = parseJsonData(input) + + expect(result[0].value).toBe('"John"') + expect(result[1].someOtherProp).toBe('test') + }) + }) + }) + + describe('stringifyScalarValue', () => { + it('should handle bigint values', () => { + const bigIntValue = BigInt('9007199254740991') + expect(stringifyScalarValue(bigIntValue)).toBe('9007199254740991') + }) + + it('should wrap string values in quotes', () => { + expect(stringifyScalarValue('hello')).toBe('"hello"') + expect(stringifyScalarValue('')).toBe('""') + }) + + it('should convert null to "null" string', () => { + expect(stringifyScalarValue(null as any)).toBe('null') + }) + + it('should convert numbers to string representation', () => { + expect(stringifyScalarValue(42)).toBe('42') + expect(stringifyScalarValue(-123.456)).toBe('-123.456') + expect(stringifyScalarValue(0)).toBe('0') + }) + + it('should convert boolean values to string representation', () => { + expect(stringifyScalarValue(true)).toBe('true') + expect(stringifyScalarValue(false)).toBe('false') + }) + }) }) diff --git a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/utils/utils.ts b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/utils/utils.ts index 6a8dca4371..31054676c7 100644 --- a/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/utils/utils.ts +++ b/redisinsight/ui/src/pages/browser/modules/key-details/components/rejson-details/utils/utils.ts @@ -1,4 +1,5 @@ -import { isArray } from 'lodash' +import { isArray, isNull, isString } from 'lodash' +import JSONBigInt from 'json-bigint' import { JSONScalarValue, ObjectTypes } from '../interfaces' import styles from '../styles.module.scss' @@ -11,7 +12,7 @@ enum ClassNames { } export function isScalar(x: JSONScalarValue) { - return ['string', 'number', 'boolean'].indexOf(typeof x) !== -1 || x === null + return ['string', 'number', 'boolean', 'bigint'].indexOf(typeof x) !== -1 || x === null } export const isValidJSON = (value: string): boolean => { @@ -61,3 +62,107 @@ export const getBrackets = (type: string, position: 'start' | 'end' = 'start') = } export const isValidKey = (key: string): boolean => /^"([^"\\]|\\.)*"$/.test(key) + +const JSONParser = JSONBigInt({ + useNativeBigInt: true, + strict: false, + alwaysParseAsBig: false, + protoAction: 'preserve', + constructorAction: 'preserve' +}) + +const safeJSONParse = (value: string) => { + // Pre-process the string to handle scientific notation + const preprocessed = value.replace(/-?\d+\.?\d*e[+-]?\d+/gi, (match) => + // Wrap scientific notation numbers in quotes to prevent BigInt conversion + `"${match}"`) + + return JSONParser.parse(preprocessed, (_key: string, value: any) => { + // Convert quoted scientific notation back to numbers + if (typeof value === 'string' && /^-?\d+\.?\d*e[+-]?\d+$/i.test(value)) { + return Number(value) + } + return value + }) +} + +export const parseValue = (value: any, type?: string): any => { + try { + if (typeof value !== 'string' || !value) { + return value + } + + if (type) { + switch (type) { + case 'integer': { + const num = BigInt(value) + return num > Number.MAX_SAFE_INTEGER ? num : Number(value) + } + case 'number': + return Number(value) + case 'boolean': + return value === 'true' + case 'null': + return null + case 'string': + if (value.startsWith('"') && value.endsWith('"')) { + return value.slice(1, -1) + } + return value + default: + return value + } + } + + const parsed = safeJSONParse(value) + + if (typeof parsed === 'object' && parsed !== null) { + if (Array.isArray(parsed)) { + return parsed.map((val) => parseValue(val)) + } + const result: { [key: string]: any } = {} + Object.entries(parsed).forEach(([key, val]) => { + result[key] = parseValue(val) + }) + return result + } + return parsed + } catch (e) { + try { + return JSON.parse(value) + } catch (error) { + return value + } + } +} + +export const parseJsonData = (data: any) => { + if (!data) { + return data + } + try { + if (data && Array.isArray(data)) { + return data.map((item: { type?: string; value?: any }) => ({ + ...item, + value: item.type && item.value ? parseValue(item.value, item.type) : item.value + })) + } + + return parseValue(data) + } catch (e) { + return data + } +} + +export const stringifyScalarValue = (value: string | number | boolean | bigint): string => { + if (typeof value === 'bigint') { + return value.toString() + } + if (isString(value)) { + return `"${value}"` + } + if (isNull(value)) { + return 'null' + } + return String(value) +} diff --git a/redisinsight/ui/src/pages/database-analysis/components/header/Header.tsx b/redisinsight/ui/src/pages/database-analysis/components/header/Header.tsx index 771aa2d68b..7a764e0024 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/header/Header.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/header/Header.tsx @@ -20,10 +20,11 @@ import { appContextDbConfig } from 'uiSrc/slices/app/context' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { ConnectionType } from 'uiSrc/slices/interfaces' import AnalyticsTabs from 'uiSrc/components/analytics-tabs' -import { Nullable, getDbIndex } from 'uiSrc/utils' +import { Nullable, comboBoxToArray, getDbIndex } from 'uiSrc/utils' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { ANALYZE_CLUSTER_TOOLTIP_MESSAGE, ANALYZE_TOOLTIP_MESSAGE } from 'uiSrc/constants/recommendations' import { FormatedDate } from 'uiSrc/components' +import { DEFAULT_DELIMITER } from 'uiSrc/constants' import { ShortDatabaseAnalysis } from 'apiSrc/modules/database-analysis/models' import { AnalysisProgress } from 'apiSrc/modules/database-analysis/models/analysis-progress' @@ -50,7 +51,7 @@ const Header = (props: Props) => { const { instanceId } = useParams<{ instanceId: string }>() const dispatch = useDispatch() - const { treeViewDelimiter: delimiter = '' } = useSelector(appContextDbConfig) + const { treeViewDelimiter = [DEFAULT_DELIMITER] } = useSelector(appContextDbConfig) const analysisOptions: EuiSuperSelectOption[] = items.map((item) => { const { createdAt, id, db } = item @@ -76,7 +77,7 @@ const Header = (props: Props) => { provider, } }) - dispatch(createNewAnalysis(instanceId, delimiter)) + dispatch(createNewAnalysis(instanceId, comboBoxToArray(treeViewDelimiter))) } return ( diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-keys/Table.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-keys/Table.tsx index 8669e72d68..c6ce96a0b6 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/top-keys/Table.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/top-keys/Table.tsx @@ -65,7 +65,7 @@ const Table = (props: Props) => { const handleRedirect = (name: string) => { dispatch(changeSearchMode(SearchMode.Pattern)) - dispatch(setBrowserTreeDelimiter(delimiter)) + dispatch(setBrowserTreeDelimiter([{ label: delimiter }])) dispatch(setFilter(null)) dispatch(setSearchMatch(name, SearchMode.Pattern)) dispatch(resetKeysData(SearchMode.Pattern)) diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/Table.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/Table.tsx index b87cbdfce7..de41a09172 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/Table.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/Table.tsx @@ -63,7 +63,7 @@ const NameSpacesTable = (props: Props) => { const handleRedirect = (nsp: string, filter: string) => { dispatch(changeSearchMode(SearchMode.Pattern)) - dispatch(setBrowserTreeDelimiter(delimiter)) + dispatch(setBrowserTreeDelimiter([{ label: delimiter }])) dispatch(setFilter(filter)) dispatch(setSearchMatch(`${nsp}${delimiter}*`, SearchMode.Pattern)) dispatch(resetKeysData(SearchMode.Pattern)) diff --git a/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx b/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx index 9de868da7a..efda01bd42 100644 --- a/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx +++ b/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx @@ -293,7 +293,7 @@ const DatabasesListWrapper = ({ width, onEditInstance, editedInstance, onDeleteI { field: 'modules', className: styles.columnModules, - name: 'Modules', + name: 'Capabilities', width: '30%', dataType: 'string', render: (_cellData, { modules = [], isRediStack }: Instance) => ( diff --git a/redisinsight/ui/src/pages/home/components/form/DbInfo.tsx b/redisinsight/ui/src/pages/home/components/form/DbInfo.tsx index cd21a23b42..e618c65eab 100644 --- a/redisinsight/ui/src/pages/home/components/form/DbInfo.tsx +++ b/redisinsight/ui/src/pages/home/components/form/DbInfo.tsx @@ -130,7 +130,7 @@ const DbInfo = (props: Props) => { className={styles.dbInfoModulesLabel} label={( - Modules: + Capabilities: )} /> diff --git a/redisinsight/ui/src/pages/rdi/home/RdiPage.spec.tsx b/redisinsight/ui/src/pages/rdi/home/RdiPage.spec.tsx index afc8a597c8..5fadc1fecc 100644 --- a/redisinsight/ui/src/pages/rdi/home/RdiPage.spec.tsx +++ b/redisinsight/ui/src/pages/rdi/home/RdiPage.spec.tsx @@ -250,6 +250,7 @@ describe('RdiPage', () => { expect(createInstanceAction).toBeCalledWith( { name: 'name', url: 'url', username: 'username', password: 'password' }, + expect.any(Function), expect.any(Function) ) }) diff --git a/redisinsight/ui/src/pages/rdi/home/RdiPage.tsx b/redisinsight/ui/src/pages/rdi/home/RdiPage.tsx index 319467cd65..1af35e3904 100644 --- a/redisinsight/ui/src/pages/rdi/home/RdiPage.tsx +++ b/redisinsight/ui/src/pages/rdi/home/RdiPage.tsx @@ -18,6 +18,7 @@ import { } from 'uiSrc/telemetry' import HomePageTemplate from 'uiSrc/templates/home-page-template' import { setTitle } from 'uiSrc/utils' +import { Rdi as RdiInstanceResponse } from 'apiSrc/modules/rdi/models/rdi' import EmptyMessage from './empty-message/EmptyMessage' import ConnectionForm from './connection-form/ConnectionForm' import RdiHeader from './header/RdiHeader' @@ -62,7 +63,26 @@ const RdiPage = () => { if (editInstance) { dispatch(editInstanceAction(editInstance.id, instance, onSuccess)) } else { - dispatch(createInstanceAction({ ...instance }, onSuccess)) + dispatch(createInstanceAction( + { ...instance }, + (data: RdiInstanceResponse) => { + sendEventTelemetry({ + event: TelemetryEvent.RDI_ENDPOINT_ADDED, + eventData: { + rdiId: data.id, + } + }) + onSuccess() + }, + (error) => { + sendEventTelemetry({ + event: TelemetryEvent.RDI_ENDPOINT_ADD_FAILED, + eventData: { + error, + } + }) + } + )) } sendEventTelemetry({ diff --git a/redisinsight/ui/src/pages/rdi/home/instance-list/RdiInstancesListWrapper.spec.tsx b/redisinsight/ui/src/pages/rdi/home/instance-list/RdiInstancesListWrapper.spec.tsx index df4ef442a5..25aeb26a92 100644 --- a/redisinsight/ui/src/pages/rdi/home/instance-list/RdiInstancesListWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/rdi/home/instance-list/RdiInstancesListWrapper.spec.tsx @@ -195,4 +195,22 @@ describe('RdiInstancesListWrapper', () => { }); (sendEventTelemetry as jest.Mock).mockRestore() }) + + it('should call proper telemetry on instance click', async () => { + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock) + render() + + await act(() => { + fireEvent.click(screen.getByTestId('rdi-alias-1')) + }) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.OPEN_RDI_CLICKED, + eventData: { + rdiId: '1', + } + }); + (sendEventTelemetry as jest.Mock).mockRestore() + }) }) diff --git a/redisinsight/ui/src/pages/rdi/home/instance-list/RdiInstancesListWrapper.tsx b/redisinsight/ui/src/pages/rdi/home/instance-list/RdiInstancesListWrapper.tsx index 25c4bf9aea..6e23fc7d2c 100644 --- a/redisinsight/ui/src/pages/rdi/home/instance-list/RdiInstancesListWrapper.tsx +++ b/redisinsight/ui/src/pages/rdi/home/instance-list/RdiInstancesListWrapper.tsx @@ -69,6 +69,12 @@ const RdiInstancesListWrapper = ({ width, onEditInstance, editedInstance, onDele }, [width]) const handleCheckConnectToInstance = (id: string) => { + sendEventTelemetry({ + event: TelemetryEvent.OPEN_RDI_CLICKED, + eventData: { + rdiId: id, + } + }) dispatch(checkConnectToRdiInstanceAction( id, (id: string) => history.push(Pages.rdiPipeline(id)), diff --git a/redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/deploy-pipeline-button/DeployPipelineButton.tsx b/redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/deploy-pipeline-button/DeployPipelineButton.tsx index 468808b814..1072755d2b 100644 --- a/redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/deploy-pipeline-button/DeployPipelineButton.tsx +++ b/redisinsight/ui/src/pages/rdi/instance/components/header/components/buttons/deploy-pipeline-button/DeployPipelineButton.tsx @@ -110,7 +110,7 @@ const DeployPipelineButton = ({ loading, disabled }: Props) => { className={cx(styles.resetPipelineCheckbox, { [styles.checked]: resetPipeline })} checked={resetPipeline} onChange={(e) => handleSelectReset(e.target.checked)} - data-testId="reset-pipeline-checkbox" + data-testid="reset-pipeline-checkbox" /> { span { - color: var(--recommendationsCountBgColor) !important; - } } diff --git a/redisinsight/ui/src/pages/rdi/pipeline-management/components/template-popover/TemplatePopover.tsx b/redisinsight/ui/src/pages/rdi/pipeline-management/components/template-popover/TemplatePopover.tsx index 025a1ee813..dde1776459 100644 --- a/redisinsight/ui/src/pages/rdi/pipeline-management/components/template-popover/TemplatePopover.tsx +++ b/redisinsight/ui/src/pages/rdi/pipeline-management/components/template-popover/TemplatePopover.tsx @@ -47,7 +47,6 @@ const TemplatePopover = (props: Props) => { span { - color: var(--recommendationsCountBgColor) !important; - } } } diff --git a/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.tsx b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.tsx index eec84eef2c..9261e51213 100644 --- a/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.tsx +++ b/redisinsight/ui/src/pages/redis-cluster/RedisClusterDatabasesPage.tsx @@ -126,7 +126,7 @@ const RedisClusterDatabasesPage = () => { { field: 'modules', className: 'column_modules', - name: 'Modules', + name: 'Capabilities', dataType: 'auto', align: 'left', width: '190px', diff --git a/redisinsight/ui/src/services/index.ts b/redisinsight/ui/src/services/index.ts index a35cf5a229..432c379246 100644 --- a/redisinsight/ui/src/services/index.ts +++ b/redisinsight/ui/src/services/index.ts @@ -1,5 +1,6 @@ /* eslint-disable import/first */ export * from './storage' +export * from './migrateStorageData' import apiService from './apiService' import resourcesService from './resourcesService' diff --git a/redisinsight/ui/src/services/migrateStorageData.ts b/redisinsight/ui/src/services/migrateStorageData.ts new file mode 100644 index 0000000000..9a950e4e69 --- /dev/null +++ b/redisinsight/ui/src/services/migrateStorageData.ts @@ -0,0 +1,26 @@ +import { isString } from 'lodash' +import { BrowserStorageItem } from 'uiSrc/constants' +import { getDBConfigStorageField, localStorageService, setDBConfigStorageField } from './storage' + +export const migrateLocalStorageData = () => { + migrateDelimiterTreeView() +} + +const migrateDelimiterTreeView = () => { + const prefix = 'dbConfig_' + const storage = localStorageService.getAll() + + // Iterate over all keys and filter for the dbConfig_ prefix + Object.keys(storage).forEach((key) => { + if (key.startsWith(prefix)) { + const instanceId = key.replace(prefix, '') + + const treeViewDelimiter = getDBConfigStorageField(instanceId, BrowserStorageItem.treeViewDelimiter) + + // Check if treeViewDelimiter is a string and needs transform to array + if (isString(treeViewDelimiter)) { + setDBConfigStorageField(instanceId, BrowserStorageItem.treeViewDelimiter, [{ label: treeViewDelimiter }]) + } + } + }) +} diff --git a/redisinsight/ui/src/services/storage.ts b/redisinsight/ui/src/services/storage.ts index 2c94db4901..d4dcc28809 100644 --- a/redisinsight/ui/src/services/storage.ts +++ b/redisinsight/ui/src/services/storage.ts @@ -1,17 +1,26 @@ import { isObjectLike } from 'lodash' +import { Maybe } from 'uiSrc/utils' import BrowserStorageItem from '../constants/storage' class StorageService { private storage: Storage - constructor(storage: Storage) { + private envKey: Maybe + + constructor(storage: Storage, envKey?: string) { this.storage = storage + this.envKey = envKey + } + + private getKey(itemName: string): string { + return this.envKey ? `${this.envKey}_${itemName}` : itemName } get(itemName: string = '') { + const key = this.getKey(itemName) let item try { - item = this.storage.getItem(itemName) + item = this.storage.getItem(key) } catch (error) { console.error(`getItem from storage error: ${error}`) } @@ -26,16 +35,13 @@ class StorageService { return null } - getAll() { - return this.storage - } - set(itemName: string = '', item: any) { try { + const key = this.getKey(itemName) if (isObjectLike(item)) { - this.storage.setItem(itemName, JSON.stringify(item)) + this.storage.setItem(key, JSON.stringify(item)) } else { - this.storage.setItem(itemName, item) + this.storage.setItem(key, item) } } catch (error) { console.error(`setItem to storage error: ${error}`) @@ -43,11 +49,18 @@ class StorageService { } remove(itemName: string = '') { - this.storage.removeItem(itemName) + const key = this.getKey(itemName) + this.storage.removeItem(key) + } + + getAll() { + return this.storage } } -export const localStorageService = new StorageService(localStorage) -export const sessionStorageService = new StorageService(sessionStorage) +const envKey = window.__RI_PROXY_PATH__ + +export const localStorageService = new StorageService(localStorage, envKey) +export const sessionStorageService = new StorageService(sessionStorage, envKey) export const getObjectStorageField = (itemName = '', field = '') => { try { diff --git a/redisinsight/ui/src/services/theme.ts b/redisinsight/ui/src/services/theme.ts index 8fe84b98a8..7373dc01a7 100644 --- a/redisinsight/ui/src/services/theme.ts +++ b/redisinsight/ui/src/services/theme.ts @@ -13,22 +13,19 @@ class ThemeService { } applyTheme(newTheme: Theme) { - const actualTheme = newTheme + let actualTheme = newTheme + if (newTheme === Theme.System) { - if (window.matchMedia && window.matchMedia(THEME_MATCH_MEDIA_DARK).matches) { - newTheme = Theme.Dark - } else { - newTheme = Theme.Light - } + actualTheme = window.matchMedia?.(THEME_MATCH_MEDIA_DARK)?.matches ? Theme.Dark : Theme.Light } const sheet = new CSSStyleSheet() - sheet?.replaceSync(this.themes[newTheme]) + sheet?.replaceSync(this.themes[actualTheme]) document.adoptedStyleSheets = [sheet] localStorageService.set(BrowserStorageItem.theme, actualTheme) - document.body.classList.value = `theme_${newTheme}` + document.body.classList.value = `theme_${actualTheme}` } static getTheme() { diff --git a/redisinsight/ui/src/slices/analytics/dbAnalysis.ts b/redisinsight/ui/src/slices/analytics/dbAnalysis.ts index f7288480dc..ef7e67d454 100644 --- a/redisinsight/ui/src/slices/analytics/dbAnalysis.ts +++ b/redisinsight/ui/src/slices/analytics/dbAnalysis.ts @@ -199,7 +199,7 @@ export function fetchDBAnalysisReportsHistory( export function createNewAnalysis( instanceId: string, - delimiter: string, + delimiters: string[], onSuccessAction?: (data: DatabaseAnalysis) => void, onFailAction?: () => void, ) { @@ -213,7 +213,7 @@ export function createNewAnalysis( ApiEndpoints.DATABASE_ANALYSIS, ), { - delimiter, + delimiter: delimiters?.[0], } ) diff --git a/redisinsight/ui/src/slices/app/context.ts b/redisinsight/ui/src/slices/app/context.ts index 4421886dff..842a5e4fb5 100644 --- a/redisinsight/ui/src/slices/app/context.ts +++ b/redisinsight/ui/src/slices/app/context.ts @@ -1,4 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { EuiComboBoxOptionOption } from '@elastic/eui' import { RelativeWidthSizes } from 'uiSrc/components/virtual-table/interfaces' import { CapabilityStorageItem, ConfigDBStorageItem } from 'uiSrc/constants/storage' import { Maybe, Nullable } from 'uiSrc/utils' @@ -37,7 +38,7 @@ export const initialState: StateAppContext = { contextRdiInstanceId: '', lastPage: '', dbConfig: { - treeViewDelimiter: DEFAULT_DELIMITER, + treeViewDelimiter: [DEFAULT_DELIMITER], treeViewSort: DEFAULT_TREE_SORTING, slowLogDurationUnit: DEFAULT_SLOWLOG_DURATION_UNIT, showHiddenRecommendations: DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS, @@ -56,7 +57,6 @@ export const initialState: StateAppContext = { }, panelSizes: {}, tree: { - delimiter: DEFAULT_DELIMITER, openNodes: {}, selectedLeaf: null, }, @@ -127,7 +127,7 @@ const appContextSlice = createSlice({ state.workspace = payload || AppWorkspace.Databases }, setDbConfig: (state, { payload }) => { - state.dbConfig.treeViewDelimiter = payload?.treeViewDelimiter ?? DEFAULT_DELIMITER + state.dbConfig.treeViewDelimiter = payload?.treeViewDelimiter ?? [DEFAULT_DELIMITER] state.dbConfig.treeViewSort = payload?.treeViewSort ?? DEFAULT_TREE_SORTING state.dbConfig.slowLogDurationUnit = payload?.slowLogDurationUnit ?? DEFAULT_SLOWLOG_DURATION_UNIT state.dbConfig.showHiddenRecommendations = payload?.showHiddenRecommendations @@ -136,8 +136,8 @@ const appContextSlice = createSlice({ state.dbConfig.slowLogDurationUnit = payload setDBConfigStorageField(state.contextInstanceId, ConfigDBStorageItem.slowLogDurationUnit, payload) }, - setBrowserTreeDelimiter: (state, { payload }: { payload: string }) => { - state.dbConfig.treeViewDelimiter = payload + setBrowserTreeDelimiter: (state, { payload }: { payload: EuiComboBoxOptionOption[] }) => { + state.dbConfig.treeViewDelimiter = payload as any setDBConfigStorageField(state.contextInstanceId, BrowserStorageItem.treeViewDelimiter, payload) }, setBrowserTreeSort: (state, { payload }: PayloadAction) => { diff --git a/redisinsight/ui/src/slices/browser/keys.ts b/redisinsight/ui/src/slices/browser/keys.ts index daf1e7a9d2..bac4fa232e 100644 --- a/redisinsight/ui/src/slices/browser/keys.ts +++ b/redisinsight/ui/src/slices/browser/keys.ts @@ -519,7 +519,7 @@ export function fetchPatternKeysAction( sourceKeysFetch = CancelToken.source() const state = stateInit() - const scanThreshold = state.user.settings.config?.scanThreshold || SCAN_COUNT_DEFAULT; + const scanThreshold = state.user.settings.config?.scanThreshold || SCAN_COUNT_DEFAULT const { search: match, filter: type } = state.browser.keys const { encoding } = state.app.info @@ -594,7 +594,7 @@ export function fetchMorePatternKeysAction(oldKeys: IKeyPropTypes[] = [], cursor sourceKeysFetch = CancelToken.source() const state = stateInit() - const scanThreshold = state.user.settings.config?.scanThreshold ?? SCAN_COUNT_DEFAULT; + const scanThreshold = state.user.settings.config?.scanThreshold ?? SCAN_COUNT_DEFAULT const { search: match, filter: type } = state.browser.keys const { encoding } = state.app.info const { data, status } = await apiService.post( @@ -643,7 +643,11 @@ export function fetchMorePatternKeysAction(oldKeys: IKeyPropTypes[] = [], cursor } // Asynchronous thunk action -export function fetchKeyInfo(key: RedisResponseBuffer, resetData?: boolean) { +export function fetchKeyInfo( + key: RedisResponseBuffer, + resetData?: boolean, + onSuccess?: (data: Nullable) => void +) { return async (dispatch: AppDispatch, stateInit: () => RootState) => { dispatch(defaultSelectedKeyAction()) @@ -662,6 +666,7 @@ export function fetchKeyInfo(key: RedisResponseBuffer, resetData?: boolean) { if (isStatusSuccessful(status)) { dispatch(loadKeyInfoSuccess(data)) dispatch(updateSelectedKeyRefreshTime(Date.now())) + onSuccess?.(data) } if (data.type === KeyTypes.Hash) { diff --git a/redisinsight/ui/src/slices/browser/rejson.ts b/redisinsight/ui/src/slices/browser/rejson.ts index 1ad081db16..bea5dc50ad 100644 --- a/redisinsight/ui/src/slices/browser/rejson.ts +++ b/redisinsight/ui/src/slices/browser/rejson.ts @@ -13,6 +13,7 @@ import { Nullable, } from 'uiSrc/utils' import successMessages from 'uiSrc/components/notifications/success-messages' +import { parseJsonData } from 'uiSrc/pages/browser/modules/key-details/components/rejson-details/utils' import { GetRejsonRlResponseDto, @@ -352,10 +353,14 @@ export function fetchVisualisationResults(path = '.', forceRetrieve = false) { ) if (isStatusSuccessful(status)) { - return data + return { + ...data, + data: parseJsonData(data?.data) + } } throw new Error(data.toString()) - } catch (error) { + } catch (_err) { + const error = _err as AxiosError if (!axios.isCancel(error)) { const errorMessage = getApiErrorMessage(error) dispatch(loadRejsonBranchFailure(errorMessage)) diff --git a/redisinsight/ui/src/slices/interfaces/app.ts b/redisinsight/ui/src/slices/interfaces/app.ts index 994b3c41e2..f1ca5bec56 100644 --- a/redisinsight/ui/src/slices/interfaces/app.ts +++ b/redisinsight/ui/src/slices/interfaces/app.ts @@ -1,4 +1,5 @@ import { AxiosError } from 'axios' +import { EuiComboBoxOptionOption } from '@elastic/eui' import { RelativeWidthSizes } from 'uiSrc/components/virtual-table/interfaces' import { Nullable } from 'uiSrc/utils' import { DurationUnits, FeatureFlags, ICommands, SortOrder } from 'uiSrc/constants' @@ -64,7 +65,7 @@ export interface StateAppContext { contextRdiInstanceId: string lastPage: string dbConfig: { - treeViewDelimiter: string + treeViewDelimiter: EuiComboBoxOptionOption[] treeViewSort: SortOrder slowLogDurationUnit: DurationUnits showHiddenRecommendations: boolean @@ -85,7 +86,6 @@ export interface StateAppContext { [key: string]: number } tree: { - delimiter: string openNodes: { [key: string]: boolean } diff --git a/redisinsight/ui/src/slices/interfaces/cloud.ts b/redisinsight/ui/src/slices/interfaces/cloud.ts index b94f3efa70..6e27ec7476 100644 --- a/redisinsight/ui/src/slices/interfaces/cloud.ts +++ b/redisinsight/ui/src/slices/interfaces/cloud.ts @@ -17,6 +17,7 @@ export interface StateAppOAuth { source: Nullable job: Nullable user: { + initialLoading: boolean error: string loading: boolean data: Nullable diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index 2eb1f64ab4..c8ccc4cda4 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -189,16 +189,18 @@ export const COMMAND_MODULES = { [RedisDefaultModules.Bloom]: [RedisDefaultModules.Bloom], } -const RediSearchModulesText = [...REDISEARCH_MODULES].reduce((prev, next) => ({ ...prev, [next]: 'RediSearch' }), {}) +const RediSearchModulesText = [...REDISEARCH_MODULES].reduce((prev, next) => ({ ...prev, [next]: 'Redis Query Engine' }), {}) // Enums don't allow to use dynamic key export const DATABASE_LIST_MODULES_TEXT = Object.freeze({ - [RedisDefaultModules.AI]: 'RedisAI', - [RedisDefaultModules.Graph]: 'RedisGraph', - [RedisDefaultModules.Gears]: 'RedisGears', - [RedisDefaultModules.Bloom]: 'RedisBloom', - [RedisDefaultModules.ReJSON]: 'RedisJSON', - [RedisDefaultModules.TimeSeries]: 'RedisTimeSeries', + [RedisDefaultModules.AI]: 'AI', + [RedisDefaultModules.Graph]: 'Graph', + [RedisDefaultModules.Gears]: 'Gears', + [RedisDefaultModules.RedisGears]: 'Gears', + [RedisDefaultModules.RedisGears2]: 'Gears', + [RedisDefaultModules.Bloom]: 'Probabilistic', + [RedisDefaultModules.ReJSON]: 'JSON', + [RedisDefaultModules.TimeSeries]: 'Time Series', [RedisCustomModulesName.Proto]: 'redis-protobuf', [RedisCustomModulesName.IpTables]: 'RedisPushIpTables', ...RediSearchModulesText, diff --git a/redisinsight/ui/src/slices/interfaces/rdi.ts b/redisinsight/ui/src/slices/interfaces/rdi.ts index 925d48e29f..4f4479354d 100644 --- a/redisinsight/ui/src/slices/interfaces/rdi.ts +++ b/redisinsight/ui/src/slices/interfaces/rdi.ts @@ -213,6 +213,14 @@ export interface RdiInstance extends RdiInstanceResponse { error: string } +export interface IErrorData { + message: string + statusCode: number + error: string + errorCode?: number + errors?: string[] +} + export interface InitialStateRdiInstances { loading: boolean error: string diff --git a/redisinsight/ui/src/slices/oauth/cloud.ts b/redisinsight/ui/src/slices/oauth/cloud.ts index 5243f72649..1f3392f8fe 100644 --- a/redisinsight/ui/src/slices/oauth/cloud.ts +++ b/redisinsight/ui/src/slices/oauth/cloud.ts @@ -52,6 +52,7 @@ export const initialState: StateAppOAuth = { isOpenSelectAccountDialog: false, showProgress: true, user: { + initialLoading: true, loading: false, error: '', data: null, @@ -191,6 +192,9 @@ const oauthCloudSlice = createSlice({ logoutUserFailure: (state) => { state.user.loading = false state.user.data = null + }, + setInitialLoadingState: (state, { payload }: PayloadAction) => { + state.user.initialLoading = payload } }, }) @@ -228,7 +232,8 @@ export const { removeAllCapiKeysFailure, logoutUser, logoutUserSuccess, - logoutUserFailure + logoutUserFailure, + setInitialLoadingState } = oauthCloudSlice.actions // A selector diff --git a/redisinsight/ui/src/slices/rdi/instances.ts b/redisinsight/ui/src/slices/rdi/instances.ts index c2f3312739..9441b6a9ef 100644 --- a/redisinsight/ui/src/slices/rdi/instances.ts +++ b/redisinsight/ui/src/slices/rdi/instances.ts @@ -5,12 +5,12 @@ import { AxiosError } from 'axios' import { ApiEndpoints } from 'uiSrc/constants' import { apiService } from 'uiSrc/services' import successMessages from 'uiSrc/components/notifications/success-messages' -import { getApiErrorMessage, isStatusSuccessful, Nullable } from 'uiSrc/utils' +import { getApiErrorMessage, isStatusSuccessful, Maybe, Nullable } from 'uiSrc/utils' import { Rdi as RdiInstanceResponse } from 'apiSrc/modules/rdi/models/rdi' import { AppDispatch, RootState } from '../store' import { addErrorNotification, addMessageNotification } from '../app/notifications' -import { InitialStateRdiInstances, RdiInstance } from '../interfaces/rdi' +import { IErrorData, InitialStateRdiInstances, RdiInstance } from '../interfaces/rdi' export const initialState: InitialStateRdiInstances = { loading: true, @@ -128,6 +128,10 @@ const instancesSlice = createSlice({ // reset connected instance resetConnectedInstance: (state) => { state.connectedInstance = initialState.connectedInstance + }, + + updateConnectedInstance: (state, { payload }: { payload: RdiInstance }) => { + state.connectedInstance = { ...state.connectedInstance, ...payload } } } }) @@ -151,7 +155,8 @@ export const { setConnectedInstance, setConnectedInstanceSuccess, setConnectedInstanceFailure, - resetConnectedInstance + resetConnectedInstance, + updateConnectedInstance, } = instancesSlice.actions // selectors @@ -184,7 +189,11 @@ export function fetchInstancesAction(onSuccess?: (data: RdiInstance[]) => void) } // Asynchronous thunk action -export function createInstanceAction(payload: Partial, onSuccess?: (data: RdiInstanceResponse) => void) { +export function createInstanceAction( + payload: Partial, + onSuccess?: (data: RdiInstanceResponse) => void, + onFail?: (error: Maybe) => void, +) { return async (dispatch: AppDispatch) => { dispatch(defaultInstanceChanging()) @@ -202,6 +211,8 @@ export function createInstanceAction(payload: Partial, onSuccess?: const error = _err as AxiosError const errorMessage = getApiErrorMessage(error) dispatch(defaultInstanceChangingFailure(errorMessage)) + const errorData = error?.response?.data as IErrorData + onFail?.(errorData?.errorCode || errorData?.error) dispatch(addErrorNotification(error)) } } @@ -213,7 +224,7 @@ export function editInstanceAction( payload: Partial, onSuccess?: (data: RdiInstanceResponse) => void ) { - return async (dispatch: AppDispatch) => { + return async (dispatch: AppDispatch, stateInit: () => RootState) => { dispatch(defaultInstanceChanging()) try { @@ -226,6 +237,11 @@ export function editInstanceAction( dispatch(defaultInstanceChangingSuccess()) dispatch(fetchInstancesAction()) + const state = stateInit() + if (state.rdi.instances.connectedInstance?.id === data.id) { + dispatch(updateConnectedInstance(data)) + } + onSuccess?.(data) } } catch (_err) { diff --git a/redisinsight/ui/src/slices/tests/analytics/dbAnalysis.spec.ts b/redisinsight/ui/src/slices/tests/analytics/dbAnalysis.spec.ts index 839d36c71e..a957531a2d 100644 --- a/redisinsight/ui/src/slices/tests/analytics/dbAnalysis.spec.ts +++ b/redisinsight/ui/src/slices/tests/analytics/dbAnalysis.spec.ts @@ -369,7 +369,7 @@ describe('db analysis slice', () => { // Act await store.dispatch( - createNewAnalysis('instanceId', 'delimiter') + createNewAnalysis('instanceId', ['delimiter']) ) // Assert @@ -403,7 +403,7 @@ describe('db analysis slice', () => { // Act await store.dispatch( - createNewAnalysis('instanceId', 'delimiter') + createNewAnalysis('instanceId', ['delimiter']) ) // Assert diff --git a/redisinsight/ui/src/slices/tests/app/context.spec.ts b/redisinsight/ui/src/slices/tests/app/context.spec.ts index 874ac9da0b..b4d693291e 100644 --- a/redisinsight/ui/src/slices/tests/app/context.spec.ts +++ b/redisinsight/ui/src/slices/tests/app/context.spec.ts @@ -449,7 +449,7 @@ describe('slices', () => { // Arrange const data = { slowLogDurationUnit: 'msec', - treeViewDelimiter: ':-', + treeViewDelimiter: [{ label: ':-' }], treeViewSort: SortOrder.DESC, showHiddenRecommendations: true, } @@ -496,7 +496,7 @@ describe('slices', () => { describe('setBrowserTreeDelimiter', () => { it('should properly set browser tree delimiter', () => { // Arrange - const delimiter = '_' + const delimiter = [{ label: '_' }] const state = { ...initialState.dbConfig, diff --git a/redisinsight/ui/src/slices/tests/oauth/cloud.spec.ts b/redisinsight/ui/src/slices/tests/oauth/cloud.spec.ts index 5a864cc430..e042118b7c 100644 --- a/redisinsight/ui/src/slices/tests/oauth/cloud.spec.ts +++ b/redisinsight/ui/src/slices/tests/oauth/cloud.spec.ts @@ -56,7 +56,7 @@ import reducer, { logoutUser, logoutUserSuccess, logoutUserFailure, - logoutUserAction, oauthCloudUserSelector + logoutUserAction, oauthCloudUserSelector, setInitialLoadingState } from '../../oauth/cloud' let store: typeof mockedStore @@ -848,6 +848,27 @@ describe('oauth cloud slice', () => { }) }) + describe('setInitialLoadingState', () => { + it('should properly set the state', () => { + // Arrange + const userState = { + ...initialState.user, + initialLoading: false + } + + // Act + const nextState = reducer(initialState as any, setInitialLoadingState(false)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + oauth: { + cloud: nextState + } + }) + expect(oauthCloudUserSelector(rootState)).toEqual(userState) + }) + }) + describe('thunks', () => { describe('fetchUserInfo', () => { it('call both fetchUserInfo and getUserInfoSuccess when fetch is successed', async () => { diff --git a/redisinsight/ui/src/slices/tests/rdi/instances.spec.ts b/redisinsight/ui/src/slices/tests/rdi/instances.spec.ts index 88dc5efaff..1594095917 100644 --- a/redisinsight/ui/src/slices/tests/rdi/instances.spec.ts +++ b/redisinsight/ui/src/slices/tests/rdi/instances.spec.ts @@ -13,10 +13,17 @@ import reducer, { instancesSelector, fetchConnectedInstanceAction, checkConnectToRdiInstanceAction, -} from 'uiSrc/slices/rdi/instances' + createInstanceAction, + defaultInstanceChanging, + defaultInstanceChangingSuccess, + defaultInstanceChangingFailure, + editInstanceAction, + updateConnectedInstance } from 'uiSrc/slices/rdi/instances' import { apiService } from 'uiSrc/services' -import { addErrorNotification, IAddInstanceErrorPayload } from 'uiSrc/slices/app/notifications' +import { addErrorNotification, addMessageNotification, IAddInstanceErrorPayload } from 'uiSrc/slices/app/notifications' import { RdiInstance } from 'uiSrc/slices/interfaces' +import successMessages from 'uiSrc/components/notifications/success-messages' +import { Rdi } from 'apiSrc/modules/rdi/models' let store: typeof mockedStore @@ -160,6 +167,31 @@ describe('rdi instances slice', () => { }) }) + describe('updateConnectedInstance', () => { + it('should properly update connected instance', () => { + // Arrange + const state = { + ...initialState, + connectedInstance: { + ...initialState.connectedInstance, + ...mockRdiInstance, + } + } + + // Act + const nextState = reducer(initialState, updateConnectedInstance(mockRdiInstance as Rdi)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + rdi: { + instances: nextState + }, + }) + + expect(instancesSelector(rootState)).toEqual(state) + }) + }) + // thunks describe('thunks', () => { @@ -210,6 +242,109 @@ describe('rdi instances slice', () => { }) }) + describe('createInstanceAction', () => { + const onSuccess = jest.fn() + const onFail = jest.fn() + it('succeed to create data and call success callback', async () => { + const responsePayload = { data: mockRdiInstance, status: 200 } + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + apiService.get = jest.fn().mockResolvedValue({ status: 200, data: [] }) + + // Act + await store.dispatch( + createInstanceAction(mockRdiInstance, onSuccess, onFail) + ) + + // Assert + const expectedActions = [ + defaultInstanceChanging(), + defaultInstanceChangingSuccess(), + addMessageNotification(successMessages.ADDED_NEW_RDI_INSTANCE(mockRdiInstance.name)) + ] + + expect(store.getActions()).toEqual(expect.arrayContaining(expectedActions)) + expect(onSuccess).toBeCalledWith(mockRdiInstance) + }) + + it('failed to create data and call onFail callback', async () => { + const errorMessage = 'Something was wrong!' + const errorCode = 11403 + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage, errorCode }, + }, + } + + apiService.post = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch( + createInstanceAction(mockRdiInstance, onSuccess, onFail) + ) + + // Assert + const expectedActions = [ + defaultInstanceChanging(), + defaultInstanceChangingFailure(errorMessage), + addErrorNotification(responsePayload as AxiosError), + ] + + expect(store.getActions()).toEqual(expectedActions) + expect(onFail).toBeCalledWith(errorCode) + }) + }) + + describe('editInstanceAction', () => { + it('succeed to edit data and calls a success callback', async () => { + const onSuccess = jest.fn() + const responsePayload = { data: mockRdiInstance, status: 200 } + + apiService.patch = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch( + editInstanceAction('123', mockRdiInstance, onSuccess) + ) + + // Assert + const expectedActions = [ + defaultInstanceChanging(), + defaultInstanceChangingSuccess(), + ] + + expect(store.getActions()).toEqual(expect.arrayContaining(expectedActions)) + expect(onSuccess).toBeCalledWith(mockRdiInstance) + }) + + it('failed to edit data', async () => { + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.patch = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch( + editInstanceAction('123', mockRdiInstance) + ) + + // Assert + const expectedActions = [ + defaultInstanceChanging(), + defaultInstanceChangingFailure(errorMessage), + addErrorNotification(responsePayload as AxiosError), + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + }) + describe('checkConnectToRdiInstanceAction', () => { it('succeed to fetch data', async () => { const responsePayload = { status: 200 } diff --git a/redisinsight/ui/src/slices/tests/workbench/wb-custom-tutorials.spec.ts b/redisinsight/ui/src/slices/tests/workbench/wb-custom-tutorials.spec.ts index 1bd907ba95..304c1f61e2 100644 --- a/redisinsight/ui/src/slices/tests/workbench/wb-custom-tutorials.spec.ts +++ b/redisinsight/ui/src/slices/tests/workbench/wb-custom-tutorials.spec.ts @@ -28,6 +28,7 @@ import reducer, { uploadDataBulkFailed, uploadDataBulkAction, defaultItems, + setWbCustomTutorialsState, } from '../../workbench/wb-custom-tutorials' let store: typeof mockedStore @@ -371,6 +372,85 @@ describe('slices', () => { expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state) }) + + describe('setWbCustomTutorialsState', () => { + it('should properly set open state', () => { + // Arrange + const currentState = { + ...initialState, + items: [{ + ...defaultItems[0], + args: { + initialIsOpen: false + }, + children: MOCK_TUTORIALS_ITEMS + }] + } + + const state = { + ...initialState, + items: [{ + ...defaultItems[0], + args: { + defaultInitialIsOpen: false, + initialIsOpen: true + }, + children: MOCK_TUTORIALS_ITEMS + }] + } + + // Act + const nextState = reducer(currentState, setWbCustomTutorialsState(true)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + workbench: { + customTutorials: nextState, + }, + }) + + expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state) + }) + + it('should properly return open state', () => { + // Arrange + const currentState = { + ...initialState, + items: [{ + ...defaultItems[0], + args: { + defaultInitialIsOpen: false, + initialIsOpen: true + }, + children: MOCK_TUTORIALS_ITEMS + }] + } + + const state = { + ...initialState, + items: [{ + ...defaultItems[0], + args: { + defaultInitialIsOpen: false, + initialIsOpen: false + }, + children: MOCK_TUTORIALS_ITEMS + }] + } + + // Act + const nextState = reducer(currentState, setWbCustomTutorialsState()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + workbench: { + customTutorials: nextState, + }, + }) + + expect(workbenchCustomTutorialsSelector(rootState)).toEqual(state) + }) + }) }) // thunks diff --git a/redisinsight/ui/src/slices/workbench/wb-custom-tutorials.ts b/redisinsight/ui/src/slices/workbench/wb-custom-tutorials.ts index 8992515a05..eeeb912623 100644 --- a/redisinsight/ui/src/slices/workbench/wb-custom-tutorials.ts +++ b/redisinsight/ui/src/slices/workbench/wb-custom-tutorials.ts @@ -1,8 +1,8 @@ -import { createSlice } from '@reduxjs/toolkit' -import { remove } from 'lodash' +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { isUndefined, remove } from 'lodash' import { AxiosError } from 'axios' import { ApiEndpoints } from 'uiSrc/constants' -import { getApiErrorMessage, getUrl, isStatusSuccessful, } from 'uiSrc/utils' +import { getApiErrorMessage, getUrl, isStatusSuccessful, Maybe, } from 'uiSrc/utils' import { apiService } from 'uiSrc/services' import { EnablementAreaComponent, @@ -86,7 +86,19 @@ const workbenchCustomTutorialsSlice = createSlice({ }, uploadDataBulkFailed: (state, { payload }) => { remove(state.bulkUpload.pathsInProgress, (p) => p === payload) - } + }, + setWbCustomTutorialsState: (state, { payload }: PayloadAction>) => { + if (state.items[0].args) { + const { defaultInitialIsOpen, initialIsOpen } = state.items[0].args + if (isUndefined(payload)) { + state.items[0].args.initialIsOpen = defaultInitialIsOpen ?? initialIsOpen + return + } + + state.items[0].args.defaultInitialIsOpen = initialIsOpen + state.items[0].args.initialIsOpen = payload + } + }, } }) @@ -108,6 +120,7 @@ export const { uploadDataBulk, uploadDataBulkSuccess, uploadDataBulkFailed, + setWbCustomTutorialsState, } = workbenchCustomTutorialsSlice.actions // The reducer diff --git a/redisinsight/ui/src/styles/base/_inputs.scss b/redisinsight/ui/src/styles/base/_inputs.scss index ab4bd8d69b..df1938840e 100644 --- a/redisinsight/ui/src/styles/base/_inputs.scss +++ b/redisinsight/ui/src/styles/base/_inputs.scss @@ -51,3 +51,42 @@ input[name='sshPassphrase'] ~ .euiFormControlLayoutIcons { width: 34px !important; height: 29px !important; } + +.euiComboBox.euiComboBox-isOpen .euiComboBox__inputWrap { + background-image: none !important; + border-bottom: solid 2px var(--euiColorPrimary) !important; +} +.euiComboBox .euiComboBox__inputWrap { + .euiComboBoxPill { + background-color: var(--buttonDarkenBgColor) !important; + } + .euiBadge__iconButton { + margin-top: 1px; + margin-left: 8px; + margin-right: 3px; + border-radius: 50%; + background-color: var(--euiColorPrimary); + color: var(--rdiSecondaryBgColor); + width: 13px; + height: 13px; + + &:hover { + transform: translateY(-1px); + } + &:active { + transform: translateY(1px); + } + + svg { + width: 10px; + height: 10px; + + &:focus { + background: inherit; + } + } + } + .euiComboBox__input > input { + color: var(--inputTextColor) !important; + } +} diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index b384e915f8..89d23ae97e 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -303,6 +303,9 @@ export enum TelemetryEvent { RDI_INSTANCE_ADD_CLICKED = 'RDI_INSTANCE_ADD_CLICKED', RDI_INSTANCE_ADD_CANCELLED = 'RDI_INSTANCE_ADD_CANCELLED', RDI_INSTANCE_SUBMITTED = 'RDI_INSTANCE_SUBMITTED', + OPEN_RDI_CLICKED = 'OPEN_RDI_CLICKED', + RDI_ENDPOINT_ADDED = 'RDI_ENDPOINT_ADDED', + RDI_ENDPOINT_ADD_FAILED = 'RDI_ENDPOINT_ADD_FAILED', RDI_PIPELINE_UPLOAD_FROM_SERVER_CLICKED = 'RDI_PIPELINE_UPLOAD_FROM_SERVER_CLICKED', RDI_DEPLOY_CLICKED = 'RDI_DEPLOY_CLICKED', RDI_PIPELINE_RESET_CLICKED = 'RDI_PIPELINE_RESET_CLICKED', diff --git a/redisinsight/ui/src/utils/capability.ts b/redisinsight/ui/src/utils/capability.ts index 38c162962f..19371f3244 100644 --- a/redisinsight/ui/src/utils/capability.ts +++ b/redisinsight/ui/src/utils/capability.ts @@ -24,7 +24,7 @@ export const getTutorialCapability = (source: any = '') => { case getSourceTutorialByCapability(RedisDefaultModules.FTL): return getCapability( 'searchAndQuery', - 'Search and query capability', + 'Redis Query Engine capability', findMarkdownPath(store.getState()?.workbench?.tutorials?.items, { id: 'sq-intro' }) ) diff --git a/redisinsight/ui/src/utils/formatters/bufferFormatters.ts b/redisinsight/ui/src/utils/formatters/bufferFormatters.ts index 3b43fb5e05..fe4cf98847 100644 --- a/redisinsight/ui/src/utils/formatters/bufferFormatters.ts +++ b/redisinsight/ui/src/utils/formatters/bufferFormatters.ts @@ -3,6 +3,7 @@ import { ObjectInputStream } from 'java-object-serialization' import { TextDecoder, TextEncoder } from 'text-encoding' import { Buffer } from 'buffer' import { KeyValueFormat } from 'uiSrc/constants' +import JavaDate from './java-date' // eslint-disable-next-line import/order import { RedisResponseBuffer, @@ -12,6 +13,8 @@ import { } from 'uiSrc/slices/interfaces' import { Nullable } from '../types' +ObjectInputStream.RegisterObjectClass(JavaDate, JavaDate.ClassName, JavaDate.SerialVersionUID) + const decoder = new TextDecoder('utf-8') const encoder = new TextEncoder() @@ -171,6 +174,10 @@ const bufferToJava = (reply: RedisResponseBuffer) => { return decoded } + if (decoded instanceof Date) { + return decoded + } + const { fields } = decoded const fieldsArray = Array.from(fields, ([key, value]) => ({ [key]: value })) return { ...decoded, fields: fieldsArray } diff --git a/redisinsight/ui/src/utils/formatters/java-date.ts b/redisinsight/ui/src/utils/formatters/java-date.ts new file mode 100644 index 0000000000..eb609dd0f1 --- /dev/null +++ b/redisinsight/ui/src/utils/formatters/java-date.ts @@ -0,0 +1,42 @@ +import { ObjectInputStream, JavaSerializable } from 'java-object-serialization' + +export default class JavaDate implements JavaSerializable { + // The class name in the serialized data + static readonly ClassName = 'java.util.Date' + + // The serial version UID followed for 'java.util.Date' + static readonly SerialVersionUID = '7523967970034938905' + + // The maximum value for a Java long + readonly JAVA_MAX_LONG = 9223372036854775807n // 2^63 - 1 + + // The maximum value for a two's complement long + readonly TWO_COMPLEMENT_MAX_LONG = 18446744073709551616n // 2^64 + + time: bigint = 0n + + readObject(stream: ObjectInputStream): void { + this.time = stream.readLong() + } + + readResolve() { + let timeValue: number + + // Handle two's complement conversion for negative numbers + if (this.time > this.JAVA_MAX_LONG) { + // If the number is larger than MAX_LONG, it's a negative number in two's complement + timeValue = Number(this.time - this.TWO_COMPLEMENT_MAX_LONG) + } else { + timeValue = Number(this.time) + } + + const date = new Date(timeValue) + + // Validate the date + if (Number.isNaN(date.getTime())) { + throw new Error(`Invalid date value: ${timeValue} (original: ${this.time})`) + } + + return date + } +} diff --git a/redisinsight/ui/src/utils/formatters/valueFormatters.tsx b/redisinsight/ui/src/utils/formatters/valueFormatters.tsx index cc83cf71f7..45f2bcda45 100644 --- a/redisinsight/ui/src/utils/formatters/valueFormatters.tsx +++ b/redisinsight/ui/src/utils/formatters/valueFormatters.tsx @@ -96,7 +96,7 @@ const formattingBuffer = ( } case KeyValueFormat.PHP: { try { - const decoded = unserialize(Buffer.from(reply.data), {}, { strict: false, encoding: 'binary' }) + const decoded = unserialize(bufferToUTF8(reply), {}, { strict: false, encoding: 'utf8' }) const value = JSONBigInt.stringify(decoded) return JSONViewer({ value, ...props }) } catch (e) { @@ -216,7 +216,7 @@ const bufferToSerializedFormat = ( } case KeyValueFormat.PHP: { try { - const decoded = unserialize(Buffer.from(value.data), {}, { strict: false, encoding: 'binary' }) + const decoded = unserialize(bufferToUTF8(value), {}, { strict: false, encoding: 'utf8' }) const stringified = JSON.stringify(decoded) return reSerializeJSON(stringified, space) } catch (e) { diff --git a/redisinsight/ui/src/utils/tests/capability.spec.ts b/redisinsight/ui/src/utils/tests/capability.spec.ts index fe93038982..962182a5ee 100644 --- a/redisinsight/ui/src/utils/tests/capability.spec.ts +++ b/redisinsight/ui/src/utils/tests/capability.spec.ts @@ -16,7 +16,7 @@ describe('getSourceTutorialByCapability', () => { }) const emptyCapability = { name: '', telemetryName: '', path: null, } -const searchCapability = { name: 'Search and query capability', telemetryName: 'searchAndQuery', path: null, } +const searchCapability = { name: 'Redis Query Engine capability', telemetryName: 'searchAndQuery', path: null, } const jsonCapability = { name: 'JSON capability', telemetryName: 'JSON', path: null, } const tsCapability = { name: 'Time series data structure', telemetryName: 'timeSeries', path: null, } const bloomCapability = { name: 'Probabilistic data structures', telemetryName: 'probabilistic', path: null, } diff --git a/redisinsight/ui/src/utils/tests/formatters/bufferFormatters.spec.ts b/redisinsight/ui/src/utils/tests/formatters/bufferFormatters.spec.ts index cc588e564a..06c9bc7c12 100644 --- a/redisinsight/ui/src/utils/tests/formatters/bufferFormatters.spec.ts +++ b/redisinsight/ui/src/utils/tests/formatters/bufferFormatters.spec.ts @@ -1,5 +1,6 @@ import { input } from '@testing-library/user-event/dist/types/event' import { RedisResponseBufferType } from 'uiSrc/slices/interfaces' +import JavaDate from 'uiSrc/utils/formatters/java-date' import { bufferToString, anyToBuffer, @@ -16,6 +17,12 @@ import { bufferToUint8Array, } from 'uiSrc/utils' +try { + // Register JavaDate class for deserialization + ObjectInputStream.RegisterObjectClass(JavaDate, JavaDate.ClassName, JavaDate.SerialVersionUID) + // eslint-disable-next-line no-empty +} catch (e) {} + const defaultValues = [ { 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' }, @@ -134,6 +141,7 @@ describe('binaryToBuffer', () => { 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 } }, + { uint8Array: [ 172, 237, 0, 5, 115, 114, 0, 14, 106, 97, 118, 97, 46, 117, 116, 105, 108, 46, 68, 97, 116, 101, 104, 106, 129, 1, 75, 89, 116, 25, 3, 0, 0, 120, 112, 119, 8, 0, 0, 1, 146, 226, 121, 165, 136, 120, ], value: new Date(Number(1730376476040n)) }, ] const getBufferToJavaTests = javaValues.map(({ uint8Array, value }) => @@ -150,3 +158,4 @@ describe('bufferToUint8Array', () => { expect(bufferToUint8Array(anyToBuffer(uint8Array))).toEqual(new Uint8Array(uint8Array)) }) }) + diff --git a/redisinsight/ui/src/utils/tests/formatters/valueFormatters.spec.ts b/redisinsight/ui/src/utils/tests/formatters/valueFormatters.spec.ts index b61c21c20a..fde8558b13 100644 --- a/redisinsight/ui/src/utils/tests/formatters/valueFormatters.spec.ts +++ b/redisinsight/ui/src/utils/tests/formatters/valueFormatters.spec.ts @@ -94,7 +94,7 @@ describe('bufferToSerializedFormat', () => { describe(KeyValueFormat.PHP, () => { describe('should properly serialize', () => { - const testValues = [[1], '""', 6677, true, { a: { b: [1, 2, '3'] } }].map((v) => ({ + const testValues = [[1], '""', '反序列化', 6677, true, { a: { b: [1, 2, '3'] } }].map((v) => ({ input: stringToBuffer(serialize(v)), expected: JSON.stringify(v) })) @@ -156,7 +156,7 @@ describe('stringToSerializedBufferFormat', () => { describe(KeyValueFormat.PHP, () => { describe('should properly unserialize', () => { - const testValues = [[1], '""', 6677, true, { a: { b: [1, 2, '3'] } }].map((v) => ({ + const testValues = [[1], '""', '反序列化', 6677, true, { a: { b: [1, 2, '3'] } }].map((v) => ({ input: JSON.stringify(v), expected: stringToBuffer(serialize(v)) })) diff --git a/redisinsight/ui/src/utils/tests/modules.spec.ts b/redisinsight/ui/src/utils/tests/modules.spec.ts index 107faaa1c1..90147a9506 100644 --- a/redisinsight/ui/src/utils/tests/modules.spec.ts +++ b/redisinsight/ui/src/utils/tests/modules.spec.ts @@ -8,33 +8,33 @@ import { } from 'uiSrc/utils/modules' const modules1: IDatabaseModule[] = [ - { moduleName: 'RedisJSON', abbreviation: 'RS' }, + { moduleName: 'JSON', abbreviation: 'RS' }, { moduleName: 'My1Module', abbreviation: 'MD' }, - { moduleName: 'RediSearch', abbreviation: 'RS' }, + { moduleName: 'Redis Query Engine', abbreviation: 'RS' }, ] const modules2: IDatabaseModule[] = [ { moduleName: '', abbreviation: '' }, { moduleName: '', abbreviation: '' }, - { moduleName: 'RedisBloom', abbreviation: 'RS' }, + { moduleName: 'Probabilistic', abbreviation: 'RS' }, { moduleName: '', abbreviation: '' }, { moduleName: '', abbreviation: '' }, { moduleName: 'MycvModule', abbreviation: 'MC' }, { moduleName: 'My1Module', abbreviation: 'MD' }, - { moduleName: 'RedisJSON', abbreviation: 'RS' }, + { moduleName: 'JSON', abbreviation: 'RS' }, { moduleName: 'My2Modul2e', abbreviation: 'MX' }, - { moduleName: 'RediSearch', abbreviation: 'RS' }, + { moduleName: 'Redis Query Engine', abbreviation: 'RS' }, ] const result1: IDatabaseModule[] = [ - { moduleName: 'RediSearch', abbreviation: 'RS' }, - { moduleName: 'RedisJSON', abbreviation: 'RS' }, + { moduleName: 'Redis Query Engine', abbreviation: 'RS' }, + { moduleName: 'JSON', abbreviation: 'RS' }, { moduleName: 'My1Module', abbreviation: 'MD' } ] const result2: IDatabaseModule[] = [ - { moduleName: 'RediSearch', abbreviation: 'RS' }, - { moduleName: 'RedisJSON', abbreviation: 'RS' }, - { moduleName: 'RedisBloom', abbreviation: 'RS' }, + { moduleName: 'Redis Query Engine', abbreviation: 'RS' }, + { moduleName: 'JSON', abbreviation: 'RS' }, + { moduleName: 'Probabilistic', abbreviation: 'RS' }, { moduleName: 'MycvModule', abbreviation: 'MC' }, { moduleName: 'My1Module', abbreviation: 'MD' }, { moduleName: 'My2Modul2e', abbreviation: 'MX' }, diff --git a/redisinsight/ui/src/utils/tests/transformers/browser.spec.ts b/redisinsight/ui/src/utils/tests/transformers/browser.spec.ts new file mode 100644 index 0000000000..ebe90f832a --- /dev/null +++ b/redisinsight/ui/src/utils/tests/transformers/browser.spec.ts @@ -0,0 +1,16 @@ +import { comboBoxToArray } from 'uiSrc/utils' + +const getOutputForFormatToTextTests: any[] = [ + [[], []], + [[{ label: '123' }, { label: 'test' }], ['123', 'test']], + [[{ label1: '123' }], []], + [[{ label: '123' }, { label: 'test' }], ['123', 'test']], +] + +describe('formatToText', () => { + it.each(getOutputForFormatToTextTests)('for input: %s (reply), should be output: %s', + (reply, expected) => { + const result = comboBoxToArray(reply) + expect(result).toEqual(expected) + }) +}) diff --git a/redisinsight/ui/src/utils/transformers/browser.ts b/redisinsight/ui/src/utils/transformers/browser.ts new file mode 100644 index 0000000000..586a8da02f --- /dev/null +++ b/redisinsight/ui/src/utils/transformers/browser.ts @@ -0,0 +1,3 @@ +import { EuiComboBoxOptionOption } from '@elastic/eui' + +export const comboBoxToArray = (items: EuiComboBoxOptionOption[]) => [...items].map(({ label }) => label) diff --git a/redisinsight/ui/src/utils/transformers/index.ts b/redisinsight/ui/src/utils/transformers/index.ts index acbecc0489..013d674df3 100644 --- a/redisinsight/ui/src/utils/transformers/index.ts +++ b/redisinsight/ui/src/utils/transformers/index.ts @@ -11,6 +11,7 @@ export * from './extrapolation' export * from './transformQueryParams' export * from './getTruncatedName' export * from './transformRdiPipeline' +export * from './browser' export { replaceSpaces, diff --git a/redisinsight/yarn.lock b/redisinsight/yarn.lock index c12a5b9c62..7a063ce0f3 100644 --- a/redisinsight/yarn.lock +++ b/redisinsight/yarn.lock @@ -129,11 +129,6 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -buildcheck@~0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238" - integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A== - cacache@^15.2.0: version "15.3.0" resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb" @@ -188,13 +183,8 @@ console-control-strings@^1.1.0: resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== -cpu-features@~0.0.9: - version "0.0.10" - resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.10.tgz#9aae536db2710c7254d7ed67cb3cbc7d29ad79c5" - integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA== - dependencies: - buildcheck "~0.0.6" - nan "^2.19.0" +"cpu-features@file:./api/stubs/cpu-features", cpu-features@~0.0.10: + version "1.0.0" debug@4, debug@^4.3.3: version "4.3.4" @@ -557,10 +547,10 @@ ms@^2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nan@^2.18.0, nan@^2.19.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.20.0.tgz#08c5ea813dd54ed16e5bd6505bf42af4f7838ca3" - integrity sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw== +nan@^2.20.0: + version "2.22.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3" + integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw== napi-build-utils@^1.0.1: version "1.0.2" @@ -810,15 +800,15 @@ sqlite3@5.1.7: node-gyp "8.x" ssh2@^1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.15.0.tgz#2f998455036a7f89e0df5847efb5421748d9871b" - integrity sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw== + version "1.16.0" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.16.0.tgz#79221d40cbf4d03d07fe881149de0a9de928c9f0" + integrity sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg== dependencies: asn1 "^0.2.6" bcrypt-pbkdf "^1.0.2" optionalDependencies: - cpu-features "~0.0.9" - nan "^2.18.0" + cpu-features "~0.0.10" + nan "^2.20.0" ssri@^8.0.0, ssri@^8.0.1: version "8.0.1" diff --git a/tests/e2e/.desktop.env b/tests/e2e/.desktop.env index 51de75c380..ba7c8f0112 100644 --- a/tests/e2e/.desktop.env +++ b/tests/e2e/.desktop.env @@ -50,4 +50,4 @@ RI_NOTIFICATION_SYNC_INTERVAL=30000 RI_FEATURES_CONFIG_URL=http://localhost:5551/remote/features-config.json RI_FEATURES_CONFIG_SYNC_INTERVAL=50000 -REMOTE_FOLDER_PATH=/home/circleci/project/tests/e2e/remote +REMOTE_FOLDER_PATH=/home/runner/work/RedisInsight/RedisInsight/tests/e2e/remote diff --git a/tests/e2e/.gitignore b/tests/e2e/.gitignore index 279f866809..7400c906bc 100644 --- a/tests/e2e/.gitignore +++ b/tests/e2e/.gitignore @@ -9,3 +9,4 @@ remote rihomedir rdi rte/rdi +chrome_logs.txt \ No newline at end of file diff --git a/tests/e2e/common-actions/browser-actions.ts b/tests/e2e/common-actions/browser-actions.ts index a778c03e31..a3ebb2a5bc 100644 --- a/tests/e2e/common-actions/browser-actions.ts +++ b/tests/e2e/common-actions/browser-actions.ts @@ -70,10 +70,10 @@ export class BrowserActions { } /** - * Verify that not patterned keys not visible with delimiter + * Verify that not patterned keys not displayed with delimiter * @param delimiter string with delimiter value */ - async verifyNotPatternedKeys(delimiter: string): Promise { + async verifyNotPatternedKeysNotDisplayed(delimiter: string): Promise { const notPatternedKeys = Selector('[data-testid^="badge"]').parent('[data-testid^="node-item_"]'); const notPatternedKeysNumber = await notPatternedKeys.count; @@ -88,9 +88,8 @@ export class BrowserActions { * @param folderName name of folder * @param delimiter string with delimiter value */ - getNodeName(startFolder: string, folderName: string, delimiter: string): string { - return startFolder + folderName + delimiter; - + getNodeName(startFolder: string, folderName: string, delimiter?: string): string { + return delimiter ? `${startFolder}${delimiter}${folderName}` : `${startFolder}${folderName}`; } /** @@ -106,29 +105,31 @@ export class BrowserActions { * @param folders name of folders for tree view build * @param delimiter string with delimiter value */ - async checkTreeViewFoldersStructure(folders: string[][], delimiter: string): Promise { - // Verify not patterned keys - await this.verifyNotPatternedKeys(delimiter); - - const foldersNumber = folders.length; + async checkTreeViewFoldersStructure(folders: string[][], delimiters: string[]): Promise { + await this.verifyNotPatternedKeysNotDisplayed(delimiters[0]); - for (let i = 0; i < foldersNumber; i++) { - const innerFoldersNumber = folders[i].length; - let prevNodeSelector = ''; + for (let i = 0; i < folders.length; i++) { + const delimiter = delimiters.length > 1 ? '-' : delimiters[0]; + let prevNodeName = ''; + let prevDelimiter = ''; - for (let j = 0; j < innerFoldersNumber; j++) { - const nodeName = this.getNodeName(prevNodeSelector, folders[i][j], delimiter); + // Expand subfolders + for (let j = 0; j < folders[i].length; j++) { + const nodeName = this.getNodeName(prevNodeName, folders[i][j], prevDelimiter); const node = this.getNodeSelector(nodeName); const fullTestIdSelector = await node.getAttribute('data-testid'); + if (!fullTestIdSelector?.includes('expanded')) { await t.click(node); } - prevNodeSelector = nodeName; + + prevNodeName = nodeName; + prevDelimiter = delimiter; } // Verify that the last folder level contains required keys const foundKeyName = `${folders[i].join(delimiter)}`; - const firstFolderName = this.getNodeName('', folders[i][0], delimiter); + const firstFolderName = this.getNodeName('', folders[i][0]); const firstFolder = this.getNodeSelector(firstFolderName); await t .expect(Selector(`[data-testid*="node-item_${foundKeyName}"]`).find('[data-testid^="key-"]').exists).ok('Specific key not found') diff --git a/tests/e2e/desktop.runner.ci.ts b/tests/e2e/desktop.runner.ci.ts index dfb4bcf3f2..133e031e33 100644 --- a/tests/e2e/desktop.runner.ci.ts +++ b/tests/e2e/desktop.runner.ci.ts @@ -29,17 +29,17 @@ import testcafe from 'testcafe'; }, { name: 'html', - output: './report/report.html' + output: './report/index.html' } ]) .run({ skipJsErrors: true, - browserInitTimeout: 60000, - selectorTimeout: 5000, - assertionTimeout: 5000, + browserInitTimeout: 120000, + selectorTimeout: 10000, + assertionTimeout: 10000, speed: 1, quarantineMode: { successThreshold: 1, attemptLimit: 3 }, - pageRequestTimeout: 8000, + pageRequestTimeout: 20000, disableMultipleWindows: true }); }) diff --git a/tests/e2e/desktop.runner.ts b/tests/e2e/desktop.runner.ts index 7afe4f0daf..7c7d0bf440 100644 --- a/tests/e2e/desktop.runner.ts +++ b/tests/e2e/desktop.runner.ts @@ -29,7 +29,7 @@ import testcafe from 'testcafe'; }, { name: 'html', - output: './report/report.html' + output: './report/index.html' } ]) .run({ diff --git a/tests/e2e/desktop.runner.win.ts b/tests/e2e/desktop.runner.win.ts index edb738b2a0..6c4bf71f1d 100644 --- a/tests/e2e/desktop.runner.win.ts +++ b/tests/e2e/desktop.runner.win.ts @@ -29,7 +29,7 @@ import testcafe from 'testcafe'; }, { name: 'html', - output: './report/report.html' + output: './report/index.html' } ]) .run({ diff --git a/tests/e2e/docker.web.docker-compose.yml b/tests/e2e/docker.web.docker-compose.yml index 24496ae6ed..8e9280a8d9 100644 --- a/tests/e2e/docker.web.docker-compose.yml +++ b/tests/e2e/docker.web.docker-compose.yml @@ -7,8 +7,8 @@ services: dockerfile: e2e.Dockerfile tty: true volumes: - - ./results:/usr/src/app/results - - ./report:/usr/src/app/report + - ${E2E_VOLUME_PATH:-.}/results:/usr/src/app/results + - ${E2E_VOLUME_PATH:-.}/report:/usr/src/app/report - ./plugins:/usr/src/app/plugins - rihomedir:/root/.redis-insight - tmp:/tmp @@ -46,6 +46,7 @@ services: RI_ENCRYPTION_KEY: $E2E_RI_ENCRYPTION_KEY RI_SERVER_TLS_CERT: $RI_SERVER_TLS_CERT RI_SERVER_TLS_KEY: $RI_SERVER_TLS_KEY + RI_STDOUT_LOGGER: 'false' volumes: - rihomedir:/data - tmp:/tmp diff --git a/tests/e2e/helpers/common.ts b/tests/e2e/helpers/common.ts index d6ee6edf81..1ec1f6f8ce 100644 --- a/tests/e2e/helpers/common.ts +++ b/tests/e2e/helpers/common.ts @@ -9,9 +9,9 @@ const chance = new Chance(); declare global { interface Window { - windowId?: string + windowId?: string } - } +} const settingsApiUrl = `${apiUrl}/settings`; process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation] @@ -180,8 +180,8 @@ export class Common { * @param expectedUrl Expected link that is compared with actual */ static async checkURL(expectedUrl: string): Promise { - const getPageUrl = ClientFunction(() => window.location.href); - await t.expect(getPageUrl()).eql(expectedUrl, 'Opened URL is not correct'); + const getPageUrl = await this.getPageUrl(); + await t.expect(getPageUrl).eql(expectedUrl, 'Opened URL is not correct'); } /** @@ -189,8 +189,8 @@ export class Common { * @param expectedText Expected link that is compared with actual */ static async checkURLContainsText(expectedText: string): Promise { - const getPageUrl = ClientFunction(() => window.location.href); - await t.expect(getPageUrl()).contains(expectedText, `Opened URL not contains text ${expectedText}`); + const getPageUrl = await this.getPageUrl(); + await t.expect(getPageUrl).contains(expectedText, `Opened URL not contains text ${expectedText}`); } /** @@ -207,7 +207,7 @@ export class Common { * Get current page url */ static async getPageUrl(): Promise { - return (await ClientFunction(() => window.location.href))(); + return (ClientFunction(() => window.location.href))(); } /** @@ -247,7 +247,7 @@ export class Common { /** * Delete file from folder - * @param folderPath Path to file + * @param filePath Path to file */ static async deleteFileFromFolder(filePath: string): Promise { fs.unlinkSync(path.join(__dirname, filePath)); @@ -255,11 +255,29 @@ export class Common { /** * Delete file from folder if exists - * @param folderPath Path to file + * @param filePath Path to file */ static async deleteFileFromFolderIfExists(filePath: string): Promise { if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } + + /** + * Read file from folder + * @param filePath Path to file + */ + static async readFileFromFolder(filePath: string): Promise { + return fs.readFileSync(filePath, 'utf8'); + } + + /** + * Get current machine platform + */ + static getPlatform(): { isMac: boolean, isLinux: boolean } { + return { + isMac: process.platform === 'darwin', + isLinux: process.platform === 'linux' + }; + } } diff --git a/tests/e2e/helpers/conf.ts b/tests/e2e/helpers/conf.ts index 099823a22a..511d45540b 100644 --- a/tests/e2e/helpers/conf.ts +++ b/tests/e2e/helpers/conf.ts @@ -9,6 +9,8 @@ export const commonUrl = process.env.COMMON_URL || 'https://localhost:5540'; export const apiUrl = process.env.API_URL || 'https://localhost:5540/api'; export const googleUser = process.env.GOOGLE_USER || ''; export const googleUserPassword = process.env.GOOGLE_USER_PASSWORD || ''; +export const samlUser = process.env.E2E_SSO_EMAIL || ''; +export const samlUserPassword = process.env.E2E_SSO_PASSWORD || ''; export const workingDirectory = process.env.RI_APP_FOLDER_ABSOLUTE_PATH || (joinPath(os.homedir(), process.env.RI_APP_FOLDER_NAME || '.redis-insight')); diff --git a/tests/e2e/helpers/google-authorization.ts b/tests/e2e/helpers/google-authorization.ts deleted file mode 100644 index 82e411bfc1..0000000000 --- a/tests/e2e/helpers/google-authorization.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Builder, By, Key, until } from 'selenium-webdriver'; -import { googleUser, googleUserPassword } from './conf'; - -export async function processGoogleSSO(urlToUse: string): Promise { - // Create a WebDriver instance with ChromeDriver - const driver = await new Builder() - .forBrowser('chrome') - .build(); - - const protocol = 'redisinsight://'; - const callbackUrl = 'cloud/oauth/callback'; - - try { - await driver.get(urlToUse); - await driver.findElement(By.css('input[type="email"]')).sendKeys(googleUser, Key.RETURN); - await driver.wait(until.elementLocated(By.css('input[type="password"]')), 10000); - await driver.sleep(2000); - await driver.findElement(By.css('input[type="password"]')).sendKeys(googleUserPassword, Key.RETURN); - - // Wait for the authorization to complete and the redirect to your specified URI - await driver.wait(until.urlContains('#success'), 30000); - - const currentUrl = await driver.getCurrentUrl(); - const parts = currentUrl.split('?'); - const modifiedUrl = parts.length > 1 ? parts[1] : currentUrl; - const redirectUrl = `${protocol + callbackUrl }?${ modifiedUrl}`; - - // Open Redis Insight electron app using deeplink - const open = (await import('open')).default; - await open(redirectUrl, { app: { name: 'Redis Insight' } }); - } - catch (error) { - console.error('Error during Google SSO automation:', error); - } - finally { - await driver.quit(); - } -} diff --git a/tests/e2e/helpers/index.ts b/tests/e2e/helpers/index.ts index d7176a1254..0c5774181e 100644 --- a/tests/e2e/helpers/index.ts +++ b/tests/e2e/helpers/index.ts @@ -1,6 +1,7 @@ import { DatabaseScripts, DbTableParameters } from './database-scripts'; import { Common } from './common'; import { DatabaseHelper } from './database'; +import { SsoAuthorization } from './sso-authorization'; import { Telemetry } from './telemetry'; export { @@ -8,5 +9,6 @@ export { DbTableParameters, Common, DatabaseHelper, + SsoAuthorization, Telemetry }; diff --git a/tests/e2e/helpers/scripts/browser-scripts.ts b/tests/e2e/helpers/scripts/browser-scripts.ts index 906d2aa425..3a35a135b6 100644 --- a/tests/e2e/helpers/scripts/browser-scripts.ts +++ b/tests/e2e/helpers/scripts/browser-scripts.ts @@ -1,65 +1,179 @@ import { exec } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; +import CDP from 'chrome-remote-interface'; +import { promisify } from 'util'; +import { Common } from '../common'; + +interface Target { + type: string; + url: string; +} +const execPromise = promisify(exec); /** -* Open a new Chrome browser instance with a specific URL -* @param url The url to open -*/ -export function openChromeWithUrl(url: string = 'https://www.example.com') { - exec(`open -na "Google Chrome" --args --new-window "${url}"`, (error) => { - if (error) { - console.error('Error opening Chrome:', error); - return; + * Close Chrome browser instance + */ +export async function closeChrome(): Promise { + console.log('Closing Chrome...'); + try { + const { stdout, stderr } = await execPromise(`pkill chrome`); + console.log('Chrome closed successfully. stdout:', stdout); + if (stderr) { + console.error('stderr:', stderr); } - }); + } catch (error) { + console.error('Error closing Chrome:', error); + } } /** -* Retrieve opened tab in Google Chrome -* @param callback function to save opened tab -*/ -export function getOpenedChromeTab(callback: { (windows: string | NodeJS.ArrayBufferView): void; (arg0: string): void; }) { - const scriptPath = path.join(__dirname, 'get_chrome_tab_url.applescript'); - exec(`osascript ${scriptPath}`, (error, stdout) => { - if (error) { - console.error('Error retrieving tabs and windows:', error); + * Open a new Chrome browser instance + */ +export async function openChromeWindow(): Promise { + const { isMac, isLinux } = Common.getPlatform(); + + if (isMac) { + await execPromise(`open -na "Google Chrome" --args --new-window`); + console.log('Chrome opened on Mac'); + } else if (isLinux) { + console.log('Opening Chrome on Linux...'); + try { + exec(`google-chrome --remote-debugging-port=9223 --disable-gpu --disable-search-engine-choice-screen --disable-dev-shm-usage --disable-software-rasterizer --enable-logging --disable-extensions --no-default-browser-check --disable-default-apps --disable-domain-reliability --disable-web-security --no-sandbox --remote-allow-origins=* --disable-popup-blocking about:blank &`, (error, stdout, stderr) => { + if (error) { + console.error(`Error launching Chrome: ${error}`); + } else { + console.log("Chrome started successfully in the background."); + } + }); + } catch (error) { + console.error("Error occurred in execSync:", error); return; } - const windows = stdout.trim(); - callback(windows); - }); + + // Check if Chrome is running after opening it + const isChromeRunning = await waitForChromeProcess(); + if (isChromeRunning) { + console.log('Chrome is running.'); + } else { + console.error('Chrome process not found after attempting to launch.'); + } + } } /** -* Save opened chrome tab url to file -* @param logsFilePath The path to the file with logged url -* @param timeout The timeout for monitoring Chrome tabs -*/ -export function saveOpenedChromeTabUrl(logsFilePath: string, timeout: number = 1000) { - setTimeout(() => { - getOpenedChromeTab((windows: string | NodeJS.ArrayBufferView) => { - // Save the window information to a file - fs.writeFile(logsFilePath, windows, (err) => { - if (err) { - console.error('Error saving logs:', err); + * Waiting for chrome process start + * @param maxWaitTime Max waiting time + * @param interval Interval between check + */ +async function waitForChromeProcess(maxWaitTime = 10000, interval = 1000): Promise { + const start = Date.now(); + while (Date.now() - start < maxWaitTime) { + try { + const { stdout } = await execPromise(`pgrep "chrome"`); + if (stdout.trim()) { + return true; + } + } catch (error) { + // Ignore errors, Chrome may not be running yet + } + await new Promise(resolve => setTimeout(resolve, interval)); + } + return false; +} + +/** + * Retrieve opened tab in Google Chrome using Chrome DevTools Protocol + * @param urlSubstring Optional substring to match in the URL + * @returns Promise Resolves to the URL of the opened tab + */ +export async function getOpenedChromeTab(urlSubstring?: string): Promise { + const { isMac, isLinux } = Common.getPlatform(); + const maxRetries = 30; + const retryDelay = 400; + const chromeDebuggingPort = 9223; + + if (isMac) { + const scriptPath = path.join(__dirname, 'get_chrome_tab_url.applescript'); + return new Promise((resolve, reject) => { + exec(`osascript ${scriptPath}`, (error, stdout) => { + if (error) { + console.error('Error retrieving tabs and windows on macOS:', error); + reject(error); + return; } + resolve(stdout.trim()); }); }); - }, timeout); + } else if (isLinux) { + for (let attempts = 0; attempts < maxRetries; attempts++) { + console.log(`Attempting to connect to Chrome DevTools (Attempt: ${attempts + 1}/${maxRetries})...`); + + try { + const targets = await new Promise((resolve, reject) => { + CDP.List({ port: chromeDebuggingPort }, (err, targets) => { + if (err) { + console.error('Error connecting to Chrome with CDP:', err); + reject(err); + } else { + resolve(targets); + } + }); + }); + + const pageTargets = targets.filter(target => target.type === 'page'); + console.log(`Found ${pageTargets.length} open tabs in Chrome`); + console.log(`Found ${targets[0].url} url open tabs in Chrome`); + + // Check for a new tab matching criteria + const newTab = pageTargets.find(target => + (urlSubstring && target.url.includes(urlSubstring)) || + target.url.includes('authorize?') + ); + + if (newTab) { + console.log('Correct tab found:', newTab.url); + return newTab.url; + } else { + console.log('No matching tab found, retrying...'); + } + } catch (err) { + console.error('Error during Chrome connection attempt:', err); + } + + // Wait before the next attempt + await new Promise(resolve => setTimeout(resolve, retryDelay)); + } + + throw new Error('No new tab matching criteria was found within the maximum attempts.'); + } else { + throw new Error('Unsupported operating system: ' + process.platform); + } } /** -* Close the Chrome tab by prefix -*/ -export function closeChromeTabWithPrefix() { - const scriptPath = path.join(__dirname, 'close_chrome_tab.applescript'); - exec(`osascript ${scriptPath}`, (error, stdout, stderr) => { - if (error) { - console.error('Error closing tabs in Chrome:', error); - console.error('stdout:', stdout); - console.error('stderr:', stderr); - return; - } - }); + * Save opened chrome tab URL to file + * @param logsFilePath The path to the file with logged URL + * @param timeout The timeout for monitoring Chrome tabs + */ +export async function saveOpenedChromeTabUrl(logsFilePath: string, timeout = 100): Promise { + await new Promise(resolve => setTimeout(resolve, timeout)); + try { + const url = await getOpenedChromeTab(); + fs.writeFileSync(logsFilePath, url, 'utf8'); + } catch (err) { + console.error('Error saving logs:', err); + } +} + +/** + * Close Chrome browser instance + */ +export async function openChromeOnCi(): Promise { + await openChromeWindow(); + await new Promise(resolve => setTimeout(resolve, 1000)); + await closeChrome(); + await new Promise(resolve => setTimeout(resolve, 1000)); + await openChromeWindow(); + await new Promise(resolve => setTimeout(resolve, 1000)); } diff --git a/tests/e2e/helpers/sso-authorization.ts b/tests/e2e/helpers/sso-authorization.ts new file mode 100644 index 0000000000..be6f93604c --- /dev/null +++ b/tests/e2e/helpers/sso-authorization.ts @@ -0,0 +1,110 @@ +import { connect } from 'puppeteer-real-browser' +import * as fs from 'fs'; +import * as path from 'path'; +import { exec } from 'child_process'; +import { samlUser, samlUserPassword } from './conf'; +import { MyRedisDatabasePage, SsoAuthorizationPage } from '../pageObjects'; +import { Common } from './common'; +import { closeChrome, openChromeOnCi, saveOpenedChromeTabUrl } from './scripts/browser-scripts'; +import { t } from 'testcafe'; +import { AiChatBotPanel } from '../pageObjects/components/chatbot/ai-chatbot-panel'; + +export class SsoAuthorization { + /** + * Process SSO authorization using Puppeteer + * @param urlToUse The url to process authorization + * @param authorizationType The type of SSO authorization + */ + static async processSSOPuppeteer(urlToUse: string, authorizationType: 'Google' | 'Github' | 'SAML'): Promise { + const ssoAuthorizationPage = new SsoAuthorizationPage(); + const { browser, page } = await connect({ + headless: false, + args: [], + customConfig: {}, + turnstile: true, + connectOption: {}, + disableXvfb: true, + ignoreAllFlags: false, + }) + + try { + await ssoAuthorizationPage.signInUsingSso(authorizationType, page, urlToUse, samlUser, samlUserPassword); + + const currentUrl = page.url(); + const parts = currentUrl.split('?'); + const modifiedUrl = parts.length > 1 ? parts[1] : currentUrl; + + const protocol = 'redisinsight://'; + const callbackUrl = 'cloud/oauth/callback'; + const redirectUrl = `${protocol}${callbackUrl}?${modifiedUrl}`; + + await this.openRedisInsightWithDeeplink(redirectUrl); + } catch (error) { + console.error('Error during SSO:', error); + // Take a screenshot if there's an error + fs.mkdirSync('./report/screenshots/', { recursive: true }); + const screenshot = await page.screenshot(); + fs.writeFileSync(`./report/screenshots/puppeteer_screenshot_${Common.generateWord(5)}.png`, screenshot, 'base64'); + throw error; + } finally { + await browser.close(); + } + } + + /** + * Helper function for waiting for timeout + */ + static async waitForTimeout(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /** + * Open Redis Insight electron app using deeplink + * @param redirectUrl The redirect url for deeplink + */ + static async openRedisInsightWithDeeplink(redirectUrl: string) { + if (process.platform === 'linux') { + console.log('redirectUrl: ', redirectUrl); + exec(`xdg-open "${redirectUrl}"`, (error, stdout, stderr) => { + if (error) { + console.error('Error opening Redis Insight on Linux:', error); + return; + } + console.log('Redis Insight opened successfully:', stdout); + }); + } else { + const open = (await import('open')).default; + await open(redirectUrl, { app: { name: 'Redis Insight' } }); + } + } + + /** + * Sign in using SAML SSO + * @param urlToUse The url to process authorization + */ + static async signInThroughSamlSso(urlToUse: string): Promise { + const myRedisDatabasePage = new MyRedisDatabasePage(); + const aiChatBotPanel = new AiChatBotPanel(); + const logsWithUrlFilePath = path.join('test-data', 'chrome_logs.txt'); + + await openChromeOnCi(); + await t.click(myRedisDatabasePage.NavigationHeader.copilotButton); + await t.click(aiChatBotPanel.RedisCloudSigninPanel.oauthAgreement); + await t.click(aiChatBotPanel.RedisCloudSigninPanel.ssoOauthButton); + await t.typeText(aiChatBotPanel.RedisCloudSigninPanel.ssoEmailInput, samlUser, { replace: true, paste: true }); + + await t.wait(2000); + await t.click(aiChatBotPanel.RedisCloudSigninPanel.submitBtn); + await saveOpenedChromeTabUrl(logsWithUrlFilePath); + + await t.wait(2000); + urlToUse = await Common.readFileFromFolder(logsWithUrlFilePath); + await t.expect(urlToUse).contains('authorize?'); + await closeChrome(); + await t.wait(2000); + await this.processSSOPuppeteer(urlToUse, 'SAML'); + await t.expect(myRedisDatabasePage.NavigationHeader.cloudSignInButton.exists).notOk('Sign in button still displayed', { timeout: 10000 }); + await myRedisDatabasePage.reloadPage(); + await t.expect(myRedisDatabasePage.userProfileBtn.exists).ok('User profile button not displayed'); + } +} diff --git a/tests/e2e/package.json b/tests/e2e/package.json index f8ab7bbc05..72e775371c 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -13,7 +13,7 @@ "build:ui": "yarn --cwd ../../ build:ui", "redis:last": "docker run --name redis-last-version -p 7777:6379 -d redislabs/redismod", "start:app": "cross-env yarn start:api", - "test:chrome": "testcafe --compiler-options typescript.configPath=tsconfig.testcafe.json --cache --disable-multiple-windows --concurrency 1 chrome tests/ -r html:./report/report.html,spec -e -s takeOnFails=true,path=report/screenshots/,pathPattern=${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png", + "test:chrome": "ts-node ./web.runner.ts", "test:chrome:ci": "ts-node ./web.runner.ci.ts", "test": "yarn test:chrome", "lint": "eslint . --ext .ts,.js,.tsx,.jsx", @@ -31,7 +31,10 @@ "js-yaml": "^4.1.0", "lz4js": "^0.2.0", "msgpackr": "^1.11.0", - "protobufjs": "^7.4.0" + "protobufjs": "^7.4.0", + "puppeteer": "^23.7.0", + "puppeteer-extra": "^3.3.6", + "puppeteer-real-browser": "^1.3.17" }, "resolutions": { "@types/lodash": "4.14.192", @@ -43,9 +46,9 @@ "devDependencies": { "@types/archiver": "^6.0.2", "@types/chance": "1.1.6", + "@types/chrome-remote-interface": "^0.31.14", "@types/edit-json-file": "1.7.3", "@types/fs-extra": "11.0.4", - "@types/selenium-webdriver": "^4.1.26", "@types/sqlite3": "^3.1.11", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "8.9.0", @@ -62,10 +65,9 @@ "fs-extra": "^11.2.0", "open": "^10.1.0", "redis": "4.7.0", - "selenium-webdriver": "^4.25.0", "sqlite3": "^5.1.7", "supertest": "^7.0.0", - "testcafe": "3.6.2", + "testcafe": "3.7.0", "testcafe-browser-provider-electron": "0.0.21", "testcafe-reporter-html": "1.4.6", "testcafe-reporter-json": "2.2.0", diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index 7703027109..7f45b11a65 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -57,6 +57,7 @@ export class BrowserPage extends InstancePage { sortingButton = Selector('[data-testid=header-sorting-button]'); editJsonObjectButton = Selector('[data-testid=edit-json-field]'); applyEditButton = Selector('[data-testid=apply-edit-btn]'); + cancelEditButton = Selector('[data-testid=cancel-edit-btn]'); scanMoreButton = Selector('[data-testid=scan-more]'); resizeBtnKeyList = Selector('[data-test-subj=resize-btn-keyList-keyDetails]'); treeViewButton = Selector('[data-testid=view-type-list-btn]'); diff --git a/tests/e2e/pageObjects/components/browser/tree-view.ts b/tests/e2e/pageObjects/components/browser/tree-view.ts index 1a5c7d473c..0bf0fa7731 100644 --- a/tests/e2e/pageObjects/components/browser/tree-view.ts +++ b/tests/e2e/pageObjects/components/browser/tree-view.ts @@ -1,7 +1,10 @@ import { Selector, t } from 'testcafe'; import { Common } from '../../../helpers/common'; +import { FiltersDialog } from '../../dialogs'; export class TreeView { + FiltersDialog = new FiltersDialog(); + //------------------------------------------------------------------------------------------- //DECLARATION OF SELECTORS //*Declare all elements/components of the relevant page. @@ -10,17 +13,10 @@ export class TreeView { //------------------------------------------------------------------------------------------- //BUTTONS treeViewSettingsBtn = Selector('[data-testid=tree-view-settings-btn]'); - treeViewDelimiterValueSave = Selector('[data-testid=tree-view-apply-btn]'); - treeViewDelimiterValueCancel = Selector('[data-testid=tree-view-cancel-btn]'); - sortingBtn = Selector('[data-testid=tree-view-sorting-select]'); - sortingASCoption = Selector('[id=ASC]'); - sortingDESCoption = Selector('[id=DESC]'); sortingProgressBar = Selector('[data-testid=progress-key-tree]'); // TEXT ELEMENTS treeViewKeysNumber = Selector('[data-testid^=count_]'); treeViewDeviceFolder = Selector('[data-testid^=node-item_device] div'); - //INPUTS - treeViewDelimiterInput = Selector('[data-testid=tree-view-delimiter-input]'); /** * Get folder selector by folder name @@ -51,15 +47,16 @@ export class TreeView { /** * Change delimiter value - * @delimiter string with delimiter value + * @param delimiter string with delimiter value */ async changeDelimiterInTreeView(delimiter: string): Promise { // Open delimiter popup await t.click(this.treeViewSettingsBtn); + await this.FiltersDialog.clearDelimiterCombobox(); // Apply new value to the field - await t.typeText(this.treeViewDelimiterInput, delimiter, { replace: true, paste: true }); + await this.FiltersDialog.addDelimiterItem(delimiter); // Click on save button - await t.click(this.treeViewDelimiterValueSave); + await t.click(this.FiltersDialog.treeViewDelimiterValueSave); } /** @@ -69,13 +66,13 @@ export class TreeView { async changeOrderingInTreeView(order: string): Promise { // Open settings popup await t.click(this.treeViewSettingsBtn); - await t.click(this.sortingBtn); + await t.click(this.FiltersDialog.sortingBtn); order === 'ASC' - ? await t.click(this.sortingASCoption) - : await t.click(this.sortingDESCoption); + ? await t.click(this.FiltersDialog.sortingASCoption) + : await t.click(this.FiltersDialog.sortingDESCoption); // Click on save button - await t.click(this.treeViewDelimiterValueSave); + await t.click(this.FiltersDialog.treeViewDelimiterValueSave); await Common.waitForElementNotVisible(this.sortingProgressBar); } @@ -92,7 +89,7 @@ export class TreeView { * @param names folder names with sequence of subfolder */ async openTreeFolders(names: string[]): Promise { - let base = `node-item_${names[0]}:`; + let base = `node-item_${names[0]}`; await this.clickElementIfNotExpanded(base); if (names.length > 1) { for (let i = 1; i < names.length; i++) { diff --git a/tests/e2e/pageObjects/components/chatbot/ai-chatbot-panel.ts b/tests/e2e/pageObjects/components/chatbot/ai-chatbot-panel.ts index f2af2ece88..d1b960020b 100644 --- a/tests/e2e/pageObjects/components/chatbot/ai-chatbot-panel.ts +++ b/tests/e2e/pageObjects/components/chatbot/ai-chatbot-panel.ts @@ -2,8 +2,11 @@ import { Selector, t } from 'testcafe'; import { ChatBotTabs } from '../../../helpers/constants'; import { DatabaseChatBotTab } from './database-chatbot-tab'; import { GeneralChatBotTab } from './general-chatbot-tab'; +import { RedisCloudSigninPanel } from '../redis-cloud-sign-in-panel'; export class AiChatBotPanel { + RedisCloudSigninPanel = new RedisCloudSigninPanel(); + // CONTAINERS sidePanel = Selector('[data-testid=redis-copilot]'); copilotButton = Selector('[data-testid=]'); diff --git a/tests/e2e/pageObjects/components/explore-tab.ts b/tests/e2e/pageObjects/components/explore-tab.ts index c962654da5..a9161eba99 100644 --- a/tests/e2e/pageObjects/components/explore-tab.ts +++ b/tests/e2e/pageObjects/components/explore-tab.ts @@ -57,11 +57,11 @@ export class ExploreTab { * Run code * @param block Name of the block */ - async runBlockCode(block: string): Promise { + async runBlockCode(block: string): Promise { const runButton = Selector(this.runMask.replace(/\$name/g, block)); await t.scrollIntoView(runButton); await t.click(runButton); - if(await this.tutorialPopoverConfirmRunButton.exists){ + if (await this.tutorialPopoverConfirmRunButton.exists) { await t.click(this.tutorialPopoverConfirmRunButton); } } @@ -138,6 +138,7 @@ export class ExploreTab { if (await this.closeEnablementPage.exists) { await t.click(this.closeEnablementPage); } + await this.toggleMyTutorialPanel(); await t.click(deleteTutorialBtn); await t.click(this.tutorialDeleteButton); } @@ -149,4 +150,15 @@ export class ExploreTab { getTutorialByName(name: string): Selector { return Selector('div').withText(name); } + + /** + * Expand/Collapse My tutorial Panel + * @param state State of panel + */ + async toggleMyTutorialPanel(state: boolean = true): Promise { + const currentState = await this.customTutorials.getAttribute('aria-expanded') === 'true'; + if (currentState !== state) { + await t.click(this.customTutorials); + } + } } diff --git a/tests/e2e/pageObjects/components/myRedisDatabase/add-redis-database.ts b/tests/e2e/pageObjects/components/myRedisDatabase/add-redis-database.ts index 9fa2df2297..42e2bbbb44 100644 --- a/tests/e2e/pageObjects/components/myRedisDatabase/add-redis-database.ts +++ b/tests/e2e/pageObjects/components/myRedisDatabase/add-redis-database.ts @@ -1,7 +1,10 @@ import { Selector, t } from 'testcafe'; import { TlsCertificates } from '../../../helpers/constants'; +import { RedisCloudSigninPanel } from '../redis-cloud-sign-in-panel'; export class AddRedisDatabase { + RedisCloudSigninPanel = new RedisCloudSigninPanel(); + //------------------------------------------------------------------------------------------- //DECLARATION OF SELECTORS //*Declare all elements/components of the relevant page. diff --git a/tests/e2e/pageObjects/components/navigation/navigation-header.ts b/tests/e2e/pageObjects/components/navigation/navigation-header.ts index 03b7dbb7d4..0b9664da9e 100644 --- a/tests/e2e/pageObjects/components/navigation/navigation-header.ts +++ b/tests/e2e/pageObjects/components/navigation/navigation-header.ts @@ -4,6 +4,7 @@ import { InsightsPanel } from '../insights-panel'; export class NavigationHeader { insightsTriggerButton = Selector('[data-testid=insights-trigger]'); cloudSignInButton = Selector('[data-testid=cloud-sign-in-btn]'); + copilotButton = Selector('[data-testid=copilot-trigger]'); /** * Open/Close Panel diff --git a/tests/e2e/pageObjects/components/redis-cloud-sign-in-panel.ts b/tests/e2e/pageObjects/components/redis-cloud-sign-in-panel.ts new file mode 100644 index 0000000000..cb51583a31 --- /dev/null +++ b/tests/e2e/pageObjects/components/redis-cloud-sign-in-panel.ts @@ -0,0 +1,11 @@ +import { Selector } from 'testcafe'; + +export class RedisCloudSigninPanel { + ssoOauthButton = Selector('[data-testid=sso-oauth]'); + ssoEmailInput = Selector('[data-testid=sso-email]'); + submitBtn = Selector('[data-testid=btn-submit]'); + oauthAgreement = Selector('[for=ouath-agreement]'); + googleOauth = Selector('[data-testid=google-oauth]'); + githubOauth = Selector('[data-testid=github-oauth]'); + ssoOauth = Selector('[data-testid=sso-oauth]'); +} diff --git a/tests/e2e/pageObjects/dialogs/authorization-dialog.ts b/tests/e2e/pageObjects/dialogs/authorization-dialog.ts index de787bc50f..b472adc4dd 100644 --- a/tests/e2e/pageObjects/dialogs/authorization-dialog.ts +++ b/tests/e2e/pageObjects/dialogs/authorization-dialog.ts @@ -1,10 +1,9 @@ import { Selector } from 'testcafe'; +import { RedisCloudSigninPanel } from '../components/redis-cloud-sign-in-panel'; export class AuthorizationDialog { + RedisCloudSigninPanel = new RedisCloudSigninPanel(); + //COMPONENTS authDialog = Selector('[data-testid=social-oauth-dialog]'); - //BUTTONS - googleAuth = Selector('[data-testid=google-oauth]'); - gitHubAuth = Selector('[data-testid=github-oauth]'); - ssoAuth = Selector('[data-testid=sso-oauth]'); } diff --git a/tests/e2e/pageObjects/dialogs/filters-dialog.ts b/tests/e2e/pageObjects/dialogs/filters-dialog.ts new file mode 100644 index 0000000000..f640a60ad8 --- /dev/null +++ b/tests/e2e/pageObjects/dialogs/filters-dialog.ts @@ -0,0 +1,57 @@ +import { Selector, t } from 'testcafe'; + +export class FiltersDialog { + // INPUTS + delimiterCombobox = Selector('[data-testid=delimiter-combobox]'); + delimiterComboboxInput = Selector('[data-test-subj=comboBoxSearchInput]'); + // BUTTONS + treeViewDelimiterValueCancel = Selector('[data-testid=tree-view-cancel-btn]'); + treeViewDelimiterValueSave = Selector('[data-testid=tree-view-apply-btn]'); + sortingBtn = Selector('[data-testid=tree-view-sorting-select]'); + sortingASCoption = Selector('[id=ASC]'); + sortingDESCoption = Selector('[id=DESC]'); + + /** + * Get Delimiter badge selector by title + * @param delimiterTitle title of the delimiter item + */ + getDelimiterBadgeByTitle(delimiterTitle: string): Selector { + return this.delimiterCombobox.find(`span[title='${delimiterTitle}']`); + } + + /** + * Get Delimiter close button selector by title + * @param delimiterTitle title of the delimiter item + */ + getDelimiterCloseBtnByTitle(delimiterTitle: string): Selector { + return this.getDelimiterBadgeByTitle(delimiterTitle).find('button'); + } + + /** + * Add new delimiter + * @param delimiterName name of the delimiter item + */ + async addDelimiterItem(delimiterName: string): Promise { + await t.click(this.delimiterComboboxInput); + await t.typeText(this.delimiterComboboxInput, delimiterName, { paste: true }) + } + + /** + * Delete existing delimiter + * @param delimiterName name of the delimiter item + */ + async removeDelimiterItem(delimiterName: string): Promise { + await t.click(this.getDelimiterCloseBtnByTitle(delimiterName)); + } + + /** + * Remove all existing delimiters in combobox + */ + async clearDelimiterCombobox(): Promise { + const delimiters = this.delimiterCombobox.find('button'); + const count = await delimiters.count; + for (let i = 0; i < count; i++) { + await t.click(delimiters.nth(i)); + } + } +} diff --git a/tests/e2e/pageObjects/dialogs/index.ts b/tests/e2e/pageObjects/dialogs/index.ts index c96a11a7c2..73c4d96b82 100644 --- a/tests/e2e/pageObjects/dialogs/index.ts +++ b/tests/e2e/pageObjects/dialogs/index.ts @@ -1,7 +1,9 @@ import { OnboardingCardsDialog } from './onboarding-cards-dialog'; +import { FiltersDialog } from './filters-dialog'; import { UserAgreementDialog } from './user-agreement-dialog'; export { OnboardingCardsDialog, + FiltersDialog, UserAgreementDialog }; diff --git a/tests/e2e/pageObjects/index.ts b/tests/e2e/pageObjects/index.ts index dba65891de..2d03ce5cef 100644 --- a/tests/e2e/pageObjects/index.ts +++ b/tests/e2e/pageObjects/index.ts @@ -7,6 +7,7 @@ import { MemoryEfficiencyPage } from './memory-efficiency-page'; import { ClusterDetailsPage } from './cluster-details-page'; import { PubSubPage } from './pub-sub-page'; import { SlowLogPage } from './slow-log-page'; +import { SsoAuthorizationPage } from './sso-authorization-page'; import { BasePage } from './base-page'; import { InstancePage } from './instance-page'; @@ -20,6 +21,7 @@ export { ClusterDetailsPage, PubSubPage, SlowLogPage, + SsoAuthorizationPage, BasePage, InstancePage, }; diff --git a/tests/e2e/pageObjects/my-redis-databases-page.ts b/tests/e2e/pageObjects/my-redis-databases-page.ts index 0696a99fa5..641df04d90 100644 --- a/tests/e2e/pageObjects/my-redis-databases-page.ts +++ b/tests/e2e/pageObjects/my-redis-databases-page.ts @@ -7,6 +7,7 @@ import { BaseOverviewPage } from './base-overview-page'; import { NavigationPanel } from './components/navigation-panel'; import { NavigationHeader } from './components/navigation/navigation-header'; import { AuthorizationDialog } from './dialogs/authorization-dialog'; +import { RedisCloudSigninPanel } from './components/redis-cloud-sign-in-panel'; const databaseAPIRequests = new DatabaseAPIRequests(); @@ -55,13 +56,13 @@ export class MyRedisDatabasePage extends BaseOverviewPage { exportPasswordsCheckbox = Selector('[data-testid=export-passwords]~div', { timeout: 500 }); //ICONS moduleColumn = Selector('[data-test-subj=tableHeaderCell_modules_3]'); - moduleSearchIcon = Selector('[data-testid^=RediSearch]'); - moduleGraphIcon = Selector('[data-testid^=RedisGraph]'); - moduleJSONIcon = Selector('[data-testid^=RedisJSON]'); - moduleTimeseriesIcon = Selector('[data-testid^=RedisTimeSeries]'); - moduleBloomIcon = Selector('[data-testid^=RedisBloom]'); - moduleAIIcon = Selector('[data-testid^=RedisAI]'); - moduleGearsIcon = Selector('[data-testid^=RedisGears]'); + moduleSearchIcon = Selector("[data-testid^='Redis Query Engine']"); + moduleGraphIcon = Selector('[data-testid^=Graph]'); + moduleJSONIcon = Selector('[data-testid^=JSON]'); + moduleTimeseriesIcon = Selector("[data-testid^='Time Series']"); + moduleBloomIcon = Selector('[data-testid^=Probabilistic]'); + moduleAIIcon = Selector('[data-testid^=AI]'); + moduleGearsIcon = Selector('[data-testid^=Gears]'); redisStackIcon = Selector('[data-testid=redis-stack-icon]'); tooltipRedisStackLogo = Selector('[data-testid=tooltip-redis-stack-icon]'); //TEXT INPUTS (also referred to as 'Text fields') @@ -91,9 +92,6 @@ export class MyRedisDatabasePage extends BaseOverviewPage { databaseContainer = Selector('.databaseContainer'); connectionTypeTitle = Selector('[data-test-subj=tableHeaderCell_connectionType_2]'); signInAgreement = Selector('[class="euiCheckbox__square"]'); - googleAuth = Selector('[data-testid=google-oauth]'); - gitHubAuth = Selector('[data-testid=github-oauth]'); - ssoAuth = Selector('[data-testid=sso-oauth]'); /** * Click on the database by name diff --git a/tests/e2e/pageObjects/sso-authorization-page.ts b/tests/e2e/pageObjects/sso-authorization-page.ts new file mode 100644 index 0000000000..75d0b7ba35 --- /dev/null +++ b/tests/e2e/pageObjects/sso-authorization-page.ts @@ -0,0 +1,110 @@ +import { PageWithCursor } from "puppeteer-real-browser"; +import { SsoAuthorization } from "../helpers"; + +export class SsoAuthorizationPage { + // BUTTONS + submitFormButton = 'input[type="submit"]'; + tryAnotherWayButton = `//*[text()='Try another way']` + googleNextButton = '#identifierNext'; + googleSubmitPasswordButton = '#passwordNext'; + // INPUTS + oktaUserNameInput = 'input[autocomplete="username"]'; + oktaPasswordInput = 'input[autocomplete="current-password"]'; + googleEmailInput = 'input[type="email"]'; + goooglePasswordInput = 'input[type="password"]'; + githubUserNameInput = '#login_field'; + githubPasswordInput = '#password'; + + /** + * Sign in using SSO + * @param authorizationType The authorization page type 'Google' || 'Github' || 'SAML' + * @param page Puppeteer page instance + * @param urlToUse The url to process authorization + * @param username The username to okta account + * @param password The password to okta account + */ + async signInUsingSso(authorizationType: 'Google' | 'Github' | 'SAML', page: PageWithCursor, urlToUse: string, username: string, password: string): Promise { + await page.goto(urlToUse); + await SsoAuthorization.waitForTimeout(2000); + + + switch (authorizationType) { + case 'SAML': + await this.submitOktaForm(page, username, password); + break; + case 'Google': + await this.submitGoogleForm(page, username, password); + break; + case 'Github': + await this.submitGithubForm(page, username, password); + break; + default: + throw new Error(`Unsupported authorization type: ${authorizationType}`); + } + + await SsoAuthorization.waitForTimeout(2000); + // Wait for the authorization to complete + await page.waitForFunction(() => window.location.href.includes('#success'), { timeout: 11000 }); + } + + /** + * Submit login OKTA form + * @param urlToUse The url to process authorization + * @param username The username to okta account + * @param password The password to okta account + */ + async submitOktaForm(page: PageWithCursor, username: string, password: string): Promise { + await page.waitForSelector(this.oktaUserNameInput, { visible: true }); + await page.type(this.oktaUserNameInput, username, { delay: Math.random() * 100 + 50 }); + await page.type(this.oktaPasswordInput, password, { delay: Math.random() * 100 + 50 }); + await page.click(this.submitFormButton); + } + + /** + * Submit login Google form + * @param page Puppeteer page instance + * @param username The username to okta account + * @param password The password to okta account + */ + async submitGoogleForm(page: PageWithCursor, username: string, password: string): Promise { + await page.waitForSelector(this.googleEmailInput, { visible: true }); + await page.type(this.googleEmailInput, username, { delay: Math.random() * 100 + 50 }); + await Promise.all([ + page.click(this.googleNextButton), + page.waitForNavigation({ waitUntil: 'networkidle0' }) + ]); + await page.waitForSelector(this.goooglePasswordInput, { visible: true }); + await SsoAuthorization.waitForTimeout(500); + + await page.type(this.goooglePasswordInput, password, { delay: Math.random() * 100 + 50 }); + await Promise.all([ + page.click(this.googleSubmitPasswordButton), + page.waitForNavigation({ waitUntil: 'networkidle0' }) + ]); + await SsoAuthorization.waitForTimeout(500); + + // Check for "Try another way" button + const tryAnotherWayButtons = await page.$$(this.tryAnotherWayButton); + if (tryAnotherWayButtons.length > 0) { + const buttonVisible = await tryAnotherWayButtons[0].isIntersectingViewport(); + if (buttonVisible) { + await tryAnotherWayButtons[0].click(); + } else { + console.log("'Try another way' button not found or not visible."); + } + } + } + + /** + * Submit login GitHub form + * @param urlToUse The url to process authorization + * @param username The username to okta account + * @param password The password to okta account + */ + async submitGithubForm(page: PageWithCursor, username: string, password: string): Promise { + await page.waitForSelector(this.githubUserNameInput, { visible: true }); + await page.type(this.githubUserNameInput, username, { delay: Math.random() * 100 + 50 }); + await page.type(this.githubPasswordInput, password, { delay: Math.random() * 100 + 50 }); + await page.click(this.submitFormButton); + } +} diff --git a/tests/e2e/test-data/big-json/json-BigInt.json b/tests/e2e/test-data/big-json/json-BigInt.json new file mode 100644 index 0000000000..88ac3252b3 --- /dev/null +++ b/tests/e2e/test-data/big-json/json-BigInt.json @@ -0,0 +1,48 @@ +{ + "result": [ + { + "message": 1234567899876543211111111, + "phoneNumber": "(870) 933-5398 x28029", + "phoneVariation": "+90 363 988 10 51", + "status": "disabled", + "name": { + "first": "Hudson", + "middle": "Marlowe", + "last": "Romaguera" + }, + "username": "Hudson-Romaguera", + "password": "15dmjQlqS2ZBhVq", + "emails": [ + "Van11@gmail.com", + "Elissa.VonRueden82@example.com" + ], + "location": { + "street": "9109 2nd Avenue", + "city": "Howeview", + "state": "North Dakota", + "country": "Saint Helena", + "zip": "62862-7589", + "coordinates": { + "latitude": "81.1514", + "longitude": "-134.2206" + } + }, + "website": "https://skinny-barn.info", + "domain": "responsible-notebook.net", + "job": { + "title": "Internal Solutions Officer", + "descriptor": "Principal", + "area": "Branding", + "type": "Developer", + "company": "Shields Group" + }, + "creditCard": { + "number": "3409-218740-43272", + "cvv": "909", + "issuer": "visa" + }, + "uuid": "91d6042d-8453-440f-9bf1-7412fee085ee", + "objectId": 123456789987654321 + } + ] +} \ No newline at end of file diff --git a/tests/e2e/tests/electron/regression/database/cloud-sso.e2e.ts b/tests/e2e/tests/electron/regression/database/cloud-sso.e2e.ts index b404bf2137..e8597937d8 100644 --- a/tests/e2e/tests/electron/regression/database/cloud-sso.e2e.ts +++ b/tests/e2e/tests/electron/regression/database/cloud-sso.e2e.ts @@ -1,23 +1,25 @@ import * as path from 'path'; -import * as fs from 'fs'; import { MyRedisDatabasePage } from '../../../../pageObjects'; import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { commonUrl } from '../../../../helpers/conf'; +import { commonUrl, samlUser } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { modifyFeaturesConfigJson, updateControlNumber } from '../../../../helpers/insights'; -import { processGoogleSSO } from '../../../../helpers/google-authorization'; -import { openChromeWithUrl, saveOpenedChromeTabUrl } from '../../../../helpers/scripts/browser-scripts'; +import { closeChrome, openChromeOnCi, openChromeWindow, saveOpenedChromeTabUrl } from '../../../../helpers/scripts/browser-scripts'; +import { Common, SsoAuthorization } from '../../../../helpers'; +import { AiChatBotPanel } from '../../../../pageObjects/components/chatbot/ai-chatbot-panel'; const myRedisDatabasePage = new MyRedisDatabasePage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const aiChatBotPanel = new AiChatBotPanel(); let urlToUse = ''; const pathes = { defaultRemote: path.join('.', 'test-data', 'features-configs', 'insights-default-remote.json'), electronConfig: path.join('.', 'test-data', 'features-configs', 'sso-electron-build.json') }; +const logsWithUrlFilePath = path.join('test-data', 'chrome_logs.txt'); fixture `Cloud SSO` .meta({ type: 'regression', rte: rte.standalone }) @@ -30,6 +32,7 @@ fixture `Cloud SSO` await updateControlNumber(48.2); }) .afterEach(async() => { + await Common.deleteFileFromFolderIfExists(logsWithUrlFilePath); await databaseAPIRequests.deleteAllDatabasesApi(); }); test('Verify that user can see SSO feature if it is enabled in feature config', async t => { @@ -52,36 +55,78 @@ test('Verify that user can see SSO feature if it is enabled in feature config', await t.expect(myRedisDatabasePage.AddRedisDatabase.useCloudKeys.exists).ok('Use Cloud Keys accordion not displayed when SSO feature enabled'); await t.click(myRedisDatabasePage.AddRedisDatabase.useCloudAccount); // Verify that Auth buttons are displayed for auto-discovery panel on Electron app - await t.expect(myRedisDatabasePage.googleAuth.exists).ok('Google auth button not displayed when SSO feature enabled'); - await t.expect(myRedisDatabasePage.gitHubAuth.exists).ok('Github auth button not displayed when SSO feature enabled'); - await t.expect(myRedisDatabasePage.ssoAuth.exists).ok('SSO auth button not displayed when SSO feature enabled'); + await t.expect(myRedisDatabasePage.AddRedisDatabase.RedisCloudSigninPanel.googleOauth.exists).ok('Google auth button not displayed when SSO feature enabled'); + await t.expect(myRedisDatabasePage.AddRedisDatabase.RedisCloudSigninPanel.githubOauth.exists).ok('Github auth button not displayed when SSO feature enabled'); + await t.expect(myRedisDatabasePage.AddRedisDatabase.RedisCloudSigninPanel.ssoOauth.exists).ok('SSO auth button not displayed when SSO feature enabled'); }); -// skip until adding linux support -test.skip('Verify that user can sign in using SSO via Google authorization', async t => { - const logsFilename = 'chrome_logs.txt'; - const logsFilePath = path.join('test-data', logsFilename); +// skip until adding tests for SSO feature +test.skip('Verify that user can sign in using SSO SAML auth', async t => { + // Open Chrome with a sample URL and save it to logs file + await openChromeOnCi(); + await t.click(myRedisDatabasePage.NavigationHeader.copilotButton); + await t.click(aiChatBotPanel.RedisCloudSigninPanel.oauthAgreement); + await t.click(aiChatBotPanel.RedisCloudSigninPanel.ssoOauthButton); + await t.typeText(aiChatBotPanel.RedisCloudSigninPanel.ssoEmailInput, samlUser, { replace: true, paste: true }); + await t.click(aiChatBotPanel.RedisCloudSigninPanel.submitBtn); + await saveOpenedChromeTabUrl(logsWithUrlFilePath); + await t.wait(2000); + urlToUse = await Common.readFileFromFolder(logsWithUrlFilePath); + await t.expect(urlToUse).contains('authorize?'); + await closeChrome(); + await SsoAuthorization.processSSOPuppeteer(urlToUse, 'SAML'); + await t.expect(myRedisDatabasePage.NavigationHeader.cloudSignInButton.exists).notOk('Sign in button still displayed', { timeout: 10000 }); + await myRedisDatabasePage.reloadPage(); + await t.expect(myRedisDatabasePage.userProfileBtn.exists).ok('User profile button not displayed'); + await t.click(myRedisDatabasePage.userProfileBtn); + await t.expect(myRedisDatabasePage.userProfileAccountInfo.textContent).contains('Geor', 'User not signed in'); +}); +// Can be run only locally for google auth +test.skip('Verify that user can sign in using SSO Google auth', async t => { await t.expect(myRedisDatabasePage.promoButton.exists).ok('Import Cloud database button not displayed when SSO feature enabled'); - await t.click(myRedisDatabasePage.NavigationHeader.cloudSignInButton); - // Navigate to Google Auth button - await t.pressKey('tab'); - await t.pressKey('tab'); - await t.pressKey('space'); - await t.pressKey('shift+tab'); - await t.pressKey('shift+tab'); - // Open Chrome with a sample URL and save it to logs file - openChromeWithUrl(); - saveOpenedChromeTabUrl(logsFilePath); + await openChromeWindow(); + await t.wait(2000); + await t.click(myRedisDatabasePage.NavigationHeader.copilotButton); + await t.click(aiChatBotPanel.RedisCloudSigninPanel.oauthAgreement); + + await t.wait(2000); + await saveOpenedChromeTabUrl(logsWithUrlFilePath); // Click the button to trigger the Google authorization page - await t.pressKey('enter'); + await t.click(aiChatBotPanel.RedisCloudSigninPanel.googleOauth); + + await t.wait(2000); + urlToUse = await Common.readFileFromFolder(logsWithUrlFilePath); + await t.expect(urlToUse).contains('authorize?'); + await closeChrome(); + await SsoAuthorization.processSSOPuppeteer(urlToUse, 'Google'); + await t.expect(myRedisDatabasePage.NavigationHeader.cloudSignInButton.exists).notOk('Sign in button still displayed', { timeout: 10000 }); + await myRedisDatabasePage.reloadPage(); + await t.expect(myRedisDatabasePage.userProfileBtn.exists).ok('User profile button not displayed'); + await t.click(myRedisDatabasePage.userProfileBtn); + await t.expect(myRedisDatabasePage.userProfileAccountInfo.textContent).contains('Geor', 'User not signed in'); +}); +// Can be run only locally for github auth +test.skip('Verify that user can sign in using SSO Github auth', async t => { + // Open Chrome with a sample URL and save it to logs file + await openChromeWindow(); + await t.wait(1000); + await t.click(myRedisDatabasePage.NavigationHeader.copilotButton); + await t.click(aiChatBotPanel.RedisCloudSigninPanel.oauthAgreement); + await t.wait(2000); + await saveOpenedChromeTabUrl(logsWithUrlFilePath); + // Click the button to trigger the Github authorization page + await t.click(aiChatBotPanel.RedisCloudSigninPanel.githubOauth); - urlToUse = fs.readFileSync(logsFilePath, 'utf8'); - await processGoogleSSO(urlToUse); + await t.wait(2000); + urlToUse = await Common.readFileFromFolder(logsWithUrlFilePath); + await t.expect(urlToUse).contains('authorize?'); + await closeChrome(); + await SsoAuthorization.processSSOPuppeteer(urlToUse, 'Github'); await t.expect(myRedisDatabasePage.NavigationHeader.cloudSignInButton.exists).notOk('Sign in button still displayed', { timeout: 10000 }); await myRedisDatabasePage.reloadPage(); await t.expect(myRedisDatabasePage.userProfileBtn.exists).ok('User profile button not displayed'); await t.click(myRedisDatabasePage.userProfileBtn); - await t.expect(myRedisDatabasePage.userProfileAccountInfo.textContent).contains('ri-sso-test-1', 'User not signed in'); + await t.expect(myRedisDatabasePage.userProfileAccountInfo.textContent).contains('Geor', 'User not signed in'); }); diff --git a/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts index 27db258c3e..0c6dcde6d0 100644 --- a/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts @@ -180,14 +180,14 @@ test }) .after(async() => { await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV5Config); - })('No RediSearch module message', async t => { - const noRedisearchMessage = 'RediSearch is not available for this database'; + })('No Redis Query Engine module message', async t => { + const noRedisearchMessage = 'Redis Query Engine is not available for this database'; const externalPageLinkFirst = 'https://redis.io/try-free'; const externalPageLinkSecond = '?utm_source=redisinsight&utm_medium=app&utm_campaign=redisinsight_browser_search' await t.click(browserPage.redisearchModeBtn); - // Verify that user can see message in the dialog when he doesn't have RediSearch module - await t.expect(browserPage.noReadySearchDialogTitle.textContent).contains(noRedisearchMessage, 'Invalid text in no redisearch popover'); + // Verify that user can see message in the dialog when he doesn't have Redis Query Engine module + await t.expect(browserPage.noReadySearchDialogTitle.textContent).contains(noRedisearchMessage, 'Invalid text in no Redis Query Engine popover'); // Verify that user can navigate by link to create a Redis db await t.click(browserPage.redisearchFreeLink); diff --git a/tests/e2e/tests/web/critical-path/database/modules.e2e.ts b/tests/e2e/tests/web/critical-path/database/modules.e2e.ts index c7855dfeb9..33040e685a 100644 --- a/tests/e2e/tests/web/critical-path/database/modules.e2e.ts +++ b/tests/e2e/tests/web/critical-path/database/modules.e2e.ts @@ -11,7 +11,7 @@ const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); const chance = new Chance(); -const moduleNameList = ['RediSearch', 'RedisJSON', 'RedisGraph', 'RedisTimeSeries', 'RedisBloom', 'RedisGears', 'RedisAI']; +const moduleNameList = ['Redis Query Engine', 'JSON', 'Graph', 'Time Series', 'Probabilistic', 'Gears', 'AI']; const moduleList = [myRedisDatabasePage.moduleSearchIcon, myRedisDatabasePage.moduleJSONIcon, myRedisDatabasePage.moduleGraphIcon, myRedisDatabasePage.moduleTimeseriesIcon, myRedisDatabasePage.moduleBloomIcon, myRedisDatabasePage.moduleGearsIcon, myRedisDatabasePage.moduleAIIcon]; const uniqueId = chance.string({ length: 10 }); let database = { @@ -42,7 +42,7 @@ test('Verify that user can see DB modules on DB list page for Standalone DB', as // Verify that user can see the following sorting order: Search, JSON, Graph, TimeSeries, Bloom, Gears, AI for modules const databaseLine = myRedisDatabasePage.dbNameList.withExactText(database.databaseName).parent('tr'); await t.expect(databaseLine.visible).ok('Database not found in db list'); - const moduleIcons = databaseLine.find('[data-testid^=Redi]'); + const moduleIcons = databaseLine.find('[data-testid*=_module]'); const numberOfIcons = await moduleIcons.count; for (let i = 0; i < numberOfIcons; i++) { const moduleName = await moduleIcons.nth(i).getAttribute('data-testid'); diff --git a/tests/e2e/tests/web/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/web/critical-path/memory-efficiency/recommendations.e2e.ts index 12849d4b04..753896558e 100644 --- a/tests/e2e/tests/web/critical-path/memory-efficiency/recommendations.e2e.ts +++ b/tests/e2e/tests/web/critical-path/memory-efficiency/recommendations.e2e.ts @@ -12,6 +12,7 @@ import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { RecommendationsActions } from '../../../../common-actions/recommendations-actions'; import { Common } from '../../../../helpers/common'; import { APIKeyRequests } from '../../../../helpers/api/api-keys'; +import { Telemetry } from '../../../../helpers'; const memoryEfficiencyPage = new MemoryEfficiencyPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); @@ -21,6 +22,16 @@ const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); const apiKeyRequests = new APIKeyRequests(); +const telemetry = new Telemetry(); + +const logger = telemetry.createLogger(); + +const telemetryEvent = 'DATABASE_ANALYSIS_TIPS_COLLAPSED'; +const expectedProperties = [ + 'databaseId', + 'provider', + 'recommendation' +]; // const externalPageLink = 'https://docs.redis.com/latest/ri/memory-optimizations/'; let keyName = `recomKey-${Common.generateWord(10)}`; @@ -46,6 +57,7 @@ fixture `Memory Efficiency Recommendations` await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test + .requestHooks(logger) .before(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); // Go to Analysis Tools page @@ -82,6 +94,10 @@ test // Verify that user can expand/collapse recommendation const expandedTextContaiterSize = await memoryEfficiencyPage.getRecommendationByName(luaScriptRecommendation).offsetHeight; await t.click(memoryEfficiencyPage.getRecommendationButtonByName(luaScriptRecommendation)); + + //Verify telemetry event + await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger); + await t.expect(memoryEfficiencyPage.getRecommendationByName(luaScriptRecommendation).offsetHeight) .lt(expandedTextContaiterSize, 'Lua script recommendation not collapsed'); await t.click(memoryEfficiencyPage.getRecommendationButtonByName(luaScriptRecommendation)); diff --git a/tests/e2e/tests/web/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts b/tests/e2e/tests/web/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts index 450e698f1b..f862c457e5 100644 --- a/tests/e2e/tests/web/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts +++ b/tests/e2e/tests/web/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts @@ -4,6 +4,7 @@ import { commonUrl, ossStandaloneConfig, ossStandaloneV5Config } from '../../../ import { rte } from '../../../../helpers/constants'; import { verifyMessageDisplayingInPubSub } from '../../../../helpers/pub-sub'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { Telemetry } from '../../../../helpers'; const myRedisDatabasePage = new MyRedisDatabasePage(); const pubSubPage = new PubSubPage(); @@ -11,6 +12,16 @@ const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); const browserPage = new BrowserPage(); +const telemetry = new Telemetry(); + +const logger = telemetry.createLogger(); + +const telemetryEvent = 'PUBSUB_MESSAGES_CLEARED'; +const expectedProperties = [ + 'databaseId', + 'messages', + 'provider' +]; fixture `Subscribe/Unsubscribe from a channel` .meta({ rte: rte.standalone, type: 'critical_path' }) @@ -145,13 +156,17 @@ test('Verify that the Message field input is preserved until user Publish a mess // Verify that the Channel field input is preserved until user modify it (publishing a message does not clear the field) await t.expect(pubSubPage.channelNameInput.value).eql('testChannel', 'Channel input is empty', { timeout: 10000 }); }); -test('Verify that user can clear all the messages from the pubsub window', async t => { +test.requestHooks(logger)('Verify that user can clear all the messages from the pubsub window', async t => { await pubSubPage.subsribeToChannelAndPublishMessage('testChannel', 'message'); await pubSubPage.publishMessage('testChannel2', 'second m'); // Verify the tooltip text 'Clear Messages' appears on hover the clear button await t.hover(pubSubPage.clearPubSubButton); await t.expect(pubSubPage.clearButtonTooltip.textContent).contains('Clear Messages', 'Clear Messages tooltip not displayed'); await t.click(pubSubPage.clearPubSubButton); + + //Verify telemetry event + await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger); + // Verify that the clear of the messages does not affect the subscription state await t.expect(pubSubPage.subscribeStatus.textContent).eql('You are subscribed', 'User is not subscribed', { timeout: 10000 }); // Verify that the Messages counter is reset after clear messages diff --git a/tests/e2e/tests/web/critical-path/rdi/add-rdi-instance.e2e.ts b/tests/e2e/tests/web/critical-path/rdi/add-rdi-instance.e2e.ts index ee8d89a77a..5d033aa07c 100644 --- a/tests/e2e/tests/web/critical-path/rdi/add-rdi-instance.e2e.ts +++ b/tests/e2e/tests/web/critical-path/rdi/add-rdi-instance.e2e.ts @@ -7,7 +7,7 @@ import { RdiInstancePage } from '../../../../pageObjects/rdi-instance-page'; import { commonUrl } from '../../../../helpers/conf'; import { RdiPopoverOptions, RedisOverviewPage } from '../../../../helpers/constants'; import { MyRedisDatabasePage } from '../../../../pageObjects'; -import { Common, DatabaseHelper } from '../../../../helpers'; +import { Common, DatabaseHelper, Telemetry } from '../../../../helpers'; import { RdiApiRequests } from '../../../../helpers/api/api-rdi'; import { goBackHistory } from '../../../../helpers/utils'; @@ -18,6 +18,22 @@ const myRedisDatabasePage = new MyRedisDatabasePage(); const databaseHelper = new DatabaseHelper(); const rdiApiRequests = new RdiApiRequests(); +const telemetry = new Telemetry(); + +const logger = telemetry.createLogger(); + +const telemetryEvents = ['RDI_INSTANCE_LIST_SEARCHED','RDI_START_OPTION_SELECTED']; + +const instanceExpectedProperties = [ + 'instancesFullCount', + 'instancesSearchedCount' +]; + +const pipelineExpectedProperties = [ + 'id', + 'option' +]; + const rdiInstance: RdiInstance = { alias: 'Alias', url: 'https://11.111.111.111', @@ -104,19 +120,24 @@ test('Verify that user can add and remove RDI', async() => { await t.expect(rdiInstancesListPage.emptyRdiList.textContent).contains('Redis Data Integration', 'The instance is not removed'); }); -test +test.requestHooks(logger) .after(async() => { await rdiInstancesListPage.deleteAllInstance(); })('Verify that user can search by RDI', async() => { await rdiInstancesListPage.addRdi(rdiInstance); await rdiInstancesListPage.addRdi(rdiInstance2); await t.typeText(rdiInstancesListPage.searchInput, rdiInstance2.alias); + + //Verify telemetry event + await telemetry.verifyEventHasProperties(telemetryEvents[0], instanceExpectedProperties, logger); + const addedRdiInstance = await rdiInstancesListPage.getRdiInstanceValuesByIndex(0); await t.expect(addedRdiInstance.alias).eql(rdiInstance2.alias, 'correct item is displayed'); await t.expect(await rdiInstancesListPage.rdiInstanceRow.count).eql(1, 'search works incorrectly'); }); -test('Verify that sorting on the list of rdi saved when rdi opened', async t => { +test.requestHooks(logger) +('Verify that sorting on the list of rdi saved when rdi opened', async t => { // Sort by Connection Type await rdiInstancesListPage.addRdi(rdiInstance); await rdiInstancesListPage.addRdi(rdiInstance3); @@ -128,6 +149,10 @@ test('Verify that sorting on the list of rdi saved when rdi opened', async t => await rdiInstancesListPage.compareInstances(actualDatabaseList, sortedByAlias); await rdiInstancesListPage.clickRdiByName(rdiInstance.alias); await rdiInstancePage.selectStartPipelineOption(RdiPopoverOptions.Pipeline); + + //verify telemetry event + await telemetry.verifyEventHasProperties(telemetryEvents[1], pipelineExpectedProperties, logger); + await t.click(rdiInstancePage.RdiHeader.breadcrumbsLink); actualDatabaseList = await rdiInstancesListPage.getAllRdiNames(); await rdiInstancesListPage.compareInstances(actualDatabaseList, sortedByAlias); diff --git a/tests/e2e/tests/web/critical-path/settings/settings.e2e.ts b/tests/e2e/tests/web/critical-path/settings/settings.e2e.ts index f48392a55d..ff81c682ff 100644 --- a/tests/e2e/tests/web/critical-path/settings/settings.e2e.ts +++ b/tests/e2e/tests/web/critical-path/settings/settings.e2e.ts @@ -2,10 +2,21 @@ import { MyRedisDatabasePage, SettingsPage } from '../../../../pageObjects'; import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { commonUrl } from '../../../../helpers/conf'; +import { Common, Telemetry } from '../../../../helpers'; const myRedisDatabasePage = new MyRedisDatabasePage(); const settingsPage = new SettingsPage(); const databaseHelper = new DatabaseHelper(); +const telemetry = new Telemetry(); + +const logger = telemetry.createLogger(); + +const telemetryEvent = 'SETTINGS_WORKBENCH_EDITOR_CLEAR_CHANGED'; + +const expectedProperties = [ + 'currentValue', + 'newValue' +]; const explicitErrorHandler = (): void => { window.addEventListener('error', e => { @@ -52,5 +63,19 @@ test('Verify that user can turn on/off Analytics in Settings in the application' await myRedisDatabasePage.reloadPage(); await t.click(settingsPage.accordionPrivacySettings); await t.expect(await settingsPage.getAnalyticsSwitcherValue()).eql(value, 'Analytics was switched properly'); + // Verify that telemetry is turned off + if(value === false){ + await t.click(settingsPage.accordionWorkbenchSettings); + //turn on and turn off option + await t.click(settingsPage.switchEditorCleanupOption); + await t.click(settingsPage.switchEditorCleanupOption); + + try { + await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger); + await t.expect(true).eql(false, 'telemetry is sent when analytics is disabled'); + } catch (error) { + await t.expect(true).eql(true, 'telemetry is not sent when analytics is disabled'); + } + } } }); diff --git a/tests/e2e/tests/web/critical-path/slow-log/slow-log.e2e.ts b/tests/e2e/tests/web/critical-path/slow-log/slow-log.e2e.ts index f04578921f..29b4e750e9 100644 --- a/tests/e2e/tests/web/critical-path/slow-log/slow-log.e2e.ts +++ b/tests/e2e/tests/web/critical-path/slow-log/slow-log.e2e.ts @@ -3,6 +3,7 @@ import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { Telemetry } from '../../../../helpers'; const slowLogPage = new SlowLogPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); @@ -10,6 +11,21 @@ const browserPage = new BrowserPage(); const overviewPage = new ClusterDetailsPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const telemetry = new Telemetry(); + +const logger = telemetry.createLogger(); + +const telemetryEvents = ['SLOWLOG_CLEARED','SLOWLOG_LOADED']; +const clearExpectedProperties = [ + 'databaseId', + 'provider' +]; + +const loadExpectedProperties = [ + 'databaseId', + 'numberOfCommands', + 'provider' +]; const slowerThanParameter = 1; let maxCommandLength = 50; @@ -108,7 +124,8 @@ test('Verify that user can set slowlog-log-slower-than value in milliseconds and await t.expect(parseFloat(microsecondsDuration.replace(' ', '')) / 1000).eql(parseFloat(millisecondsDuration)); await t.expect(parseFloat(microsecondsDuration.replace(' ', ''))).eql(parseFloat(millisecondsDuration) * 1000); }); -test('Verify that user can reset settings to default on Slow Log page', async t => { +test.requestHooks(logger) +('Verify that user can reset settings to default on Slow Log page', async t => { // Set slowlog-max-len=0 command = 'info'; await slowLogPage.changeSlowerThanParameter(slowerThanParameter, slowLogPage.slowLogConfigureMicroSecondsUnit); @@ -117,12 +134,20 @@ test('Verify that user can reset settings to default on Slow Log page', async t await t.expect(slowLogPage.slowLogCommandValue.withExactText(command).exists).ok('Logged command not found'); await t.click(slowLogPage.slowLogClearButton); await t.click(slowLogPage.slowLogConfirmClearButton); + + //Verify telemetry event + await telemetry.verifyEventHasProperties(telemetryEvents[0], clearExpectedProperties, logger); + // Verify that user can clear Slow Log await t.expect(slowLogPage.slowLogEmptyResult.exists).ok('Slow log is not cleared'); // Set slower than parameter and max length await slowLogPage.changeSlowerThanParameter(slowerThanParameter, slowLogPage.slowLogConfigureMicroSecondsUnit); await slowLogPage.changeMaxLengthParameter(maxCommandLength); + + //Verify telemetry event + await telemetry.verifyEventHasProperties(telemetryEvents[1], loadExpectedProperties, logger); + // Reset settings to default await slowLogPage.resetToDefaultConfig(); // Compare configuration after re-setting diff --git a/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts b/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts index db1e2ffa7c..78aee90bd8 100644 --- a/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts +++ b/tests/e2e/tests/web/critical-path/tree-view/delimiter.e2e.ts @@ -1,22 +1,27 @@ import { BrowserPage } from '../../../../pageObjects'; -import { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf'; +import { commonUrl, ossStandaloneBigConfig, ossStandaloneV8Config } from '../../../../helpers/conf'; import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { BrowserActions } from '../../../../common-actions/browser-actions'; +import { APIKeyRequests } from '../../../../helpers/api/api-keys'; +import { HashKeyParameters } from '../../../../pageObjects/browser-page'; const browserPage = new BrowserPage(); const browserActions = new BrowserActions(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const apiKeyRequests = new APIKeyRequests(); + +let keyNames: string[]; fixture `Delimiter tests` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) - .beforeEach(async() => { + .beforeEach(async () => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); }) - .afterEach(async() => { + .afterEach(async () => { await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); }); test('Verify that user can see that input is not saved when the Cancel button is clicked', async t => { @@ -24,18 +29,72 @@ test('Verify that user can see that input is not saved when the Cancel button is await t.click(browserPage.treeViewButton); await t.click(browserPage.TreeView.treeViewSettingsBtn); // Check the default delimiter value - await t.expect(browserPage.TreeView.treeViewDelimiterInput.value).eql(':', 'Default delimiter not applied'); + await t.expect(browserPage.TreeView.FiltersDialog.getDelimiterBadgeByTitle(':').exists).eql(true, 'Default delimiter not applied'); // Apply new value to the field - await t.typeText(browserPage.TreeView.treeViewDelimiterInput, 'test', { replace: true }); + await browserPage.TreeView.FiltersDialog.removeDelimiterItem(':'); + await browserPage.TreeView.FiltersDialog.addDelimiterItem('test'); // Click on Cancel button - await t.click(browserPage.TreeView.treeViewDelimiterValueCancel); + await t.click(browserPage.TreeView.FiltersDialog.treeViewDelimiterValueCancel); // Check the previous delimiter value await t.click(browserPage.TreeView.treeViewSettingsBtn); - await t.expect(browserPage.TreeView.treeViewDelimiterInput.value).eql(':', 'Previous delimiter not applied'); - await t.click(browserPage.TreeView.treeViewDelimiterValueCancel); + await t.expect(browserPage.TreeView.FiltersDialog.getDelimiterBadgeByTitle(':').exists).eql(true, 'Previous delimiter not applied'); + await t.expect(browserPage.TreeView.FiltersDialog.getDelimiterBadgeByTitle('test').exists).eql(false, 'Previous delimiter not applied'); + await t.click(browserPage.TreeView.FiltersDialog.treeViewDelimiterValueCancel); // Change delimiter await browserPage.TreeView.changeDelimiterInTreeView('-'); // Verify that when user changes the delimiter and clicks on Save button delimiter is applied - await browserActions.checkTreeViewFoldersStructure([['device_us', 'west'], ['mobile_eu', 'central'], ['mobile_us', 'east'], ['user_us', 'west'], ['device_eu', 'central'], ['user_eu', 'central']], '-'); + await browserActions.checkTreeViewFoldersStructure([['device_us', 'west'], ['mobile_eu', 'central'], ['mobile_us', 'east'], ['user_us', 'west'], ['device_eu', 'central'], ['user_eu', 'central']], ['-']); }); +test + .before(async () => { + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV8Config); + keyNames = [ + `device:common-dev`, + `device-common:dev`, + `device:common:dev`, + `device-common-dev`, + `device:common-stage`, + `device:common1-stage`, + `mobile:common-dev`, + `mobile:common-stage` + ]; + for (const keyName of keyNames) { + let hashKeyParameters: HashKeyParameters = { + keyName: keyName, + fields: [ + { + field: 'field', + value: 'value', + }, + ], + } + await apiKeyRequests.addHashKeyApi( + hashKeyParameters, + ossStandaloneV8Config, + ) + } + await browserPage.reloadPage(); + }) + .after(async () => { + for (const keyName of keyNames) { + await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneV8Config.databaseName); + } + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV8Config); + })('Verify that user can set multiple delimiters in the tree view', async t => { + // Switch to tree view + await t.click(browserPage.treeViewButton); + // Verify folders ordering with default delimiter + await browserActions.checkTreeViewFoldersStructure([['device', 'common'], ['device-common'], ['mobile']], [':']); + await t.click(browserPage.TreeView.treeViewSettingsBtn); + // Apply new value to the field + await browserPage.TreeView.FiltersDialog.addDelimiterItem('-'); + await t.click(browserPage.TreeView.FiltersDialog.treeViewDelimiterValueSave); + // Verify that when user changes the delimiter and clicks on Save button delimiter is applied + await browserActions.checkTreeViewFoldersStructure([['device', 'common'], ['device', 'common1'], ['mobile', 'common']], [':', '-']); + + // Verify that namespace names tooltip contains valid names and delimiter + await t.click(browserActions.getNodeSelector('device')); + await t.hover(browserActions.getNodeSelector('device-common')); + await browserActions.verifyTooltipContainsText('device-common-*\n:\n-\n5 key(s)', true); + }); diff --git a/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts index 89deed9dc1..b7782b9173 100644 --- a/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts @@ -18,12 +18,28 @@ const telemetry = new Telemetry(); let indexName = chance.word({ length: 5 }); let keyName = chance.word({ length: 5 }); const logger = telemetry.createLogger(); -const telemetryEvent = 'EXPLORE_PANEL_TUTORIAL_OPENED'; +const tutorialTelemetryEvent = 'EXPLORE_PANEL_TUTORIAL_OPENED'; +const workbenchTelemetryEvents = ['WORKBENCH_COMMAND_SUBMITTED','WORKBENCH_MODE_CHANGED'] const telemetryPath = 'static/tutorials/ds/hashes.md'; -const expectedProperties = [ +const tutorialExpectedProperties = [ 'databaseId', 'path' ]; +const workbenchExpectedProperties = [ + 'command', + 'databaseId', + 'multiple', + 'pipeline', + 'provider', + 'rawMode', + 'results' +]; +const rawModeExpectedProperties = [ + 'changedFromMode', + 'changedToMode', + 'databaseId', + 'provider', +]; fixture `Default scripts area at Workbench` .meta({ type: 'critical_path', rte: rte.standalone }) @@ -53,6 +69,7 @@ test `FT.INFO "${indexName}"`; // Send commands await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\n')); + await telemetry.verifyEventHasProperties(workbenchTelemetryEvents[0], workbenchExpectedProperties, logger); // Run automatically added "FT._LIST" and "FT.INFO {index}" scripts await workbenchPage.NavigationHeader.togglePanel(true); const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); @@ -60,13 +77,16 @@ test await t.click(tutorials.internalLinkWorkingWithHashes); // Verify that telemetry event 'WORKBENCH_ENABLEMENT_AREA_GUIDE_OPENED' sent and has all expected properties - await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger); - await telemetry.verifyEventPropertyValue(telemetryEvent, 'path', telemetryPath, logger); + await telemetry.verifyEventHasProperties(tutorialTelemetryEvent, tutorialExpectedProperties, logger); + await telemetry.verifyEventPropertyValue(tutorialTelemetryEvent, 'path', telemetryPath, logger); await workbenchPage.sendCommandInWorkbench(addedScript); // Check the FT._LIST result await t.expect(workbenchPage.queryTextResult.textContent).contains(indexName, 'The result of the FT._LIST command not found'); + // Verify telemetry event + await t.click(workbenchPage.rawModeBtn); + await telemetry.verifyEventHasProperties(workbenchTelemetryEvents[1], rawModeExpectedProperties, logger); // Check the FT.INFO result await t.switchToIframe(workbenchPage.iframe); await t.expect(workbenchPage.queryColumns.textContent).contains('name', 'The result of the FT.INFO command not found'); diff --git a/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts b/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts index 318bbcd677..263e053492 100644 --- a/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/add-keys.e2e.ts @@ -5,12 +5,15 @@ import { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../.. import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; import { BrowserActions } from '../../../../common-actions/browser-actions'; +import path from 'path'; const browserPage = new BrowserPage(); const browserActions = new BrowserActions(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const jsonFilePath = path.join('..', '..', '..', '..', 'test-data', 'big-json', 'json-BigInt.json'); + const jsonKeys = [['JSON-string', '"test"'], ['JSON-number', '782364'], ['JSON-boolean', 'true'], ['JSON-null', 'null'], ['JSON-array', '[1, 2, 3]']]; let keyNames: string[]; let indexName: string; @@ -105,3 +108,32 @@ test // Verify that the new key is not displayed at the top for the Search capability await browserActions.verifyKeyIsNotDisplayedTop(keyName3); }); +test('Verify that user can add json with BigInt', async t => { + const keyName = Common.generateWord(12); + + // Add Json key with json object + await t.click(browserPage.plusAddKeyButton); + await t.click(browserPage.keyTypeDropDown); + await t.click(browserPage.jsonOption); + await t.click(browserPage.addKeyNameInput); + await t.typeText(browserPage.addKeyNameInput, keyName, { replace: true, paste: true }); + await t.setFilesToUpload(browserPage.jsonUploadInput, [jsonFilePath]); + await t.click(browserPage.addKeyButton); + + await t.click(browserPage.editJsonObjectButton); + await t.expect(await browserPage.jsonValueInput.textContent).contains('message', 'edit value is empty'); + await t.click(browserPage.cancelEditButton); + + await t.click(browserPage.expandJsonObject); + await t.click(browserPage.expandJsonObject); + await t.expect(await browserPage.jsonKeyValue.textContent).contains('1.2345678998765432e+24', 'BigInt is not displayed'); + await t.expect(await browserPage.jsonKeyValue.textContent).contains('123456789987654321', 'BigInt is not displayed'); + + await browserPage.addJsonKeyOnTheSameLevel('"key2"', '7777777777888889455'); + await t.expect(await browserPage.jsonKeyValue.textContent).contains('7777777777888889455', 'BigInt is not displayed'); + + await t.click(browserPage.editJsonObjectButton.nth(3)); + await t.typeText(browserPage.jsonValueInput, '121212121111112121212111', { paste: true, replace: true }); + await t.click(browserPage.applyEditButton); + await t.expect(await browserPage.jsonKeyValue.textContent).contains('1.2121212111111212e+23', 'BigInt is not displayed'); +}); diff --git a/tests/e2e/tests/web/regression/browser/formatters.e2e.ts b/tests/e2e/tests/web/regression/browser/formatters.e2e.ts new file mode 100644 index 0000000000..15a99cf463 --- /dev/null +++ b/tests/e2e/tests/web/regression/browser/formatters.e2e.ts @@ -0,0 +1,61 @@ +import { rte } from '../../../../helpers/constants'; +import { populateHashWithFields } from '../../../../helpers/keys'; +import { Common, DatabaseHelper } from '../../../../helpers'; +import { BrowserPage } from '../../../../pageObjects'; +import { + commonUrl, + ossStandaloneV8Config +} from '../../../../helpers/conf'; +import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { APIKeyRequests } from '../../../../helpers/api/api-keys'; +const apiKeyRequests = new APIKeyRequests(); + + +const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); + +const keyName = `TestHashKey-${ Common.generateWord(10) }`; +const keyToAddParameters = { fieldsCount: 1, keyName, fieldStartWith: 'hashField', fieldValueStartWith: 'hashValue' }; + +fixture `Formatters` + .meta({ + type: 'regression', + rte: rte.standalone + }) + .page(commonUrl) + .beforeEach(async() => { + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV8Config); + + }) + .afterEach(async() => { + // Clear keys and database + await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneV8Config.databaseName); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneV8Config); + }); + +test('Verify that UTF8 in PHP serialized', async t => { + const phpValueChinese = '测试'; + const phpValueCRussian = 'Привет мир!'; + const setValue =`SET ${keyName} "a:3:{s:4:\\"name\\";s:6:\\"${phpValueChinese}\\";s:3:\\"age\\";i:30;s:7:\\"message\\";s:20:\\"${phpValueCRussian}\\";}"\n`; + + await browserPage.Cli.sendCommandInCli(setValue); + await t.click(browserPage.refreshKeysButton); + + await browserPage.openKeyDetailsByKeyName(keyName); + await browserPage.selectFormatter('PHP serialized'); + await t.expect(await browserPage.getStringKeyValue()).contains(phpValueChinese, 'data is not serialized in php'); + await t.expect(await browserPage.getStringKeyValue()).contains(phpValueCRussian, 'data is not serialized in php'); +}); + +test('Verify that dataTime is displayed in Java serialized', async t => { + const hexValue ='ACED00057372000E6A6176612E7574696C2E44617465686A81014B59741903000078707708000000BEACD0567278'; + const javaTimeValue = '"1995-12-14T12:12:01.010Z"' + + await browserPage.addHashKey(keyName); + // Add valid value in HEX format for convertion + await browserPage.selectFormatter('HEX'); + await browserPage.editHashKeyValue(hexValue); + await browserPage.selectFormatter('Java serialized'); + await t.expect(browserPage.hashFieldValue.innerText).eql(javaTimeValue, 'data is not serialized in java'); +}); diff --git a/tests/e2e/tests/web/regression/database/redisstack.e2e.ts b/tests/e2e/tests/web/regression/database/redisstack.e2e.ts index 9aa87cccdd..046b3f3e3d 100644 --- a/tests/e2e/tests/web/regression/database/redisstack.e2e.ts +++ b/tests/e2e/tests/web/regression/database/redisstack.e2e.ts @@ -13,7 +13,7 @@ const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); -const moduleNameList = ['RediSearch', 'RedisGraph', 'RedisBloom', 'RedisJSON', 'RedisTimeSeries']; +const moduleNameList = ['Redis Query Engine', 'Graph', 'Probabilistic', 'JSON', 'Time Series']; fixture `Redis Stack` .meta({ type: 'regression', rte: rte.standalone }) diff --git a/tests/e2e/tests/web/regression/insights/import-tutorials.e2e.ts b/tests/e2e/tests/web/regression/insights/import-tutorials.e2e.ts index 4a047986e9..86a138171c 100644 --- a/tests/e2e/tests/web/regression/insights/import-tutorials.e2e.ts +++ b/tests/e2e/tests/web/regression/insights/import-tutorials.e2e.ts @@ -73,6 +73,11 @@ test const tutorials = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); await t.expect(tutorials.customTutorials.visible).ok('custom tutorials sections is not visible'); + // Verify that user can see "My Tutorials" tab is collapsed by default in tutorials + await t.expect(tutorials.customTutorials.getAttribute('aria-expanded')).eql('false', 'My tutorials not closed by default'); + + // Expand My tutorials + await tutorials.toggleMyTutorialPanel(); await t.click(tutorials.tutorialOpenUploadButton); await t.expect(tutorials.tutorialSubmitButton.hasAttribute('disabled')).ok('submit button is not disabled'); @@ -138,6 +143,7 @@ test await workbenchPage.NavigationHeader.togglePanel(true); const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); + await tutorials.toggleMyTutorialPanel(); await t.click(tutorials.tutorialOpenUploadButton); // Verify that user can upload tutorials using a URL await t.typeText(tutorials.tutorialLinkField, link); @@ -199,6 +205,7 @@ test // Upload custom tutorial await workbenchPage.NavigationHeader.togglePanel(true); const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); + await tutorials.toggleMyTutorialPanel(); await t .click(tutorials.tutorialOpenUploadButton) .setFilesToUpload(tutorials.tutorialImport, [zipFilePath]) @@ -269,6 +276,7 @@ test // Upload custom tutorial await workbenchPage.NavigationHeader.togglePanel(true); const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); + await tutorials.toggleMyTutorialPanel(); await t .click(tutorials.tutorialOpenUploadButton) .setFilesToUpload(tutorials.tutorialImport, [zipFilePath]) diff --git a/tests/e2e/tests/web/regression/insights/open-insights-panel.e2e.ts b/tests/e2e/tests/web/regression/insights/open-insights-panel.e2e.ts index d1346c1f85..f8376575a7 100644 --- a/tests/e2e/tests/web/regression/insights/open-insights-panel.e2e.ts +++ b/tests/e2e/tests/web/regression/insights/open-insights-panel.e2e.ts @@ -41,7 +41,7 @@ test await t.click(browserPage.NavigationPanel.myRedisDBButton); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await t.expect(browserPage.InsightsPanel.sidePanel.exists).ok('Insights panel is not opened'); - await t.expect(await browserPage.InsightsPanel.existsCompatibilityPopover.textContent).contains('Search and query capability', 'popover is not displayed'); + await t.expect(await browserPage.InsightsPanel.existsCompatibilityPopover.textContent).contains('Redis Query Engine', 'popover is not displayed'); const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); await t.expect(tab.preselectArea.textContent).contains('How To Query Your Data', 'the tutorial is incorrect'); @@ -53,7 +53,7 @@ test await t.click(browserPage.NavigationPanel.myRedisDBButton); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await t.expect(browserPage.InsightsPanel.sidePanel.exists).ok('Insights panel is not opened'); - await t.expect(await browserPage.InsightsPanel.existsCompatibilityPopover.textContent).contains('Time series data', 'popover is not displayed'); + await t.expect(await browserPage.InsightsPanel.existsCompatibilityPopover.textContent).contains('Time series data structure', 'popover is not displayed'); await t.expect(tab.preselectArea.textContent).contains('Time Series', 'the tutorial is incorrect'); }); diff --git a/tests/e2e/tests/web/regression/settings/settings.e2e.ts b/tests/e2e/tests/web/regression/settings/settings.e2e.ts index e283473795..f8ed6fa63f 100644 --- a/tests/e2e/tests/web/regression/settings/settings.e2e.ts +++ b/tests/e2e/tests/web/regression/settings/settings.e2e.ts @@ -4,7 +4,7 @@ import { commonUrl, ossClusterConfig, } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { Common, DatabaseHelper } from '../../../../helpers'; +import { Common, DatabaseHelper, Telemetry } from '../../../../helpers'; const browserPage = new BrowserPage(); const databaseAPIRequests = new DatabaseAPIRequests(); @@ -12,9 +12,21 @@ const workbenchPage = new WorkbenchPage(); const settingsPage = new SettingsPage(); const memoryEfficiencyPage = new MemoryEfficiencyPage(); const databaseHelper = new DatabaseHelper(); +const telemetry = new Telemetry(); + +const logger = telemetry.createLogger(); let keyName = Common.generateWord(20); +const telemetryEvents = ['SETTINGS_DATE_TIME_FORMAT_CHANGED','DATABASE_ANALYSIS_STARTED']; +const settingsExpectedProperties = [ + 'currentFormat' +]; +const databaseAnalysisExpectedProperties = [ + 'databaseId', + 'provider' +]; + fixture `DataTime format setting` .meta({ type: 'regression', @@ -33,7 +45,9 @@ fixture `DataTime format setting` await databaseAPIRequests.deleteAllDatabasesApi(); await settingsPage.selectTimeZoneDropdown('local'); }); -test('Verify that user can select date time format', async t => { +test + .requestHooks(logger) +('Verify that user can select date time format', async t => { const defaultDateRegExp = /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d \d{1,2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4}$/; const selectedDateReqExp = /^(0[1-9]|[12]\d|3[01])\.(0[1-9]|1[0-2])\.\d{4} ([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/; keyName = `DateTimeTestKey-${Common.generateWord(5)}`; @@ -51,6 +65,9 @@ test('Verify that user can select date time format', async t => { await t.click(workbenchPage.NavigationPanel.settingsButton); await t.click(settingsPage.accordionAppearance); await settingsPage.selectDataFormatDropdown(selectorForOption); + //Verify telemetry event + await telemetry.verifyEventHasProperties(telemetryEvents[0], settingsExpectedProperties, logger); + await t.expect(settingsPage.selectFormatDropdown.textContent).eql(selectedOption, 'option is not selected'); await t.expect(selectedDateReqExp.test(await settingsPage.dataPreview.textContent)).ok(`preview is not valid for ${selectedOption}`); @@ -70,7 +87,8 @@ test('Verify that user can select date time format', async t => { await t.expect(selectedDateReqExp.test(dateTime)).ok('date is not in default format HH:mm:ss.SSS d MMM yyyy'); }); -test('Verify that user can set custom date time format', async t => { +test .requestHooks(logger) +('Verify that user can set custom date time format', async t => { const enteredFormat = 'MMM dd yyyy/ HH.mm.ss'; const enteredDateReqExp = /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (0[1-9]|[12]\d|3[01]) \d{4}\/ ([01]\d|2[0-3])\.[0-5]\d\.[0-5]\d$/; @@ -84,5 +102,9 @@ test('Verify that user can set custom date time format', async t => { await t.click(settingsPage.NavigationPanel.analysisPageButton); await t.click(memoryEfficiencyPage.databaseAnalysisTab); await t.click(memoryEfficiencyPage.newReportBtn); + + //Verify telemetry event + await telemetry.verifyEventHasProperties(telemetryEvents[1], databaseAnalysisExpectedProperties, logger); + await t.expect(enteredDateReqExp.test((await memoryEfficiencyPage.selectedReport.textContent).trim())).ok(`custom format is not working ${enteredFormat}`); }); diff --git a/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts b/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts index 08be4a8c69..56c8249460 100644 --- a/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts +++ b/tests/e2e/tests/web/regression/tree-view/tree-view.e2e.ts @@ -133,7 +133,7 @@ test('Verify that when user deletes the key he can see the key is removed from t await t.click(browserPage.treeViewButton); // Verify the default separator await t.click(browserPage.TreeView.treeViewSettingsBtn); - await t.expect(browserPage.TreeView.treeViewDelimiterInput.value).eql(':', 'The “:” (colon) not used as a default separator for namespaces'); + await t.expect(browserPage.TreeView.FiltersDialog.getDelimiterBadgeByTitle(':').exists).eql(true, 'The “:” (colon) not used as a default separator for namespaces'); // Verify that user can see that “:” (colon) used as a default separator for namespaces and see the number of keys found per each namespace await t.expect(browserPage.TreeView.treeViewKeysNumber.visible).ok('The user can not see the number of keys'); diff --git a/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts b/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts index e8f9709587..9b03a19c40 100644 --- a/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts @@ -3,6 +3,7 @@ import { WorkbenchPage, MyRedisDatabasePage, SettingsPage, BrowserPage } from '. import { rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { Telemetry } from '../../../../helpers'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); @@ -10,6 +11,9 @@ const settingsPage = new SettingsPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); const browserPage = new BrowserPage(); +const telemetry = new Telemetry(); + +const logger = telemetry.createLogger(); const commandToSend = 'info server'; const databasesForAdding = [ @@ -17,6 +21,12 @@ const databasesForAdding = [ { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testDB2' } ]; +const telemetryEvent = 'SETTINGS_WORKBENCH_EDITOR_CLEAR_CHANGED'; +const expectedProperties = [ + 'currentValue', + 'newValue' +]; + fixture `Workbench Editor Cleanup` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) @@ -62,6 +72,7 @@ test await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName); }) + .requestHooks(logger) .after(async() => { // Clear and delete database await databaseAPIRequests.deleteStandaloneDatabasesApi(databasesForAdding); @@ -71,6 +82,8 @@ test await t.click(settingsPage.accordionWorkbenchSettings); // Disable Editor Cleanup await settingsPage.changeEditorCleanupSwitcher(false); + //Verify telemetry event + await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger); await myRedisDatabasePage.reloadPage(); await t.click(settingsPage.accordionWorkbenchSettings); // Verify that Editor Cleanup setting is saved when refreshing the page diff --git a/tests/e2e/tests/web/regression/workbench/redisearch-module-not-available.e2e.ts b/tests/e2e/tests/web/regression/workbench/redisearch-module-not-available.e2e.ts index 6aff416aa2..30bdeb3cc7 100644 --- a/tests/e2e/tests/web/regression/workbench/redisearch-module-not-available.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/redisearch-module-not-available.e2e.ts @@ -45,10 +45,10 @@ test('Verify that user can see options on what can be done to work with capabili await workbenchPage.NavigationHeader.togglePanel(true); await workbenchPage.sendCommandInWorkbench(commandJSON); // Verify change screens when capability not available - 'JSON' - await t.expect(await workbenchPage.commandExecutionResult.withText('RedisJSON is not available').visible) - .ok('Missing RedisJSON title is not visible'); + await t.expect(await workbenchPage.commandExecutionResult.withText('JSON data structure is not available').visible) + .ok('Missing JSON title is not visible'); await workbenchPage.sendCommandInWorkbench(commandFT); // Verify change screens when capability not available - 'Search' - await t.expect(await workbenchPage.commandExecutionResult.withText('RediSearch is not available').visible) - .ok('Missing RedisSearch title is not visible'); + await t.expect(await workbenchPage.commandExecutionResult.withText('Redis Query Engine is not available').visible) + .ok('Missing Search title is not visible'); }); diff --git a/tests/e2e/tests/web/smoke/database/add-standalone-db.e2e.ts b/tests/e2e/tests/web/smoke/database/add-standalone-db.e2e.ts index a4b8bd7f53..4f98cccda7 100644 --- a/tests/e2e/tests/web/smoke/database/add-standalone-db.e2e.ts +++ b/tests/e2e/tests/web/smoke/database/add-standalone-db.e2e.ts @@ -17,7 +17,7 @@ const chance = new Chance(); const databaseHelper = new DatabaseHelper(); const logger = telemetry.createLogger(); -const telemetryEvent = 'CONFIG_DATABASES_OPEN_DATABASE'; +const telemetryEvents = ['CONFIG_DATABASES_OPEN_DATABASE','CONFIG_DATABASES_CLICKED']; const expectedProperties = [ 'databaseId', 'RediSearch', @@ -29,6 +29,9 @@ const expectedProperties = [ 'RedisTimeSeries', 'customModules' ]; +const clickButtonExpectedProperties = [ + 'source' +]; let databaseName = `test_standalone-${chance.string({ length: 10 })}`; fixture `Add database` @@ -49,8 +52,10 @@ test // Fill the add database form await myRedisDatabasePage.AddRedisDatabase.addDatabaseButton.with({ visibilityCheck: true, timeout: 10000 })(); await t - .click(myRedisDatabasePage.AddRedisDatabase.addDatabaseButton) - .click(myRedisDatabasePage.AddRedisDatabase.addDatabaseManually); + .click(myRedisDatabasePage.AddRedisDatabase.addDatabaseButton); + // Verify that telemetry event 'CONFIG_DATABASES_CLICKED' sent and has all expected properties + await telemetry.verifyEventHasProperties(telemetryEvents[1], clickButtonExpectedProperties, logger); + await t.click(myRedisDatabasePage.AddRedisDatabase.addDatabaseManually); await t .typeText(myRedisDatabasePage.AddRedisDatabase.hostInput, ossStandaloneConfig.host, { replace: true, paste: true }) .typeText(myRedisDatabasePage.AddRedisDatabase.portInput, ossStandaloneConfig.port, { replace: true, paste: true }) @@ -69,7 +74,7 @@ test await myRedisDatabasePage.clickOnDBByName(databaseName); // Verify that telemetry event 'CONFIG_DATABASES_OPEN_DATABASE' sent and has all expected properties - await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger); + await telemetry.verifyEventHasProperties(telemetryEvents[0], expectedProperties, logger); await t.click(browserPage.OverviewPanel.myRedisDBLink); // Verify that user can't see an indicator of databases that were opened diff --git a/tests/e2e/tsconfig.json b/tests/e2e/tsconfig.json index a0eff03eb1..5f3f88779d 100644 --- a/tests/e2e/tsconfig.json +++ b/tests/e2e/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2020", "module": "CommonJS", "esModuleInterop": true, + "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, diff --git a/tests/e2e/tsconfig.testcafe.json b/tests/e2e/tsconfig.testcafe.json index 54c2cd3cd7..76bbc2ca54 100644 --- a/tests/e2e/tsconfig.testcafe.json +++ b/tests/e2e/tsconfig.testcafe.json @@ -2,5 +2,5 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": [] - } + } } diff --git a/tests/e2e/web.runner.ci.ts b/tests/e2e/web.runner.ci.ts index cb090db3ae..e34b994a02 100644 --- a/tests/e2e/web.runner.ci.ts +++ b/tests/e2e/web.runner.ci.ts @@ -11,7 +11,7 @@ import testcafe from 'testcafe'; experimentalDecorators: true } }) .src((process.env.TEST_FILES || 'tests/web/**/*.e2e.ts').split('\n')) - .browsers(['chromium:headless --cache --allow-insecure-localhost --disable-search-engine-choice-screen --ignore-certificate-errors']) + .browsers(['firefox:headless --disable-search-engine-choice-screen --ignore-certificate-errors']) .screenshots({ path: 'report/screenshots/', takeOnFails: true, @@ -29,12 +29,12 @@ import testcafe from 'testcafe'; }, { name: 'html', - output: './report/report.html' + output: './report/index.html' } ]) .run({ skipJsErrors: true, - browserInitTimeout: 60000, + browserInitTimeout: 240000, selectorTimeout: 5000, assertionTimeout: 5000, speed: 1, diff --git a/tests/e2e/web.runner.ts b/tests/e2e/web.runner.ts index a30406f2ce..689c3a6638 100644 --- a/tests/e2e/web.runner.ts +++ b/tests/e2e/web.runner.ts @@ -29,7 +29,7 @@ import testcafe from 'testcafe'; }, { name: 'html', - output: './report/report.html' + output: './report/index.html' } ]) .run({ diff --git a/tests/e2e/yarn.lock b/tests/e2e/yarn.lock index 9f590a1588..2b06564115 100644 --- a/tests/e2e/yarn.lock +++ b/tests/e2e/yarn.lock @@ -15,6 +15,15 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" +"@babel/code-frame@^7.0.0": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/code-frame@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.25.7.tgz#438f2c524071531d643c6f0188e1e28f130cebc7" @@ -23,11 +32,25 @@ "@babel/highlight" "^7.25.7" picocolors "^1.0.0" -"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.7", "@babel/compat-data@^7.25.8": +"@babel/code-frame@^7.25.9": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" + integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== + dependencies: + "@babel/helper-validator-identifier" "^7.25.9" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.7", "@babel/compat-data@^7.25.8": version "7.25.8" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.8.tgz#0376e83df5ab0eb0da18885c0140041f0747a402" integrity sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA== +"@babel/compat-data@^7.25.9": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.2.tgz#278b6b13664557de95b8f35b90d96785850bb56e" + integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg== + "@babel/core@^7.23.2": version "7.25.8" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.8.tgz#a57137d2a51bbcffcfaeba43cb4dd33ae3e0e1c6" @@ -59,6 +82,17 @@ "@jridgewell/trace-mapping" "^0.3.25" jsesc "^3.0.2" +"@babel/generator@^7.25.9": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.2.tgz#87b75813bec87916210e5e01939a4c823d6bb74f" + integrity sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw== + dependencies: + "@babel/parser" "^7.26.2" + "@babel/types" "^7.26.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz#63f02dbfa1f7cb75a9bdb832f300582f30bb8972" @@ -66,6 +100,13 @@ dependencies: "@babel/types" "^7.25.7" +"@babel/helper-annotate-as-pure@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz#d8eac4d2dc0d7b6e11fa6e535332e0d3184f06b4" + integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g== + dependencies: + "@babel/types" "^7.25.9" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.7.tgz#d721650c1f595371e0a23ee816f1c3c488c0d622" @@ -74,7 +115,7 @@ "@babel/traverse" "^7.25.7" "@babel/types" "^7.25.7" -"@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.7": +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz#11260ac3322dda0ef53edfae6e97b961449f5fa4" integrity sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A== @@ -85,7 +126,18 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.25.7": +"@babel/helper-compilation-targets@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875" + integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ== + dependencies: + "@babel/compat-data" "^7.25.9" + "@babel/helper-validator-option" "^7.25.9" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz#5d65074c76cae75607421c00d6bd517fe1892d6b" integrity sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw== @@ -98,6 +150,19 @@ "@babel/traverse" "^7.25.7" semver "^6.3.1" +"@babel/helper-create-class-features-plugin@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz#7644147706bb90ff613297d49ed5266bde729f83" + integrity sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/helper-replace-supers" "^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/traverse" "^7.25.9" + semver "^6.3.1" + "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz#dcb464f0e2cdfe0c25cc2a0a59c37ab940ce894e" @@ -140,13 +205,6 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-environment-visitor@^7.18.9": - version "7.24.7" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" - integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ== - dependencies: - "@babel/types" "^7.24.7" - "@babel/helper-member-expression-to-functions@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz#541a33b071f0355a63a0fa4bdf9ac360116b8574" @@ -155,6 +213,14 @@ "@babel/traverse" "^7.25.7" "@babel/types" "^7.25.7" +"@babel/helper-member-expression-to-functions@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz#9dfffe46f727005a5ea29051ac835fb735e4c1a3" + integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + "@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz#dba00d9523539152906ba49263e36d7261040472" @@ -180,12 +246,24 @@ dependencies: "@babel/types" "^7.25.7" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.7", "@babel/helper-plugin-utils@^7.8.0": +"@babel/helper-optimise-call-expression@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz#3324ae50bae7e2ab3c33f60c9a877b6a0146b54e" + integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ== + dependencies: + "@babel/types" "^7.25.9" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.7", "@babel/helper-plugin-utils@^7.8.0": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz#8ec5b21812d992e1ef88a9b068260537b6f0e36c" integrity sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw== -"@babel/helper-remap-async-to-generator@^7.18.9", "@babel/helper-remap-async-to-generator@^7.25.7": +"@babel/helper-plugin-utils@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz#9cbdd63a9443a2c92a725cca7ebca12cc8dd9f46" + integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== + +"@babel/helper-remap-async-to-generator@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.7.tgz#9efdc39df5f489bcd15533c912b6c723a0a65021" integrity sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw== @@ -194,6 +272,15 @@ "@babel/helper-wrap-function" "^7.25.7" "@babel/traverse" "^7.25.7" +"@babel/helper-remap-async-to-generator@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz#e53956ab3d5b9fb88be04b3e2f31b523afd34b92" + integrity sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.25.9" + "@babel/helper-wrap-function" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/helper-replace-supers@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz#38cfda3b6e990879c71d08d0fef9236b62bd75f5" @@ -203,6 +290,15 @@ "@babel/helper-optimise-call-expression" "^7.25.7" "@babel/traverse" "^7.25.7" +"@babel/helper-replace-supers@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz#ba447224798c3da3f8713fc272b145e33da6a5c5" + integrity sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.25.9" + "@babel/helper-optimise-call-expression" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/helper-simple-access@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz#5eb9f6a60c5d6b2e0f76057004f8dacbddfae1c0" @@ -219,21 +315,44 @@ "@babel/traverse" "^7.25.7" "@babel/types" "^7.25.7" +"@babel/helper-skip-transparent-expression-wrappers@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz#0b2e1b62d560d6b1954893fd2b705dc17c91f0c9" + integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA== + dependencies: + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + "@babel/helper-string-parser@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz#d50e8d37b1176207b4fe9acedec386c565a44a54" integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g== +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + "@babel/helper-validator-identifier@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5" integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg== +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + "@babel/helper-validator-option@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz#97d1d684448228b30b506d90cace495d6f492729" integrity sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ== +"@babel/helper-validator-option@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" + integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== + "@babel/helper-wrap-function@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.7.tgz#9f6021dd1c4fdf4ad515c809967fc4bac9a70fe7" @@ -243,6 +362,15 @@ "@babel/traverse" "^7.25.7" "@babel/types" "^7.25.7" +"@babel/helper-wrap-function@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz#d99dfd595312e6c894bd7d237470025c85eea9d0" + integrity sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g== + dependencies: + "@babel/template" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/types" "^7.25.9" + "@babel/helpers@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.7.tgz#091b52cb697a171fe0136ab62e54e407211f09c2" @@ -268,6 +396,13 @@ dependencies: "@babel/types" "^7.25.8" +"@babel/parser@^7.25.9", "@babel/parser@^7.26.2": + version "7.26.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.2.tgz#fd7b6f487cfea09889557ef5d4eeb9ff9a5abd11" + integrity sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ== + dependencies: + "@babel/types" "^7.26.0" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.7.tgz#93969ac50ef4d68b2504b01b758af714e4cbdd64" @@ -307,24 +442,6 @@ "@babel/helper-plugin-utils" "^7.25.7" "@babel/traverse" "^7.25.7" -"@babel/plugin-proposal-async-generator-functions@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz#bfb7276d2d573cb67ba379984a2334e262ba5326" - integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-remap-async-to-generator" "^7.18.9" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-proposal-class-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" - integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-proposal-decorators@^7.23.2": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.7.tgz#dabfd82df5dff3a8fc61a434233bf8227c88402c" @@ -334,37 +451,11 @@ "@babel/helper-plugin-utils" "^7.25.7" "@babel/plugin-syntax-decorators" "^7.25.7" -"@babel/plugin-proposal-object-rest-spread@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" - integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== - dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.20.7" - -"@babel/plugin-proposal-private-methods@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" - integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - "@babel/plugin-syntax-decorators@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.7.tgz#cf26fdde4e750688e133c0e33ead2506377e88f7" @@ -414,13 +505,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.25.7" -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" @@ -436,6 +520,15 @@ dependencies: "@babel/helper-plugin-utils" "^7.25.7" +"@babel/plugin-transform-async-generator-functions@^7.25.4": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz#1b18530b077d18a407c494eb3d1d72da505283a2" + integrity sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-remap-async-to-generator" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@babel/plugin-transform-async-generator-functions@^7.25.8": version "7.25.8" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.8.tgz#3331de02f52cc1f2c75b396bec52188c85b0b1ec" @@ -468,6 +561,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.25.7" +"@babel/plugin-transform-class-properties@^7.25.4": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz#a8ce84fedb9ad512549984101fa84080a9f5f51f" + integrity sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-class-properties@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz#a389cfca7a10ac80e3ff4c75fca08bd097ad1523" @@ -476,6 +577,14 @@ "@babel/helper-create-class-features-plugin" "^7.25.7" "@babel/helper-plugin-utils" "^7.25.7" +"@babel/plugin-transform-class-static-block@^7.24.7": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz#6c8da219f4eb15cae9834ec4348ff8e9e09664a0" + integrity sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-class-static-block@^7.25.8": version "7.25.8" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz#a8af22028920fe404668031eceb4c3aadccb5262" @@ -673,6 +782,15 @@ dependencies: "@babel/helper-plugin-utils" "^7.25.7" +"@babel/plugin-transform-object-rest-spread@^7.24.7": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz#0203725025074164808bcf1a2cfa90c652c99f18" + integrity sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg== + dependencies: + "@babel/helper-compilation-targets" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-parameters" "^7.25.9" + "@babel/plugin-transform-object-rest-spread@^7.25.8": version "7.25.8" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz#0904ac16bcce41df4db12d915d6780f85c7fb04b" @@ -705,13 +823,28 @@ "@babel/helper-plugin-utils" "^7.25.7" "@babel/helper-skip-transparent-expression-wrappers" "^7.25.7" -"@babel/plugin-transform-parameters@^7.20.7", "@babel/plugin-transform-parameters@^7.25.7": +"@babel/plugin-transform-parameters@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.7.tgz#80c38b03ef580f6d6bffe1c5254bb35986859ac7" integrity sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ== dependencies: "@babel/helper-plugin-utils" "^7.25.7" +"@babel/plugin-transform-parameters@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz#b856842205b3e77e18b7a7a1b94958069c7ba257" + integrity sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g== + dependencies: + "@babel/helper-plugin-utils" "^7.25.9" + +"@babel/plugin-transform-private-methods@^7.25.4": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz#847f4139263577526455d7d3223cd8bda51e3b57" + integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.25.9" + "@babel/helper-plugin-utils" "^7.25.9" + "@babel/plugin-transform-private-methods@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz#c790a04f837b4bd61d6b0317b43aa11ff67dce80" @@ -983,6 +1116,15 @@ "@babel/parser" "^7.25.7" "@babel/types" "^7.25.7" +"@babel/template@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" + integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/types" "^7.25.9" + "@babel/traverse@^7.25.7": version "7.25.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.7.tgz#83e367619be1cab8e4f2892ef30ba04c26a40fa8" @@ -996,7 +1138,20 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.24.7", "@babel/types@^7.25.7", "@babel/types@^7.25.8", "@babel/types@^7.4.4": +"@babel/traverse@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.9.tgz#a50f8fe49e7f69f53de5bea7e413cd35c5e13c84" + integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== + dependencies: + "@babel/code-frame" "^7.25.9" + "@babel/generator" "^7.25.9" + "@babel/parser" "^7.25.9" + "@babel/template" "^7.25.9" + "@babel/types" "^7.25.9" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.25.7", "@babel/types@^7.25.8", "@babel/types@^7.4.4": version "7.25.8" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.8.tgz#5cf6037258e8a9bcad533f4979025140cb9993e1" integrity sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg== @@ -1005,6 +1160,14 @@ "@babel/helper-validator-identifier" "^7.25.7" to-fast-properties "^2.0.0" +"@babel/types@^7.25.9", "@babel/types@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.0.tgz#deabd08d6b753bc8e0f198f8709fb575e31774ff" + integrity sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + "@bazel/runfiles@^5.8.1": version "5.8.1" resolved "https://registry.yarnpkg.com/@bazel/runfiles/-/runfiles-5.8.1.tgz#737d5b3dc9739767054820265cfe432a80564c82" @@ -1313,6 +1476,34 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@puppeteer/browsers@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.4.0.tgz#a0dd0f4e381e53f509109ae83b891db5972750f5" + integrity sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g== + dependencies: + debug "^4.3.6" + extract-zip "^2.0.1" + progress "^2.0.3" + proxy-agent "^6.4.0" + semver "^7.6.3" + tar-fs "^3.0.6" + unbzip2-stream "^1.4.3" + yargs "^17.7.2" + +"@puppeteer/browsers@2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.4.1.tgz#7afd271199cc920ece2ff25109278be0a3e8a225" + integrity sha512-0kdAbmic3J09I6dT8e9vE2JOCSt13wHCW5x/ly8TSt2bDtuIWe2TgLZZDHdcziw9AVCzflMAXCrVyRIhIs44Ng== + dependencies: + debug "^4.3.7" + extract-zip "^2.0.1" + progress "^2.0.3" + proxy-agent "^6.4.0" + semver "^7.6.3" + tar-fs "^3.0.6" + unbzip2-stream "^1.4.3" + yargs "^17.7.2" + "@redis/bloom@1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71" @@ -1394,16 +1585,35 @@ dependencies: "@types/readdir-glob" "*" +"@types/bezier-js@4": + version "4.1.3" + resolved "https://registry.yarnpkg.com/@types/bezier-js/-/bezier-js-4.1.3.tgz#237d4fe7e9aae7edd0c27a71f9f236f4ddc1c562" + integrity sha512-FNVVCu5mx/rJCWBxLTcL7oOajmGtWtBTDjq6DSUWUI12GeePivrZZXz+UgE0D6VYsLEjvExRO03z4hVtu3pTEQ== + "@types/chance@1.1.6": version "1.1.6" resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.1.6.tgz#2fe3de58742629602c3fbab468093b27207f04ad" integrity sha512-V+pm3stv1Mvz8fSKJJod6CglNGVqEQ6OyuqitoDkWywEODM/eJd1eSuIp9xt6DrX8BWZ2eDSIzbw1tPCUTvGbQ== +"@types/chrome-remote-interface@^0.31.14": + version "0.31.14" + resolved "https://registry.yarnpkg.com/@types/chrome-remote-interface/-/chrome-remote-interface-0.31.14.tgz#173842a9a8e9995d5111ce209aa883fc90cedd96" + integrity sha512-H9hTcLu1y+Ms6GDPXXeGhgxaOSD69yEo674vjJw5EeW1tTwYo8fEkf7A9nWlnO6ArJsS7c41iZeX6mRDQ1LhEw== + dependencies: + devtools-protocol "0.0.927104" + "@types/cookiejar@^2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" integrity sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q== +"@types/debug@^4.1.0": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + "@types/edit-json-file@1.7.3": version "1.7.3" resolved "https://registry.yarnpkg.com/@types/edit-json-file/-/edit-json-file-1.7.3.tgz#2b0c9fe362f52fee4c909b0fe0c45950a1f3da9f" @@ -1470,6 +1680,11 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== +"@types/ms@*": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + "@types/node@*", "@types/node@20.3.1", "@types/node@>=13.7.0", "@types/node@^17.0.40", "@types/node@^20.14.5": version "20.3.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.1.tgz#e8a83f1aa8b649377bb1fb5d7bac5cb90e784dfe" @@ -1482,14 +1697,6 @@ dependencies: "@types/node" "*" -"@types/selenium-webdriver@^4.1.26": - version "4.1.26" - resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.1.26.tgz#09c696a341cf8cfc1641cded11d14813350b6ca9" - integrity sha512-PUgqsyNffal0eAU0bzGlh37MJo558aporAPZoKqBeB/pF7zhKl1S3zqza0GpwFqgoigNxWhEIJzru75eeYco/w== - dependencies: - "@types/node" "*" - "@types/ws" "*" - "@types/set-value@*": version "4.0.1" resolved "https://registry.npmjs.org/@types/set-value/-/set-value-4.0.1.tgz" @@ -1520,13 +1727,6 @@ "@types/methods" "^1.1.4" "@types/superagent" "^8.1.0" -"@types/ws@*": - version "8.5.10" - resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz" - integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== - dependencies: - "@types/node" "*" - "@types/yauzl@^2.9.1": version "2.10.3" resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz" @@ -1989,11 +2189,39 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -bare-events@^2.2.0: +bare-events@^2.0.0, bare-events@^2.2.0: version "2.5.0" resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.5.0.tgz#305b511e262ffd8b9d5616b056464f8e1b3329cc" integrity sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A== +bare-fs@^2.1.1: + version "2.3.5" + resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-2.3.5.tgz#05daa8e8206aeb46d13c2fe25a2cd3797b0d284a" + integrity sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw== + dependencies: + bare-events "^2.0.0" + bare-path "^2.0.0" + bare-stream "^2.0.0" + +bare-os@^2.1.0: + version "2.4.4" + resolved "https://registry.yarnpkg.com/bare-os/-/bare-os-2.4.4.tgz#01243392eb0a6e947177bb7c8a45123d45c9b1a9" + integrity sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ== + +bare-path@^2.0.0, bare-path@^2.1.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-2.1.3.tgz#594104c829ef660e43b5589ec8daef7df6cedb3e" + integrity sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA== + dependencies: + bare-os "^2.1.0" + +bare-stream@^2.0.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.3.2.tgz#3bc62b429bcf850d2f265719b7a49ee0630a3ae4" + integrity sha512-EFZHSIBkDgSHIwj2l2QZfP4U5OcD4xFAOwhSb/vlr9PIqyGJGvB/nfClJbcnh3EY4jtPE4zsb5ztae96bVF79A== + dependencies: + streamx "^2.20.0" + base-unicode@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/base-unicode/-/base-unicode-1.0.0.tgz#44b61fd4460b18f6d47ae6f5ea95a45b16a4885a" @@ -2011,6 +2239,11 @@ basic-ftp@^5.0.2: resolved "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz" integrity sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg== +bezier-js@^6.1.3: + version "6.1.4" + resolved "https://registry.yarnpkg.com/bezier-js/-/bezier-js-6.1.4.tgz#c7828f6c8900562b69d5040afb881bcbdad82001" + integrity sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz" @@ -2105,7 +2338,7 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@^5.5.0: +buffer@^5.2.1, buffer@^5.5.0: version "5.7.1" resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -2237,6 +2470,16 @@ chownr@^2.0.0: resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== +chrome-launcher@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-1.1.2.tgz#52eff6b3fd7f24b65192b2624a108dadbcca4b9d" + integrity sha512-YclTJey34KUm5jB1aEJCq807bSievi7Nb/TU4Gu504fUYi3jw3KCIaH6L7nFWQhdEgH3V+wCh+kKD1P5cXnfxw== + dependencies: + "@types/node" "*" + escape-string-regexp "^4.0.0" + is-wsl "^2.2.0" + lighthouse-logger "^2.0.1" + chrome-remote-interface@^0.31.3: version "0.31.3" resolved "https://registry.yarnpkg.com/chrome-remote-interface/-/chrome-remote-interface-0.31.3.tgz#bd01b89f5f0e968f7eeb37b8b7c5ac20e6e1f4d0" @@ -2266,6 +2509,15 @@ chromedriver@^130.0.0: proxy-from-env "^1.1.0" tcp-port-used "^1.0.2" +chromium-bidi@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/chromium-bidi/-/chromium-bidi-0.8.0.tgz#ffd79dad7db1fcc874f1c55fcf46ded05a884269" + integrity sha512-uJydbGdTw0DEUjhoogGveneJVWX/9YuqkWePzMmkBYwtdAqo5d3J/ovNKFr+/2hWXYmYCr6it8mSSTIj6SS6Ug== + dependencies: + mitt "3.0.1" + urlpattern-polyfill "10.0.0" + zod "3.23.8" + ci-info@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" @@ -2284,6 +2536,15 @@ cli-argument-parser@0.7.4: dotenv "16.4.5" file-exists "5.0.1" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + cluster-key-slot@1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" @@ -2398,6 +2659,16 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== +cosmiconfig@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" + integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== + dependencies: + env-paths "^2.2.1" + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + crc-32@^1.2.0: version "1.2.2" resolved "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz" @@ -2483,6 +2754,13 @@ debug@4.3.1: dependencies: ms "2.1.2" +debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@^3.2.7: version "3.2.7" resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" @@ -2490,7 +2768,7 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.6, debug@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -2536,6 +2814,11 @@ deep-is@^0.1.3: resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + default-browser-id@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26" @@ -2648,6 +2931,21 @@ device-specs@^1.0.0: resolved "https://registry.yarnpkg.com/device-specs/-/device-specs-1.0.1.tgz#b1a26c717a5339815238abf07f427e0b340d35ac" integrity sha512-rxns/NDZfbdYumnn801z9uo8kWIz3Eld7Bk/F0V9zw4sZemSoD93+gxHEonLdxYulkws4iCMt7ZP8zuM8EzUSg== +devtools-protocol@0.0.1354347: + version "0.0.1354347" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1354347.tgz#5cb509610b8f61fc69a31e5c810d5bed002d85ea" + integrity sha512-BlmkSqV0V84E2WnEnoPnwyix57rQxAM5SKJjf4TbYOCGLAWtz8CDH8RIaGOjPgPCXo2Mce3kxSY497OySidY3Q== + +devtools-protocol@0.0.927104: + version "0.0.927104" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.927104.tgz#3bba0fca644bcdce1bcebb10ae392ab13428a7a0" + integrity sha512-5jfffjSuTOv0Lz53wTNNTcCUV8rv7d82AhYcapj28bC2B5tDxEZzVb7k51cNxZP2KHw24QE+sW7ZuSeD9NfMpA== + +devtools-protocol@0.0.1109433: + version "0.0.1109433" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.1109433.tgz#94f2a8cb05d3d80701e4f5ec99a48f2f231ac85c" + integrity sha512-w1Eqih66egbSr2eOoGZ+NsdF7HdxmKDo3pKFBySEGsmVvwWWNXzNCDcKrbFnd23Jf7kH1M806OfelXwu+Jk11g== + dezalgo@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" @@ -2773,7 +3071,7 @@ entities@^4.5.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== -env-paths@^2.2.0: +env-paths@^2.2.0, env-paths@^2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== @@ -2783,6 +3081,13 @@ err-code@^2.0.2: resolved "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz" integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + error-stack-parser@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" @@ -2893,7 +3198,7 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -escalade@^3.2.0: +escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== @@ -3385,6 +3690,11 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + get-func-name@^2.0.0, get-func-name@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" @@ -3464,6 +3774,15 @@ getos@^3.2.1: dependencies: async "^3.2.0" +ghost-cursor@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ghost-cursor/-/ghost-cursor-1.3.0.tgz#e53e89f3342711d04bd26a0e454aea482ddca9fd" + integrity sha512-niTjgH8o2EhYG0vJfGB45JrLAt5CRoXg+8zWIcXtcgUgLds+i6YyFXkYuHpno9gKi93KpQvxY/uVYMV9CeeJ0w== + dependencies: + "@types/bezier-js" "4" + bezier-js "^6.1.3" + debug "^4.3.4" + github-from-package@0.0.0: version "0.0.0" resolved "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz" @@ -3789,12 +4108,7 @@ ignore@^5.1.1, ignore@^5.2.0, ignore@^5.3.1: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== -immediate@~3.0.5: - version "3.0.6" - resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz" - integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== - -import-fresh@^3.2.1: +import-fresh@^3.2.1, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -3884,6 +4198,11 @@ is-array-buffer@^3.0.4: call-bind "^1.0.2" get-intrinsic "^1.2.1" +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" @@ -4126,6 +4445,13 @@ is-weakref@^1.0.2: dependencies: call-bind "^1.0.2" +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + is-wsl@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" @@ -4225,6 +4551,11 @@ json-buffer@3.0.1: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== +json-parse-even-better-errors@^2.3.0: + 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== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" @@ -4256,16 +4587,6 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -jszip@^3.10.1: - version "3.10.1" - resolved "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz" - integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== - dependencies: - lie "~3.3.0" - pako "~1.0.2" - readable-stream "~2.3.6" - setimmediate "^1.0.5" - keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -4293,12 +4614,18 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lie@~3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz" - integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== +lighthouse-logger@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-2.0.1.tgz#48895f639b61cca89346bb6f47f7403a3895fa02" + integrity sha512-ioBrW3s2i97noEmnXxmUq7cjIcVRjT5HBpAYy8zE11CxU9HqlWHHeRxfeN1tn8F7OEMVPIC9x1f8t3Z7US9ehQ== dependencies: - immediate "~3.0.5" + debug "^2.6.9" + marky "^1.2.2" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== linux-platform-info@^0.0.3: version "0.0.3" @@ -4425,6 +4752,11 @@ make-fetch-happen@^9.1.0: socks-proxy-agent "^6.0.0" ssri "^8.0.0" +marky@^1.2.2: + version "1.2.5" + resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" + integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q== + match-url-wildcard@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/match-url-wildcard/-/match-url-wildcard-0.0.4.tgz#c8533da7ec0901eddf01fc0893effa68d4e727d6" @@ -4604,6 +4936,11 @@ minizlib@^2.0.0, minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" +mitt@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" + integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: version "0.5.3" resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz" @@ -4636,6 +4973,11 @@ moment@^2.14.1, moment@^2.29.4: resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -4672,6 +5014,11 @@ mustache@^2.1.1, mustache@^2.1.2, mustache@^2.2.1, mustache@^2.3.0: resolved "https://registry.npmjs.org/mustache/-/mustache-2.3.2.tgz" integrity sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ== +nan@^2.13.2: + version "2.22.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.0.tgz#31bc433fc33213c97bad36404bb68063de604de3" + integrity sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw== + nanoid@^3.1.12, nanoid@^3.1.31: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -4970,11 +5317,6 @@ package-json-from-dist@^1.0.0: resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== -pako@~1.0.2: - version "1.0.11" - resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -4982,6 +5324,16 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + parse5@^1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" @@ -5151,6 +5503,11 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== +progress@^2.0.3: + version "2.0.3" + 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.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" @@ -5248,6 +5605,51 @@ punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +puppeteer-core@23.7.0: + version "23.7.0" + resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-23.7.0.tgz#b737476f8f5e2a36a6683d91595eaa5c0e231a37" + integrity sha512-0kC81k3K6n6Upg/k04xv+Mi8yy62bNAJiK7LCA71zfq2XKEo9WAzas1t6UQiLgaNHtGNKM0d1KbR56p/+mgEiQ== + dependencies: + "@puppeteer/browsers" "2.4.1" + chromium-bidi "0.8.0" + debug "^4.3.7" + devtools-protocol "0.0.1354347" + typed-query-selector "^2.12.0" + ws "^8.18.0" + +puppeteer-extra@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/puppeteer-extra/-/puppeteer-extra-3.3.6.tgz#fc16ff396aae52664842da9a557ea8fa51eaa8b7" + integrity sha512-rsLBE/6mMxAjlLd06LuGacrukP2bqbzKCLzV1vrhHFavqQE/taQ2UXv3H5P0Ls7nsrASa+6x3bDbXHpqMwq+7A== + dependencies: + "@types/debug" "^4.1.0" + debug "^4.1.1" + deepmerge "^4.2.2" + +puppeteer-real-browser@^1.3.17: + version "1.3.17" + resolved "https://registry.yarnpkg.com/puppeteer-real-browser/-/puppeteer-real-browser-1.3.17.tgz#64cb239b71692bbeec84ea666f93ff4bf59f6887" + integrity sha512-wHVpy0IsoXKDRFGRs2voLyGr4JYLbc66mDGmONss8ZY5jL5fMzV97SmoEHvoGW+qf2YROvjx5BvdDEc9cga+KQ== + dependencies: + chrome-launcher "^1.1.2" + ghost-cursor "^1.3.0" + puppeteer-extra "^3.3.6" + rebrowser-puppeteer-core "^23.6.101" + tree-kill "^1.2.2" + xvfb "^0.4.0" + +puppeteer@^23.7.0: + version "23.7.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-23.7.0.tgz#193dcc78bdcc5d3023cc172e9231771c350484bd" + integrity sha512-YTgo0KFe8NtBcI9hCu/xsjPFumEhu8kA7QqLr6Uh79JcEsUcUt+go966NgKYXJ+P3Fuefrzn2SXwV3cyOe/UcQ== + dependencies: + "@puppeteer/browsers" "2.4.1" + chromium-bidi "0.8.0" + cosmiconfig "^9.0.0" + devtools-protocol "0.0.1354347" + puppeteer-core "23.7.0" + typed-query-selector "^2.12.0" + qrcode-terminal@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/qrcode-terminal/-/qrcode-terminal-0.10.0.tgz#a76a48e2610a18f97fa3a2bd532b682acff86c53" @@ -5297,7 +5699,7 @@ read-file-relative@^1.2.0: dependencies: callsite "^1.0.0" -readable-stream@^2.0.1, readable-stream@^2.0.5, readable-stream@~2.3.6: +readable-stream@^2.0.1, readable-stream@^2.0.5: version "2.3.8" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== @@ -5337,6 +5739,18 @@ readdir-glob@^1.1.2: dependencies: minimatch "^5.1.0" +rebrowser-puppeteer-core@^23.6.101: + version "23.6.101" + resolved "https://registry.yarnpkg.com/rebrowser-puppeteer-core/-/rebrowser-puppeteer-core-23.6.101.tgz#b610e2100a8a7f2d2d500d69be785237b19272c7" + integrity sha512-nSrIg0Uv50RkFwQWxd1v9Kq0J2P1zcG5+xUSUzs2YIJCiRb8I3YUTN5wytt7i/P0HRFBxLyBy+RRGIMDFZf2yg== + dependencies: + "@puppeteer/browsers" "2.4.0" + chromium-bidi "0.8.0" + debug "^4.3.7" + devtools-protocol "0.0.1354347" + typed-query-selector "^2.12.0" + ws "^8.18.0" + redis@4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/redis/-/redis-4.7.0.tgz#b401787514d25dd0cfc22406d767937ba3be55d6" @@ -5419,6 +5833,11 @@ replicator@^1.0.5: resolved "https://registry.yarnpkg.com/replicator/-/replicator-1.0.5.tgz#f1e56df7e276a62afe80c2248b8ac03896f4708f" integrity sha512-saxS4y7NFkLMa92BR4bPHR41GD+f/qoDAwD2xZmN+MpDXgibkxwLO2qk7dCHYtskSkd/bWS8Jy6kC5MZUkg1tw== +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" @@ -5537,17 +5956,7 @@ sanitize-filename@^1.6.0: dependencies: truncate-utf8-bytes "^1.0.0" -selenium-webdriver@^4.25.0: - version "4.25.0" - resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.25.0.tgz#3562b49668817974bb1d13d25a50e8bc0264fcf3" - integrity sha512-zl9IX93caOT8wbcCpZzAkEtYa+hNgJ4C5GUN8uhpzggqRLvsg1asfKi0p1uNZC8buYVvsBZbx8S+9MjVAjs4oA== - dependencies: - "@bazel/runfiles" "^5.8.1" - jszip "^3.10.1" - tmp "^0.2.3" - ws "^8.18.0" - -semver@7.5.3, semver@^6.0.0, semver@^6.3.1, semver@^7.3.5, semver@^7.5.2, semver@^7.5.3, semver@^7.6.0: +semver@7.5.3, semver@^6.0.0, semver@^6.3.1, semver@^7.3.5, semver@^7.5.2, semver@^7.5.3, semver@^7.6.0, semver@^7.6.3: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== @@ -5594,11 +6003,6 @@ set-value@^4.1.0: is-plain-object "^2.0.4" is-primitive "^3.0.1" -setimmediate@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" - integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -5664,6 +6068,13 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +sleep@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/sleep/-/sleep-6.1.0.tgz#5507b520556a82ffb983d39123c5459470fa2a9e" + integrity sha512-Z1x4JjJxsru75Tqn8F4tnOFeEu3HjtITTsumYUiuz54sGKdISgLCek9AUlXlVVrkhltRFhNUsJDJE76SFHTDIQ== + dependencies: + nan "^2.13.2" + smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" @@ -5745,7 +6156,7 @@ stackframe@^1.3.4: resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== -streamx@^2.15.0: +streamx@^2.15.0, streamx@^2.20.0: version "2.20.1" resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.20.1.tgz#471c4f8b860f7b696feb83d5b125caab2fdbb93c" integrity sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA== @@ -5764,7 +6175,6 @@ streamx@^2.15.0: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" - "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -5831,7 +6241,6 @@ string_decoder@~1.1.1: integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" - strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -5925,6 +6334,17 @@ tar-fs@^2.0.0: pump "^3.0.0" tar-stream "^2.1.4" +tar-fs@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.0.6.tgz#eaccd3a67d5672f09ca8e8f9c3d2b89fa173f217" + integrity sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w== + dependencies: + pump "^3.0.0" + tar-stream "^3.1.5" + optionalDependencies: + bare-fs "^2.1.1" + bare-path "^2.1.0" + tar-stream@^2.1.4: version "2.2.0" resolved "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz" @@ -5936,7 +6356,7 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -tar-stream@^3.0.0: +tar-stream@^3.0.0, tar-stream@^3.1.5: version "3.1.7" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" integrity sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ== @@ -6007,38 +6427,7 @@ testcafe-browser-tools@2.0.26: read-file-relative "^1.2.0" which-promise "^1.0.0" -testcafe-hammerhead@31.7.2: - version "31.7.2" - resolved "https://registry.yarnpkg.com/testcafe-hammerhead/-/testcafe-hammerhead-31.7.2.tgz#bbe09be27f19216dd119cf065b80644df5cbf257" - integrity sha512-wjZ3Y4fXnew6WaoMhD7jTe/zrzSYJMLZulX+/pXS6xed9meUx7zzCSc5epPJEW8Xy3Zo09n7w+m7+2SDej0/Iw== - dependencies: - "@adobe/css-tools" "^4.3.0-rc.1" - "@electron/asar" "^3.2.3" - acorn-hammerhead "0.6.2" - bowser "1.6.0" - crypto-md5 "^1.0.0" - debug "4.3.1" - esotope-hammerhead "0.6.8" - http-cache-semantics "^4.1.0" - httpntlm "^1.8.10" - iconv-lite "0.5.1" - lodash "^4.17.21" - lru-cache "2.6.3" - match-url-wildcard "0.0.4" - merge-stream "^1.0.1" - mime "~1.4.1" - mustache "^2.1.1" - nanoid "^3.1.12" - os-family "^1.0.0" - parse5 "^7.1.2" - pinkie "2.0.4" - read-file-relative "^1.2.0" - semver "7.5.3" - tough-cookie "4.1.3" - tunnel-agent "0.6.0" - ws "^7.4.6" - -testcafe-hammerhead@>=19.4.0: +testcafe-hammerhead@31.7.3, testcafe-hammerhead@>=19.4.0: version "31.7.3" resolved "https://registry.yarnpkg.com/testcafe-hammerhead/-/testcafe-hammerhead-31.7.3.tgz#46e72e153a8ea7804571bb1e6adc48d2b60dff80" integrity sha512-LmldhnuUUNcel66z8hjwPkxGrA6jaGt6K9B8iuxOVVRuhpqFfmP3do5MeplK9NyPbIjkAW6WsHDu+nUM88IUsA== @@ -6118,32 +6507,28 @@ testcafe-reporter-xunit@^2.2.1: resolved "https://registry.yarnpkg.com/testcafe-reporter-xunit/-/testcafe-reporter-xunit-2.2.3.tgz#3636884e0351867e4b1beacf00545febad939d3f" integrity sha512-aGyc+MZPsTNwd9SeKJSjFNwEZfILzFnObzOImaDbsf57disTQfEY+9japXWav/Ef5Cv04UEW24bTFl2Q4f8xwg== -testcafe-safe-storage@^1.1.1: - version "1.1.6" - resolved "https://registry.yarnpkg.com/testcafe-safe-storage/-/testcafe-safe-storage-1.1.6.tgz#5a325c24aef538cf18843b430a1033cba8a32477" - integrity sha512-WFm1UcmO3uZs+uW8lYtBBJpnrvgTKkMQMKG9BvTEKbjeqhonEXVTxOkGEs3DM1ZB/ylPuwh7Jux7qUtjcM/D2Q== - testcafe-selector-generator@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/testcafe-selector-generator/-/testcafe-selector-generator-0.1.0.tgz#852c86f71565e5d9320da625c2260d040cbed786" integrity sha512-MTw+RigHsEYmFgzUFNErDxui1nTYUk6nm2bmfacQiKPdhJ9AHW/wue4J/l44mhN8x3E8NgOUkHHOI+1TDFXiLQ== -testcafe@3.6.2: - version "3.6.2" - resolved "https://registry.yarnpkg.com/testcafe/-/testcafe-3.6.2.tgz#53d5391d65a7b64a2eb14ec053266dc00d69bce2" - integrity sha512-y7PGzuSQt82iSJNYkN7/78PsviyFZOSQDYkHXb8UFj7BKCgrLONxZ+WZ5uk5tb1tHU/sKTHkWTwVJHkGXIamVg== +testcafe@3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/testcafe/-/testcafe-3.7.0.tgz#2fddf5093029062653f7feaf25c0e2f6110ad78e" + integrity sha512-e77yuzX/1eSqb6ctE0oW+PJbgsA975Ui8xYiVAFTQjZnt9oZixB7gEgbwRdIls5EEWmtcxJ5tvqTm1rPBsrOvA== dependencies: "@babel/core" "^7.23.2" - "@babel/plugin-proposal-async-generator-functions" "^7.20.7" - "@babel/plugin-proposal-class-properties" "^7.18.6" "@babel/plugin-proposal-decorators" "^7.23.2" - "@babel/plugin-proposal-object-rest-spread" "^7.20.7" - "@babel/plugin-proposal-private-methods" "^7.18.6" "@babel/plugin-syntax-dynamic-import" "^7.8.3" "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-transform-async-generator-functions" "^7.25.4" "@babel/plugin-transform-async-to-generator" "^7.22.5" + "@babel/plugin-transform-class-properties" "^7.25.4" + "@babel/plugin-transform-class-static-block" "^7.24.7" "@babel/plugin-transform-exponentiation-operator" "^7.22.5" "@babel/plugin-transform-for-of" "^7.22.15" + "@babel/plugin-transform-object-rest-spread" "^7.24.7" + "@babel/plugin-transform-private-methods" "^7.25.4" "@babel/plugin-transform-runtime" "7.23.3" "@babel/preset-env" "^7.23.2" "@babel/preset-flow" "^7.22.15" @@ -6151,7 +6536,7 @@ testcafe@3.6.2: "@babel/runtime" "^7.23.2" "@devexpress/bin-v8-flags-filter" "^1.3.0" "@devexpress/callsite-record" "^4.1.6" - "@types/node" "^20.14.5" + "@types/node" "20.14.5" address "^2.0.2" async-exit-hook "^1.1.2" babel-plugin-module-resolver "5.0.0" @@ -6167,6 +6552,7 @@ testcafe@3.6.2: dedent "^0.4.0" del "^3.0.0" device-specs "^1.0.0" + devtools-protocol "0.0.1109433" diff "^4.0.2" elegant-spinner "^1.0.1" email-validator "^2.0.4" @@ -6214,14 +6600,13 @@ testcafe@3.6.2: source-map-support "^0.5.16" strip-bom "^2.0.0" testcafe-browser-tools "2.0.26" - testcafe-hammerhead "31.7.2" + testcafe-hammerhead "31.7.3" testcafe-legacy-api "5.1.8" testcafe-reporter-json "^2.1.0" testcafe-reporter-list "^2.2.0" testcafe-reporter-minimal "^2.2.0" testcafe-reporter-spec "^2.2.0" testcafe-reporter-xunit "^2.2.1" - testcafe-safe-storage "^1.1.1" testcafe-selector-generator "^0.1.0" time-limit-promise "^1.0.2" tmp "0.0.28" @@ -6242,6 +6627,11 @@ text-table@^0.2.0: resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + time-limit-promise@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/time-limit-promise/-/time-limit-promise-1.0.4.tgz#33e928212273c70d52153c28ad2a7e3319b975f9" @@ -6254,11 +6644,6 @@ tmp@0.0.28: dependencies: os-tmpdir "~1.0.1" -tmp@^0.2.3: - version "0.2.3" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz" - integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== - to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -6400,6 +6785,11 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" +typed-query-selector@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/typed-query-selector/-/typed-query-selector-2.12.0.tgz#92b65dbc0a42655fccf4aeb1a08b1dddce8af5f2" + integrity sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg== + typescript@4.7.4: version "4.7.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" @@ -6420,6 +6810,14 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +unbzip2-stream@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" + integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + underscore@~1.12.1: version "1.12.1" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" @@ -6505,6 +6903,11 @@ url-to-options@^2.0.0: resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-2.0.0.tgz#416bfe77868b168b8aa7b72d74e7c29a97dca69d" integrity sha512-mfONnc9dqO0J41wUh/El+plDskrIJRcyLcx6WjEGYW2K11RnjPDAgeoNFCallADaYJfcWIvAlYyZPBw02AbfIQ== +urlpattern-polyfill@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz#f0a03a97bfb03cdf33553e5e79a2aadd22cac8ec" + integrity sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg== + utf8-byte-length@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz#f9f63910d15536ee2b2d5dd4665389715eac5c1e" @@ -6597,7 +7000,6 @@ word-wrap@1.2.4, word-wrap@^1.2.5: ansi-styles "^4.0.0" string-width "^4.1.0" strip-ansi "^6.0.0" - wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -6631,6 +7033,18 @@ ws@^8.18.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== +xvfb@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/xvfb/-/xvfb-0.4.0.tgz#52c4ddb991b7c5ae9d175d35452718b734781e85" + integrity sha512-g55AbjcBL4Bztfn7kiUrR0ne8mMUsFODDJ+HFGf5OuHJqKKccpExX2Qgn7VF2eImw1eoh6+riXHser1J4agrFA== + optionalDependencies: + sleep "6.1.0" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + yallist@4.0.0, yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" @@ -6641,6 +7055,24 @@ yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +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@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yauzl@^2.10.0: version "2.10.0" resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz" @@ -6667,3 +7099,8 @@ zip-stream@^6.0.1: archiver-utils "^5.0.0" compress-commons "^6.0.2" readable-stream "^4.0.0" + +zod@3.23.8: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== diff --git a/yarn.lock b/yarn.lock index 7791decbaa..56a809d3fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1190,6 +1190,21 @@ optionalDependencies: global-agent "^3.0.0" +"@electron/node-gyp@https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2": + version "10.2.0-electron.1" + resolved "https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2" + dependencies: + env-paths "^2.2.0" + exponential-backoff "^3.1.1" + glob "^8.1.0" + graceful-fs "^4.2.6" + make-fetch-happen "^10.2.1" + nopt "^6.0.0" + proc-log "^2.0.1" + semver "^7.3.5" + tar "^6.2.1" + which "^2.0.2" + "@electron/notarize@2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@electron/notarize/-/notarize-2.2.1.tgz#d0aa6bc43cba830c41bfd840b85dbe0e273f59fe" @@ -1211,11 +1226,12 @@ minimist "^1.2.6" plist "^3.0.5" -"@electron/rebuild@^3.3.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.6.0.tgz#60211375a5f8541a71eb07dd2f97354ad0b2b96f" - integrity sha512-zF4x3QupRU3uNGaP5X1wjpmcjfw1H87kyqZ00Tc3HvriV+4gmOGuvQjGNkrJuXdsApssdNyVwLsy+TaeTGGcVw== +"@electron/rebuild@^3.7.1": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.7.1.tgz#27ed124f7f1dbed92b222aabe68c0e4a3e6c5cea" + integrity sha512-sKGD+xav4Gh25+LcLY0rjIwcCFTw+f/HU1pB48UVbwxXXRGaXEqIH0AaYKN46dgd/7+6kuiDXzoyAEvx1zCsdw== dependencies: + "@electron/node-gyp" "https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2" "@malept/cross-spawn-promise" "^2.0.0" chalk "^4.0.0" debug "^4.1.1" @@ -1224,7 +1240,6 @@ got "^11.7.0" node-abi "^3.45.0" node-api-version "^0.2.0" - node-gyp "^9.0.0" ora "^5.1.0" read-binary-file-arch "^1.0.6" semver "^7.3.5" @@ -3682,19 +3697,6 @@ app-builder-lib@24.13.3: tar "^6.1.12" temp-file "^3.4.0" -"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== - -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" - arg@^4.1.0: version "4.1.3" resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" @@ -4587,11 +4589,6 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -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== - colord@^2.9.3: version "2.9.3" resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" @@ -4716,11 +4713,6 @@ connection-string@^4.3.2: resolved "https://registry.yarnpkg.com/connection-string/-/connection-string-4.3.6.tgz#4aa23f0b6d31f6a310afffd4e9a481d000c64836" integrity sha512-dwaq4BMeiIIWry/oQxjstPB7Xp0K1nGjaY8p6PHQB+J9ZJuIvNp7ux3Noupq0hMd/dqEHXkfdmmGFOKTbGKWmw== -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 sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== - construct-style-sheets-polyfill@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/construct-style-sheets-polyfill/-/construct-style-sheets-polyfill-3.1.0.tgz#c490abd79efdb359fafa62ec14ea55232be0eecf" @@ -5477,11 +5469,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== - dequal@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" @@ -5894,10 +5881,10 @@ electron-updater@^6.3.9: semver "^7.6.3" tiny-typed-emitter "^2.1.0" -electron@31.0.2: - version "31.0.2" - resolved "https://registry.yarnpkg.com/electron/-/electron-31.0.2.tgz#9b719fe6072060fe74cb609bcbb84694abce5b17" - integrity sha512-55efQ5yfLN+AQHcFC00AXQqtxC3iAGaxX2GQ3EDbFJ0ca9GHNOdSXkcrdBElLleiDrR2hpXNkQxN1bDn0oxe6w== +electron@33.2.0: + version "33.2.0" + resolved "https://registry.yarnpkg.com/electron/-/electron-33.2.0.tgz#2a7098653eaf1a53c7311a01d5636783019f2354" + integrity sha512-PVw1ICAQDPsnnsmpNFX/b1i/49h67pbSPxuIENd9K9WpGO1tsRaQt+K2bmXqTuoMJsbzIc75Ce8zqtuwBPqawA== dependencies: "@electron/get" "^2.0.0" "@types/node" "^20.9.0" @@ -6941,20 +6928,6 @@ fzstd@^0.1.0: resolved "https://registry.yarnpkg.com/fzstd/-/fzstd-0.1.0.tgz#1d7bccb5f819e2d073a15fccea1adb7fdc7a46a9" integrity sha512-TTvznnpde1rTv/3FOmZMbPCh75gqkJ9dKJdsKmcNiXh4olLJl3eRahHJcGzyPZjuVaytffP02Or4z5avDvqKQA== -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" - gaxios@^6.0.0, gaxios@^6.0.3: version "6.1.0" resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.1.0.tgz#8ab08adbf9cc600368a57545f58e004ccf831ccb" @@ -7071,7 +7044,7 @@ glob@^7.0.5, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.1: +glob@^8.0.1, glob@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== @@ -7272,11 +7245,6 @@ has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" -has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== - has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -8284,12 +8252,12 @@ 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== +java-object-serialization@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/java-object-serialization/-/java-object-serialization-0.1.2.tgz#229ee4b70ea89e72206e32a826afacd09d9badfd" + integrity sha512-l0V2a/E7r6ScqG+ne09KR7G1npbiVdDf3Vdk6fcn8jy5TQiTTOQbDg9OfBriv2NmnCxcm4NmFW2dAD9QaN/htw== dependencies: - tslib "^2.1.0" + tslib "^2.8.0" jest-changed-files@^29.7.0: version "29.7.0" @@ -8464,6 +8432,14 @@ jest-haste-map@^29.7.0: optionalDependencies: fsevents "^2.3.2" +jest-html-reporters@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/jest-html-reporters/-/jest-html-reporters-3.1.7.tgz#d8cb6f5d15fd518e601841f90165f37765e7ff34" + integrity sha512-GTmjqK6muQ0S0Mnksf9QkL9X9z2FGIpNSxC52E0PHDzjPQ1XDu2+XTI3B3FS43ZiUzD1f354/5FfwbNIBzT7ew== + dependencies: + fs-extra "^10.0.0" + open "^8.0.3" + jest-leak-detector@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" @@ -9260,7 +9236,7 @@ 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@^10.0.3: +make-fetch-happen@^10.2.1: version "10.2.1" resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz#f5e3835c5e9817b617f2770870d9492d28678164" integrity sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w== @@ -10096,9 +10072,9 @@ no-case@^3.0.4: tslib "^2.0.3" node-abi@^3.45.0: - version "3.54.0" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.54.0.tgz#f6386f7548817acac6434c6cba02999c9aebcc69" - integrity sha512-p7eGEiQil0YUV3ItH4/tBb781L5impVmmx2E9FRKF7d18XXzp4PGT2tdYMFY6wQqgxD0IwNZOiSJ0/K0fSi/OA== + version "3.71.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.71.0.tgz#52d84bbcd8575efb71468fbaa1f9a49b2c242038" + integrity sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw== dependencies: semver "^7.3.5" @@ -10133,23 +10109,6 @@ node-gyp-build-optional-packages@5.0.7: resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz#5d2632bbde0ab2f6e22f1bbac2199b07244ae0b3" integrity sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w== -node-gyp@^9.0.0: - version "9.4.1" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" - integrity sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ== - dependencies: - env-paths "^2.2.0" - exponential-backoff "^3.1.1" - glob "^7.1.4" - graceful-fs "^4.2.6" - make-fetch-happen "^10.0.3" - nopt "^6.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" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -10215,16 +10174,6 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -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" - nth-check@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" @@ -10332,7 +10281,7 @@ open@^7.4.2: is-docker "^2.0.0" is-wsl "^2.1.1" -open@^8.4.0: +open@^8.0.3, open@^8.4.0: version "8.4.2" resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== @@ -10984,6 +10933,11 @@ prismjs@~1.27.0: resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057" integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA== +proc-log@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-2.0.1.tgz#8f3f69a1f608de27878f91f5c688b225391cb685" + integrity sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw== + 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" @@ -11531,7 +11485,7 @@ readable-stream@^2.0.0, readable-stream@^2.3.5, readable-stream@~2.3.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.4.0, readable-stream@^3.6.0: +readable-stream@^3.4.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -12218,11 +12172,6 @@ serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: dependencies: randombytes "^2.1.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 sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - set-cookie-parser@^2.4.6: version "2.6.0" resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" @@ -12623,7 +12572,7 @@ string-length@^4.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +string-width@^4.1.0, string-width@^4.2.0, 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== @@ -12921,7 +12870,7 @@ tar-stream@^3.1.7: fast-fifo "^1.2.0" streamx "^2.15.0" -tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2: +tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== @@ -13208,10 +13157,10 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" - integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== tsutils@^3.21.0: version "3.21.0" @@ -14063,13 +14012,6 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" -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 || 3 || 4" - wildcard@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67"