diff --git a/.circleci/config.yml b/.circleci/config.yml index 4ea0e3c2..57090b51 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,610 +1,25 @@ -# Use the latest 2.1 version of CircleCI pipeline process engine. -# See: https://circleci.com/docs/configuration-reference version: 2.1 -aliases: - ui-deps-cache-key: &uiDepsCacheKey - key: ui-deps-{{ checksum "yarn.lock" }} - dev-filter: &devFilter - filters: - branches: - only: - - main - - /^build\/.*/ - build-filter: &buildFilter - filters: - branches: - only: - - /^build.*/ - stage-filter: &stageFilter - filters: - branches: - only: - - /^release.*/ - prod-filter: &prodFilter - filters: - branches: - only: - - latest - manual-build-conditions: &manual-build-conditions - or: - - << pipeline.parameters.linux >> - - << pipeline.parameters.mac >> - - << pipeline.parameters.windows >> - ignore-for-manual-build: &ignore-for-manual-build - when: - not: *manual-build-conditions - -orbs: - node: circleci/node@5.3.0 - win: circleci/windows@5.0.0 - aws: circleci/aws-cli@4.1.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 jobs: - unit-tests-ui: - executor: docker-node - steps: - - checkout - - restore_cache: - <<: *uiDepsCacheKey - - run: - name: Install dependencies - command: | - yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - run: - name: Run Unit tests UI - command: | - yarn test:cov - - save_cache: - <<: *uiDepsCacheKey - paths: - - ~/.cache/yarn - - e2e-linux: - 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: Start Xvfb - command: | - sudo apt-get install -y xvfb - Xvfb :99 -screen 0 1920x1080x24 & - - run: - name: Install dependencies - command: cd tests/e2e && yarn install - - run: - name: Compile TypeScript - command: cd tests/e2e && yarn compile - - run: - name: Verify Compiled Files - command: ls -R tests/e2e/dist/tests/ - - run: - name: Run e2e tests - command: | - cd tests/e2e/dist - export TEST_FILES=$(circleci tests glob "tests/**/*.e2e.js" | circleci tests split --split-by=timings) - echo "Running tests: $TEST_FILES" - cd ../../.. - .circleci/e2e/test.app.sh - - when: - condition: - equal: [ true, << parameters.report >> ] - steps: - - run: - name: Send report - when: always - command: | - APP_BUILD_TYPE="VSCode (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/mochawesome-report - - store_artifacts: - path: tests/e2e/mochawesome-report - destination: tests/e2e/mochawesome-report - - store_artifacts: - path: /tmp/test-resources/screenshots - destination: test-resources/screenshots - - # Build jobs - manual-build-validate: + # Placeholder job + placeholder-job: 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 >> - - linux: - executor: docker-node - 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 - - restore_cache: - <<: *uiDepsCacheKey - - attach_workspace: - at: . - - run: - name: Install dependencies - command: | - yarn install --frozen-lockfile --cache-folder ~/.cache/yarn - - run: - name: Build .vsix package - command: | - envFile=".env" - packagePath="./release/redis-for-vscode-extension-linux-x64.vsix" - yarn download:backend - - if [ << parameters.env >> == 'prod' ]; then - echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY'" >> $envFile - yarn package:prod --target linux-x64 --out ${packagePath} - exit 0; - fi - - echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY_STAGE'" >> $envFile - sed -i "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" $envFile - yarn package:stage --target linux-x64 --out ${packagePath} - - persist_to_workspace: - root: . - paths: - - release/redis-for-*.vsix - - save_cache: - <<: *uiDepsCacheKey - paths: - - ~/.cache/yarn - - macosx: - executor: macos - resource_class: macos.m1.medium.gen1 - 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: - node-version: '20.15' - - attach_workspace: - at: . - - run: - name: Install dependencies - command: | - yarn install - no_output_timeout: 15m - - run: - name: Build .vsix package - command: | - envFile=".env" - packagePath=./release/redis-for-vscode-extension-mac - - if [ << parameters.target >> ]; then - yarn download:backend << parameters.target >> - - echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY_STAGE'" >> $envFile - sed -i '' "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" $envFile - yarn package:stage --target darwin-<< parameters.target >> --out ${packagePath}-<< parameters.target >>.vsix - exit 0; - fi - - if [ << parameters.env >> == 'prod' ]; then - echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY'" >> $envFile - yarn download:backend arm64 - yarn package:prod --target darwin-arm64 --out ${packagePath}-arm64.vsix - - yarn download:backend x64 - yarn package:prod --target darwin-x64 --out ${packagePath}-x64.vsix - exit 0; - fi - - echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY_STAGE'" >> $envFile - sed -i '' "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" $envFile - yarn download:backend arm64 - yarn package:stage --target darwin-arm64 --out ${packagePath}-arm64.vsix - - yarn download:backend x64 - yarn package:stage --target darwin-x64 --out ${packagePath}-x64.vsix - - persist_to_workspace: - root: . - paths: - - release/redis-for-*.vsix - - 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: - name: Install dependencies - command: | - nvm install 20.15 - nvm use 20.15 - npm install --global yarn - - yarn install - shell: bash.exe - no_output_timeout: 15m - - run: - name: Build .vsix package - command: | - envFile=".env" - packagePath=./release/redis-for-vscode-extension-win-x64.vsix - yarn download:backend - - if [ << parameters.env >> == 'prod' ]; then - echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY'" >> $envFile - yarn package:prod --target win32-x64 --out ${packagePath} - exit 0; - fi - - sed -i "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" $envFile - yarn package:stage --target win32-x64 --out ${packagePath} - shell: bash.exe - no_output_timeout: 20m - - persist_to_workspace: - root: . - paths: - - release/redis-for-*.vsix - - # Release jobs - store-build-artifacts: - executor: linux-executor - steps: - - attach_workspace: - at: . - - store_artifacts: - path: release - destination: release - - release-aws-private: - executor: linux-executor - steps: - - checkout - - attach_workspace: - at: . - - store_artifacts: - path: release - destination: release - - run: - name: publish - command: | - chmod +x .circleci/build/sum_sha256.sh - .circleci/build/sum_sha256.sh - applicationVersion=$(jq -r '.version' package.json) - - aws s3 cp release/ s3://${AWS_BUCKET_NAME}/private/vscode/${applicationVersion} --recursive - - licenses-check: - executor: linux-executor steps: - checkout - - node/install: - install-yarn: true - node-version: '20.15' - - restore_cache: - <<: *uiDepsCacheKey - - run: - name: Run install all dependencies - command: | - yarn install - yarn --cwd tests/e2e install - - 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 -# Orchestrate jobs using workflows -# See: https://circleci.com/docs/configuration-reference/#workflows workflows: - license-checker: + # Placeholder workflow + placeholder: jobs: - - licenses-check: - name: Run License checker + - placeholder-job: + name: Placeholder filters: branches: only: - - /^license.*/ - - frontend-tests: - <<: *ignore-for-manual-build - jobs: - - unit-tests-ui: - name: Run Unit Tests - filters: - branches: - only: - - /^feature.*/ - - e2e-tests: - jobs: - - approve: - name: Start E2E Tests - type: approval - filters: - branches: - only: - - /^e2e/feature.*/ - - /^e2e/bugfix.*/ - - - linux: - name: Build extension - Linux (stage) - env: stage - filters: - branches: - only: - - /^e2e/feature.*/ - - /^e2e/bugfix.*/ - requires: - - Start E2E Tests - - - e2e-linux: - name: E2ETest (linux) - parallelism: 2 - requires: - - Build extension - Linux (stage) - - # 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 >> - - linux: - name: Build extension - Linux (stage) - env: stage - target: << pipeline.parameters.linux >> - requires: - - Validating build parameters - - store-build-artifacts: - name: Store build artifacts (stage) - requires: - - Build extension - Linux (stage) - - manual-build-mac: - when: << pipeline.parameters.mac >> - jobs: - - manual-build-validate: - name: Validating build parameters - os: mac - target: << pipeline.parameters.mac >> - - macosx: - name: Build extension - MacOS (stage) - env: stage - target: << pipeline.parameters.mac >> - requires: - - Validating build parameters - - store-build-artifacts: - name: Store build artifacts (stage) - requires: - - Build extension - MacOS (stage) - - manual-build-windows: - when: << pipeline.parameters.windows >> - jobs: - - manual-build-validate: - name: Validating build parameters - os: windows - target: << pipeline.parameters.windows >> - - windows: - name: Build extension - Windows (stage) - env: stage - target: << pipeline.parameters.windows >> - requires: - - Validating build parameters - - store-build-artifacts: - name: Store build artifacts (stage) - requires: - - Build extension - Windows (stage) - - # build vscode extension (dev) from "build" branches - build: - <<: *ignore-for-manual-build - jobs: - - linux: - name: Build extension - Linux (dev) - env: dev - <<: *buildFilter - - macosx: - name: Build extension - MacOS (dev) - env: dev - <<: *buildFilter - - windows: - name: Build extension - Windows (dev) - env: dev - <<: *buildFilter - - store-build-artifacts: - name: Store build artifacts (dev) - requires: - - Build extension - Linux (dev) - - Build extension - MacOS (dev) - - Build extension - Windows (dev) - - # Main workflow for release/* and latest branches only - release: - <<: *ignore-for-manual-build - jobs: - # unit tests (on any commit) - - unit-tests-ui: - name: Run Unit Tests - filters: &releaseAndLatestFilter - branches: - only: - - /^release.*/ - - latest - - # ================== STAGE ================== - # build extensions (stage) - - linux: - name: Build extension - Linux (stage) - <<: *stageFilter - - macosx: - name: Build extension - MacOS (stage) - <<: *stageFilter - - windows: - name: Build extension - Windows (stage) - <<: *stageFilter - # e2e tests on linux build - - e2e-linux: - name: E2ETest (linux) - parallelism: 2 - requires: - - Build extension - Linux (stage) - - - store-build-artifacts: - name: Store build artifacts (stage) - requires: - - Build extension - Linux (stage) - - Build extension - MacOS (stage) - - Build extension - Windows (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 extension - Linux (stage) - - Build extension - MacOS (stage) - - Build extension - Windows (stage) - - # ================== PROD ================== - # build and release vscode extension (prod) - - linux: - name: Build extension - Linux (prod) - env: prod - <<: *prodFilter - - macosx: - name: Build extension - MacOS (prod) - env: prod - <<: *prodFilter - - windows: - name: Build extension - Windows (prod) - env: prod - <<: *prodFilter - - # e2e tests on linux build - - e2e-linux: - name: E2ETest (Linux) - parallelism: 2 - requires: - - Build extension - Linux (prod) - - # upload release to prerelease AWS folder - - release-aws-private: - name: Release AWS S3 Private (prod) - requires: - - Build extension - Linux (prod) - - Build extension - MacOS (prod) - - Build extension - Windows (prod) - - # Manual approve for publish release - - approve-publish: - name: Approve Publish Release (prod) - type: approval - requires: - - Release AWS S3 Private (prod) - # # Publish release - # - publish-prod-aws: - # name: Publish AWS S3 - # requires: - # - Approve Publish Release (prod) - # <<: *prodFilter # double check for "latest" - - 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 00000000..4ea0e3c2 --- /dev/null +++ b/.circleci/config.yml.backup @@ -0,0 +1,610 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/configuration-reference +version: 2.1 + +aliases: + ui-deps-cache-key: &uiDepsCacheKey + key: ui-deps-{{ checksum "yarn.lock" }} + dev-filter: &devFilter + filters: + branches: + only: + - main + - /^build\/.*/ + build-filter: &buildFilter + filters: + branches: + only: + - /^build.*/ + stage-filter: &stageFilter + filters: + branches: + only: + - /^release.*/ + prod-filter: &prodFilter + filters: + branches: + only: + - latest + manual-build-conditions: &manual-build-conditions + or: + - << pipeline.parameters.linux >> + - << pipeline.parameters.mac >> + - << pipeline.parameters.windows >> + ignore-for-manual-build: &ignore-for-manual-build + when: + not: *manual-build-conditions + +orbs: + node: circleci/node@5.3.0 + win: circleci/windows@5.0.0 + aws: circleci/aws-cli@4.1.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 + +jobs: + unit-tests-ui: + executor: docker-node + steps: + - checkout + - restore_cache: + <<: *uiDepsCacheKey + - run: + name: Install dependencies + command: | + yarn install --frozen-lockfile --cache-folder ~/.cache/yarn + - run: + name: Run Unit tests UI + command: | + yarn test:cov + - save_cache: + <<: *uiDepsCacheKey + paths: + - ~/.cache/yarn + + e2e-linux: + 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: Start Xvfb + command: | + sudo apt-get install -y xvfb + Xvfb :99 -screen 0 1920x1080x24 & + - run: + name: Install dependencies + command: cd tests/e2e && yarn install + - run: + name: Compile TypeScript + command: cd tests/e2e && yarn compile + - run: + name: Verify Compiled Files + command: ls -R tests/e2e/dist/tests/ + - run: + name: Run e2e tests + command: | + cd tests/e2e/dist + export TEST_FILES=$(circleci tests glob "tests/**/*.e2e.js" | circleci tests split --split-by=timings) + echo "Running tests: $TEST_FILES" + cd ../../.. + .circleci/e2e/test.app.sh + - when: + condition: + equal: [ true, << parameters.report >> ] + steps: + - run: + name: Send report + when: always + command: | + APP_BUILD_TYPE="VSCode (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/mochawesome-report + - store_artifacts: + path: tests/e2e/mochawesome-report + destination: tests/e2e/mochawesome-report + - store_artifacts: + path: /tmp/test-resources/screenshots + destination: test-resources/screenshots + + # 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 >> + + linux: + executor: docker-node + 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 + - restore_cache: + <<: *uiDepsCacheKey + - attach_workspace: + at: . + - run: + name: Install dependencies + command: | + yarn install --frozen-lockfile --cache-folder ~/.cache/yarn + - run: + name: Build .vsix package + command: | + envFile=".env" + packagePath="./release/redis-for-vscode-extension-linux-x64.vsix" + yarn download:backend + + if [ << parameters.env >> == 'prod' ]; then + echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY'" >> $envFile + yarn package:prod --target linux-x64 --out ${packagePath} + exit 0; + fi + + echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY_STAGE'" >> $envFile + sed -i "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" $envFile + yarn package:stage --target linux-x64 --out ${packagePath} + - persist_to_workspace: + root: . + paths: + - release/redis-for-*.vsix + - save_cache: + <<: *uiDepsCacheKey + paths: + - ~/.cache/yarn + + macosx: + executor: macos + resource_class: macos.m1.medium.gen1 + 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: + node-version: '20.15' + - attach_workspace: + at: . + - run: + name: Install dependencies + command: | + yarn install + no_output_timeout: 15m + - run: + name: Build .vsix package + command: | + envFile=".env" + packagePath=./release/redis-for-vscode-extension-mac + + if [ << parameters.target >> ]; then + yarn download:backend << parameters.target >> + + echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY_STAGE'" >> $envFile + sed -i '' "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" $envFile + yarn package:stage --target darwin-<< parameters.target >> --out ${packagePath}-<< parameters.target >>.vsix + exit 0; + fi + + if [ << parameters.env >> == 'prod' ]; then + echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY'" >> $envFile + yarn download:backend arm64 + yarn package:prod --target darwin-arm64 --out ${packagePath}-arm64.vsix + + yarn download:backend x64 + yarn package:prod --target darwin-x64 --out ${packagePath}-x64.vsix + exit 0; + fi + + echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY_STAGE'" >> $envFile + sed -i '' "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" $envFile + yarn download:backend arm64 + yarn package:stage --target darwin-arm64 --out ${packagePath}-arm64.vsix + + yarn download:backend x64 + yarn package:stage --target darwin-x64 --out ${packagePath}-x64.vsix + - persist_to_workspace: + root: . + paths: + - release/redis-for-*.vsix + + 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: + name: Install dependencies + command: | + nvm install 20.15 + nvm use 20.15 + npm install --global yarn + + yarn install + shell: bash.exe + no_output_timeout: 15m + - run: + name: Build .vsix package + command: | + envFile=".env" + packagePath=./release/redis-for-vscode-extension-win-x64.vsix + yarn download:backend + + if [ << parameters.env >> == 'prod' ]; then + echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY'" >> $envFile + yarn package:prod --target win32-x64 --out ${packagePath} + exit 0; + fi + + sed -i "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" $envFile + yarn package:stage --target win32-x64 --out ${packagePath} + shell: bash.exe + no_output_timeout: 20m + - persist_to_workspace: + root: . + paths: + - release/redis-for-*.vsix + + # Release jobs + store-build-artifacts: + executor: linux-executor + steps: + - attach_workspace: + at: . + - store_artifacts: + path: release + destination: release + + release-aws-private: + executor: linux-executor + steps: + - checkout + - attach_workspace: + at: . + - store_artifacts: + path: release + destination: release + - run: + name: publish + command: | + chmod +x .circleci/build/sum_sha256.sh + .circleci/build/sum_sha256.sh + applicationVersion=$(jq -r '.version' package.json) + + aws s3 cp release/ s3://${AWS_BUCKET_NAME}/private/vscode/${applicationVersion} --recursive + + licenses-check: + executor: linux-executor + steps: + - checkout + - node/install: + install-yarn: true + node-version: '20.15' + - restore_cache: + <<: *uiDepsCacheKey + - run: + name: Run install all dependencies + command: | + yarn install + yarn --cwd tests/e2e install + - 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 + +# Orchestrate jobs using workflows +# See: https://circleci.com/docs/configuration-reference/#workflows +workflows: + license-checker: + jobs: + - licenses-check: + name: Run License checker + filters: + branches: + only: + - /^license.*/ + + frontend-tests: + <<: *ignore-for-manual-build + jobs: + - unit-tests-ui: + name: Run Unit Tests + filters: + branches: + only: + - /^feature.*/ + + e2e-tests: + jobs: + - approve: + name: Start E2E Tests + type: approval + filters: + branches: + only: + - /^e2e/feature.*/ + - /^e2e/bugfix.*/ + + - linux: + name: Build extension - Linux (stage) + env: stage + filters: + branches: + only: + - /^e2e/feature.*/ + - /^e2e/bugfix.*/ + requires: + - Start E2E Tests + + - e2e-linux: + name: E2ETest (linux) + parallelism: 2 + requires: + - Build extension - Linux (stage) + + # 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 >> + - linux: + name: Build extension - Linux (stage) + env: stage + target: << pipeline.parameters.linux >> + requires: + - Validating build parameters + - store-build-artifacts: + name: Store build artifacts (stage) + requires: + - Build extension - Linux (stage) + + manual-build-mac: + when: << pipeline.parameters.mac >> + jobs: + - manual-build-validate: + name: Validating build parameters + os: mac + target: << pipeline.parameters.mac >> + - macosx: + name: Build extension - MacOS (stage) + env: stage + target: << pipeline.parameters.mac >> + requires: + - Validating build parameters + - store-build-artifacts: + name: Store build artifacts (stage) + requires: + - Build extension - MacOS (stage) + + manual-build-windows: + when: << pipeline.parameters.windows >> + jobs: + - manual-build-validate: + name: Validating build parameters + os: windows + target: << pipeline.parameters.windows >> + - windows: + name: Build extension - Windows (stage) + env: stage + target: << pipeline.parameters.windows >> + requires: + - Validating build parameters + - store-build-artifacts: + name: Store build artifacts (stage) + requires: + - Build extension - Windows (stage) + + # build vscode extension (dev) from "build" branches + build: + <<: *ignore-for-manual-build + jobs: + - linux: + name: Build extension - Linux (dev) + env: dev + <<: *buildFilter + - macosx: + name: Build extension - MacOS (dev) + env: dev + <<: *buildFilter + - windows: + name: Build extension - Windows (dev) + env: dev + <<: *buildFilter + - store-build-artifacts: + name: Store build artifacts (dev) + requires: + - Build extension - Linux (dev) + - Build extension - MacOS (dev) + - Build extension - Windows (dev) + + # Main workflow for release/* and latest branches only + release: + <<: *ignore-for-manual-build + jobs: + # unit tests (on any commit) + - unit-tests-ui: + name: Run Unit Tests + filters: &releaseAndLatestFilter + branches: + only: + - /^release.*/ + - latest + + # ================== STAGE ================== + # build extensions (stage) + - linux: + name: Build extension - Linux (stage) + <<: *stageFilter + - macosx: + name: Build extension - MacOS (stage) + <<: *stageFilter + - windows: + name: Build extension - Windows (stage) + <<: *stageFilter + # e2e tests on linux build + - e2e-linux: + name: E2ETest (linux) + parallelism: 2 + requires: + - Build extension - Linux (stage) + + - store-build-artifacts: + name: Store build artifacts (stage) + requires: + - Build extension - Linux (stage) + - Build extension - MacOS (stage) + - Build extension - Windows (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 extension - Linux (stage) + - Build extension - MacOS (stage) + - Build extension - Windows (stage) + + # ================== PROD ================== + # build and release vscode extension (prod) + - linux: + name: Build extension - Linux (prod) + env: prod + <<: *prodFilter + - macosx: + name: Build extension - MacOS (prod) + env: prod + <<: *prodFilter + - windows: + name: Build extension - Windows (prod) + env: prod + <<: *prodFilter + + # e2e tests on linux build + - e2e-linux: + name: E2ETest (Linux) + parallelism: 2 + requires: + - Build extension - Linux (prod) + + # upload release to prerelease AWS folder + - release-aws-private: + name: Release AWS S3 Private (prod) + requires: + - Build extension - Linux (prod) + - Build extension - MacOS (prod) + - Build extension - Windows (prod) + + # Manual approve for publish release + - approve-publish: + name: Approve Publish Release (prod) + type: approval + requires: + - Release AWS S3 Private (prod) + # # Publish release + # - publish-prod-aws: + # name: Publish AWS S3 + # requires: + # - Approve Publish Release (prod) + # <<: *prodFilter # double check for "latest" + + 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/.env b/.env index 25ddeaf5..a1ba2a50 100644 --- a/.env +++ b/.env @@ -8,14 +8,14 @@ RI_APP_PORT=5541 RI_APP_VERSION='1.0.0' RI_APP_PREFIX='api' RI_APP_FOLDER_NAME='.redis-for-vscode' -RI_CDN_PATH='https://s3.amazonaws.com/redisinsight.download/public/releases/2.54.1/web-mini' +RI_CDN_PATH='https://s3.us-east-1.amazonaws.com/redisinsight.test/public/zalenski/vscode/web-mini' RI_WITHOUT_BACKEND=false # RI_WITHOUT_BACKEND=true RI_STDOUT_LOGGER=false RI_AUTO_BOOTSTRAP=false RI_MIGRATE_OLD_FOLDERS=false RI_BUILD_TYPE='VS_CODE' -RI_ENCRYPTION_KEYTAR=false RI_ANALYTICS_START_EVENTS=true RI_AGREEMENTS_PATH='../../webviews/resources/agreements-spec.json' +RI_ENCRYPTION_KEYTAR_SERVICE="redis-for-vscode" # RI_SEGMENT_WRITE_KEY='SEGMENT_WRITE_KEY' diff --git a/.eslintrc.js b/.eslintrc.js index 849d7afa..0ba652ab 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -86,6 +86,8 @@ module.exports = { 'function-paren-newline': 'off', 'prefer-regex-literals': 'off', 'react/display-name': 'off', + 'react/jsx-indent-props': [2, 2], + 'react/jsx-indent': [2, 2], 'no-promise-executor-return': 'off', 'import/order': [ 1, diff --git a/.github/actions/install-all-build-libs/action.yml b/.github/actions/install-all-build-libs/action.yml index 3d8142dc..3ea1fa54 100644 --- a/.github/actions/install-all-build-libs/action.yml +++ b/.github/actions/install-all-build-libs/action.yml @@ -8,7 +8,7 @@ runs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '20.15' + node-version: '20.18.0' - name: Install dependencies for root package.js shell: bash diff --git a/.github/workflows/pipeline-build-linux.yml b/.github/workflows/pipeline-build-linux.yml index 8339b0c9..f9519361 100644 --- a/.github/workflows/pipeline-build-linux.yml +++ b/.github/workflows/pipeline-build-linux.yml @@ -35,7 +35,7 @@ jobs: uses: ./.github/actions/download-backend - name: Set RI_SEGMENT_WRITE_KEY to .env file - run: echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY'" >> $envFile + run: echo "RI_SEGMENT_WRITE_KEY='${{ env.RI_SEGMENT_WRITE_KEY }}'" >> ${{ env.envFile }} - name: Build linux package (production) if: inputs.environment == 'production' @@ -45,7 +45,7 @@ jobs: - name: Build linux package (staging) if: inputs.environment == 'staging' run: | - sed -i "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" $envFile + sed -i "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" ${{ env.envFile }} yarn package:stage --target linux-x64 --out ${packagePath} - uses: actions/upload-artifact@v4 diff --git a/.github/workflows/pipeline-build-macos.yml b/.github/workflows/pipeline-build-macos.yml index d2296cab..c12a9b3e 100644 --- a/.github/workflows/pipeline-build-macos.yml +++ b/.github/workflows/pipeline-build-macos.yml @@ -28,7 +28,7 @@ jobs: uses: ./.github/actions/install-all-build-libs - name: Set RI_SEGMENT_WRITE_KEY to .env file - run: echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY'" >> $envFile + run: echo "RI_SEGMENT_WRITE_KEY='${{ env.RI_SEGMENT_WRITE_KEY }}'" >> ${{ env.envFile }} - name: Download backend x64 uses: ./.github/actions/download-backend @@ -38,7 +38,7 @@ jobs: - name: Build macos x64 package (staging) if: inputs.environment != 'production' run: | - sed -i '' "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" $envFile + sed -i '' "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" ${{ env.envFile }} yarn package:stage --target darwin-x64 --out ${packagePath}-x64.vsix @@ -55,7 +55,7 @@ jobs: - name: Build macos arm64 package (staging) if: inputs.environment != 'production' run: | - sed -i '' "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" $envFile + sed -i '' "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" ${{ env.envFile }} yarn package:stage --target darwin-arm64 --out ${packagePath}-arm64.vsix diff --git a/.github/workflows/pipeline-build-windows.yml b/.github/workflows/pipeline-build-windows.yml index c8b40802..dc3600e1 100644 --- a/.github/workflows/pipeline-build-windows.yml +++ b/.github/workflows/pipeline-build-windows.yml @@ -25,7 +25,7 @@ jobs: uses: ./.github/actions/download-backend - name: Set RI_SEGMENT_WRITE_KEY to .env file - run: echo "RI_SEGMENT_WRITE_KEY='$RI_SEGMENT_WRITE_KEY'" >> $envFile + run: echo "RI_SEGMENT_WRITE_KEY='${{ env.RI_SEGMENT_WRITE_KEY }}'" >> ${{ env.envFile }} - name: Build windows package (production) if: inputs.environment == 'production' @@ -35,7 +35,7 @@ jobs: - name: Build windows package (staging) if: inputs.environment == 'staging' run: | - sed -i "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" $envFile + sed -i "s/^RI_APP_FOLDER_NAME=.*/RI_APP_FOLDER_NAME='.redis-for-vscode-stage'/" ${{ env.envFile }} yarn package:stage --target win32-x64 --out ${{ env.packagePath }} - uses: actions/upload-artifact@v4 diff --git a/.github/workflows/tests-e2e-linux.yml b/.github/workflows/tests-e2e-linux.yml index df52448d..a9850a1d 100644 --- a/.github/workflows/tests-e2e-linux.yml +++ b/.github/workflows/tests-e2e-linux.yml @@ -28,7 +28,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '20.15' + node-version: '20.18.0' - name: Download linux artifact uses: actions/download-artifact@v4 diff --git a/.github/workflows/tests-frontend.yml b/.github/workflows/tests-frontend.yml index 207a4008..c13eabe2 100644 --- a/.github/workflows/tests-frontend.yml +++ b/.github/workflows/tests-frontend.yml @@ -3,7 +3,15 @@ on: workflow_call: env: + SLACK_AUDIT_REPORT_CHANNEL: ${{ secrets.SLACK_AUDIT_REPORT_CHANNEL }} SLACK_AUDIT_REPORT_KEY: ${{ secrets.SLACK_AUDIT_REPORT_KEY }} + 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 }} + REPORT_NAME: "report-vscode-fe" + S3_PATH: "report-vscode-fe" jobs: unit-tests: @@ -18,21 +26,30 @@ jobs: - name: Unit tests UI run: yarn test:cov - - name: Publish Test Results - uses: EnricoMi/publish-unit-test-result-action@v2 + - name: Get current date + id: date if: always() - with: - check_name: 'FE Unit tests summary' - comment_mode: 'failures' - files: reports/junit.xml + uses: RedisInsight/RedisInsight/.github/actions/get-current-date@873a0ebf55c85d3127bb4efb4d0636d9ab838226 - - name: Generate test results - uses: dorny/test-reporter@v1 + + - name: Deploy πŸš€ + if: always() + run: | + + GZIP_FILE=html.meta.json.gz + S3_SUB_PATH="test-reports/${{ steps.date.outputs.date }}/${{ github.run_id }}/${{ env.REPORT_NAME }}" + + aws s3 cp report/ s3://${AWS_BUCKET_NAME_TEST}/public/${S3_SUB_PATH} --recursive --exclude "*.gz" + + # s3 modified "gzip" content-type + # https://github.com/aws/aws-cli/issues/1131 + aws s3 cp report/${GZIP_FILE} s3://${AWS_BUCKET_NAME_TEST}/public/${S3_SUB_PATH}/${GZIP_FILE} --content-type "application/x-gzip" --metadata-directive REPLACE + + echo "S3_SUB_PATH=${S3_SUB_PATH}" >> $GITHUB_ENV + + + - name: Add link to report in the workflow summary if: always() - with: - name: 'Test results: FE unit tests' - path: reports/junit.xml - reporter: jest-junit - list-tests: 'failed' - list-suites: 'failed' - fail-on-error: 'false' + run: | + link="${{ vars.DEFAULT_TEST_REPORTS_URL }}/${S3_SUB_PATH}/index.html" + echo "[${link}](${link})" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index f82dd99e..97518b0a 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ coverage /.nyc_output **/coverage reports +report # IDEs and editors /.idea diff --git a/.vscode/settings.json b/.vscode/settings.json index bb1001f5..9653f327 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,9 @@ "source.fixAll.eslint": "explicit" }, "eslint.validate": ["javascript", "typescript"], + "eslint.workingDirectories": [ + { "directory": "./", "changeProcessCWD": true } + ], "files.associations": { "*.css": "postcss", "*.scss": "postcss" @@ -35,6 +38,7 @@ ".eslintcache": true, "bower_components": true, "release": true, + "src/webviews/public": true, "npm-debug.log.*": true, "tests/**/__snapshots__": true, "yarn.lock": true, @@ -44,7 +48,13 @@ "**/pnpm-lock.yaml": true, "**/test-extensions": true }, - "cSpell.words": ["githubocto", "tailwindcss", "webviews", "zustand"], + "cSpell.words": [ + "githubocto", + "keyspace", + "tailwindcss", + "webviews", + "zustand" + ], "testing.automaticallyOpenPeekView": "never", "[typescriptreact]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" diff --git a/.vscodeignore b/.vscodeignore index 033ae5f3..0fe0f708 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -28,6 +28,8 @@ vite.config.mjs # test coverage test +report +reports tests test-workspace .vscode-test diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 2d2ba56d..2d260197 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -41,7 +41,7 @@ "Key Name": "Key Name", " will be deleted.": " will be deleted.", "Delete": "Delete", - "will be deleted from Redis for VS Code.": "will be deleted from Redis for VS Code.", + "will be removed from Redis for VS Code.": "will be removed from Redis for VS Code.", "Key Size": "Key Size", "Key Size: ": "Key Size: ", "Length": "Length", @@ -295,9 +295,6 @@ "Host:": "Host:", "Database Index:": "Database Index:", "Modules:": "Modules:", - "Select Logical Database": "Select Logical Database", - "Database Index": "Database Index", - "Enter Database Index": "Enter Database Index", "No decompression": "No decompression", "Enable automatic data decompression": "Enable automatic data decompression", "Decompression format": "Decompression format", diff --git a/package.json b/package.json index e921933c..c23678bc 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "download:backend": "tsc ./scripts/downloadBackend.ts && node ./scripts/downloadBackend.js", "dev": "vite dev", "dev:key": "cross-env RI_DATA_ROUTE=main/key vite dev", + "dev:database": "cross-env RI_DATA_ROUTE=main/add_database vite dev", "dev:sidebar": "cross-env RI_DATA_ROUTE=sidebar vite dev", "l10n:collect": "npx @vscode/l10n-dev export -o ./l10n ./src", "watch": "tsc -watch -p ./", @@ -194,9 +195,9 @@ "@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react-swc": "^3.6.0", "@vitest/coverage-v8": "^1.3.1", - "@vitest/ui": "^1.3.1", + "@vitest/ui": "^1.6.0", "@vscode/l10n-dev": "^0.0.35", - "@vscode/vsce": "^3.0.0", + "@vscode/vsce": "^3.2.1", "cross-env": "^7.0.3", "csv-parser": "^3.0.0", "csv-stringify": "^6.5.1", @@ -236,6 +237,7 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.4.5", + "upath": "^2.0.1", "uuid": "^9.0.1", "vite": "^5.2.10", "vite-plugin-react-click-to-component": "^3.0.0", @@ -286,7 +288,7 @@ "react-inlinesvg": "^4.1.1", "react-monaco-editor": "^0.55.0", "react-router-dom": "^6.17.0", - "react-select": "^5.8.0", + "react-select": "^5.8.3", "react-spinners": "^0.13.8", "react-virtualized": "^9.22.5", "react-virtualized-auto-sizer": "^1.0.20", diff --git a/scripts/downloadBackend.ts b/scripts/downloadBackend.ts index 0d0caaaf..7550b5ee 100755 --- a/scripts/downloadBackend.ts +++ b/scripts/downloadBackend.ts @@ -3,8 +3,10 @@ import * as fs from 'fs' import * as path from 'path' import * as cp from 'child_process' import * as dotenv from 'dotenv' +import * as upath from 'upath' import { parse as parseUrl } from 'url' + dotenv.config({ path: [ path.join(__dirname, '..', '.env'), @@ -80,13 +82,28 @@ async function downloadRedisBackendArchive( }) } +function getNormalizedString(string: string) { + return string?.startsWith('D:') + ? upath.normalize(string).replace('D:', '/d') + : string +} + function unzipRedisServer(redisInsideArchivePath: string, extractDir: string) { // tar does not create extractDir by default if (!fs.existsSync(extractDir)) { fs.mkdirSync(extractDir) } - cp.spawnSync('tar', ['-xf', redisInsideArchivePath, '-C', extractDir, '--strip-components', '1', 'api']) + cp.spawnSync('tar', [ + '-xf', + getNormalizedString(redisInsideArchivePath), + '-C', + getNormalizedString(extractDir), + '--strip-components', + '1', + 'api', + ]) + // remove tutorials fs.rmSync(tutorialsPath, { recursive: true, force: true }); diff --git a/src/extension.ts b/src/extension.ts index 02f0d3de..6bd9af33 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -164,7 +164,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('RedisForVSCode.editDatabaseClose', (args) => { WebviewPanel.getInstance({ viewId: ViewId.EditDatabase }).dispose() - sidebarProvider.view?.webview.postMessage({ action: 'RefreshTree', data: args }) + sidebarProvider.view?.webview.postMessage({ action: 'UpdateDatabaseInList', data: args }) const keyDetailsWebview = WebviewPanel.instances[ViewId.Key] if (keyDetailsWebview) { diff --git a/src/resources/agreements-spec.json b/src/resources/agreements-spec.json index 71d31fdc..890abb4f 100644 --- a/src/resources/agreements-spec.json +++ b/src/resources/agreements-spec.json @@ -1,5 +1,5 @@ { - "version": "1.0.1", + "version": "1.0.2", "agreements": { "analytics": { "defaultValue": false, @@ -35,6 +35,37 @@ "title": "Server Side Public License", "label": "I have read and understood the Terms", "requiredText": "Accept the Server Side Public License" + }, + "encryption": { + "conditional": true, + "checker": "KEYTAR", + "defaultOption": "false", + "options": { + "true": { + "defaultValue": true, + "displayInSetting": false, + "required": false, + "editable": true, + "disabled": false, + "category": "privacy", + "since": "1.0.2", + "title": "Encryption", + "label": "Encrypt sensitive information", + "description": "Select to encrypt sensitive information using system keychain. Otherwise, this information is stored locally in plain text, which may incur security risk." + }, + "false": { + "defaultValue": false, + "displayInSetting": false, + "required": false, + "editable": true, + "disabled": true, + "category": "privacy", + "since": "1.0.2", + "title": "Encryption", + "label": "Encrypt sensitive information", + "description": "Install or enable the system keychain to encrypt and securely store your sensitive information added before using the application. Otherwise, this information will be stored locally in plain text and may lead to security risks." + } + } } } } diff --git a/src/webviews/src/actions/processCliAction.ts b/src/webviews/src/actions/processCliAction.ts index 305a3a69..4ebdf0ba 100644 --- a/src/webviews/src/actions/processCliAction.ts +++ b/src/webviews/src/actions/processCliAction.ts @@ -4,9 +4,11 @@ import { useDatabasesStore } from 'uiSrc/store' export const processCliAction = (message: CliAction) => { const prevDatabaseId = useDatabasesStore.getState().connectedDatabase?.id + const prevDatabaseIndex = useDatabasesStore.getState().connectedDatabase?.db const database = message?.data?.database + const dbIndex = database?.db ?? 0 - if (prevDatabaseId === database?.id) { + if (prevDatabaseId! + prevDatabaseIndex === database?.id + dbIndex) { return } window.ri.database = database diff --git a/src/webviews/src/components/auto-refresh/AutoRefresh.spec.tsx b/src/webviews/src/components/auto-refresh/AutoRefresh.spec.tsx new file mode 100644 index 00000000..62870f3c --- /dev/null +++ b/src/webviews/src/components/auto-refresh/AutoRefresh.spec.tsx @@ -0,0 +1,158 @@ +import React from 'react' +import { instance, mock } from 'ts-mockito' +import { fireEvent, screen, render, act } from 'testSrc/helpers' +import { AutoRefresh, Props } from './AutoRefresh' +import { DEFAULT_REFRESH_RATE } from './utils' + +const mockedProps = mock() + +const INLINE_ITEM_EDITOR = 'inline-item-editor' + +describe('AutoRefresh', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('prop "displayText = true" should show Refresh text', () => { + const { queryByTestId } = render() + + expect(queryByTestId('refresh-message-label')).toBeInTheDocument() + }) + + it('prop "displayText = false" should hide Refresh text', () => { + const { queryByTestId } = render() + + expect(queryByTestId('refresh-message-label')).not.toBeInTheDocument() + }) + + it('should call onRefresh', () => { + const onRefresh = vi.fn() + render() + + fireEvent.click(screen.getByTestId('refresh-btn')) + expect(onRefresh).toBeCalled() + }) + + it('refresh text should contain "Last refresh" time with disabled auto-refresh', async () => { + render() + + expect(screen.getByTestId('refresh-message-label')).toHaveTextContent(/Last refresh:/i) + expect(screen.getByTestId('refresh-message')).toHaveTextContent('now') + }) + + it('refresh text should contain "Auto-refresh" time with enabled auto-refresh', async () => { + render() + + fireEvent.click(screen.getByTestId('auto-refresh-config-btn')) + fireEvent.click(screen.getByTestId('auto-refresh-switch')) + + expect(screen.getByTestId('refresh-message-label')).toHaveTextContent(/Auto refresh:/i) + expect(screen.getByTestId('refresh-message')).toHaveTextContent(DEFAULT_REFRESH_RATE) + }) + + it('should locate refresh message label when testid is provided', () => { + render() + + expect(screen.getByTestId('testid-refresh-message-label')).toBeInTheDocument() + }) + + it('should locate refresh message when testid is provided', () => { + render() + + expect(screen.getByTestId('testid-refresh-message')).toBeInTheDocument() + }) + + it('should locate refresh button when testid is provided', () => { + render() + + expect(screen.getByTestId('testid-refresh-btn')).toBeInTheDocument() + }) + + it('should locate auto-refresh config button when testid is provided', () => { + render() + + expect(screen.getByTestId('testid-auto-refresh-config-btn')).toBeInTheDocument() + }) + + it('should locate auto-refresh switch when testid is provided', () => { + render() + + fireEvent.click(screen.getByTestId('testid-auto-refresh-config-btn')) + expect(screen.getByTestId('testid-auto-refresh-switch')).toBeInTheDocument() + }) + + describe('AutoRefresh Config', () => { + it('Auto refresh config should render', () => { + const { queryByTestId } = render() + + fireEvent.click(screen.getByTestId('auto-refresh-config-btn')) + expect(queryByTestId('auto-refresh-switch')).toBeInTheDocument() + }) + + it('should call onRefresh after enable auto-refresh and set 1 sec', async () => { + const onRefresh = vi.fn() + render() + + fireEvent.click(screen.getByTestId('auto-refresh-config-btn')) + fireEvent.click(screen.getByTestId('auto-refresh-switch')) + fireEvent.click(screen.getByTestId(INLINE_ITEM_EDITOR)) + + fireEvent.input(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value: '1' } }) + expect(screen.getByTestId(INLINE_ITEM_EDITOR)).toHaveValue('1') + + screen.getByTestId(/apply-btn/).click() + + await act(async () => { + await new Promise((r) => setTimeout(r, 1300)) + }) + expect(onRefresh).toBeCalledTimes(1) + + await act(async () => { + await new Promise((r) => setTimeout(r, 1300)) + }) + expect(onRefresh).toBeCalledTimes(2) + + await act(async () => { + await new Promise((r) => setTimeout(r, 1300)) + }) + expect(onRefresh).toBeCalledTimes(3) + }) + }) + + it('should NOT call onRefresh with disabled state', async () => { + const onRefresh = vi.fn() + const { rerender } = render() + + fireEvent.click(screen.getByTestId('auto-refresh-config-btn')) + fireEvent.click(screen.getByTestId('auto-refresh-switch')) + fireEvent.click(screen.getByTestId(INLINE_ITEM_EDITOR)) + fireEvent.input(screen.getByTestId(INLINE_ITEM_EDITOR), { target: { value: '1' } }) + + expect(screen.getByTestId(INLINE_ITEM_EDITOR)).toHaveValue('1') + + screen.getByTestId(/apply-btn/).click() + + await act(() => { + rerender() + }) + + await act(async () => { + await new Promise((r) => setTimeout(r, 1300)) + }) + expect(onRefresh).toBeCalledTimes(0) + + await act(async () => { + await new Promise((r) => setTimeout(r, 1300)) + }) + expect(onRefresh).toBeCalledTimes(0) + + await act(() => { + rerender() + }) + + await act(async () => { + await new Promise((r) => setTimeout(r, 1300)) + }) + expect(onRefresh).toBeCalledTimes(1) + }) +}) diff --git a/src/webviews/src/components/auto-refresh/AutoRefresh.tsx b/src/webviews/src/components/auto-refresh/AutoRefresh.tsx new file mode 100644 index 00000000..fc4ede88 --- /dev/null +++ b/src/webviews/src/components/auto-refresh/AutoRefresh.tsx @@ -0,0 +1,248 @@ +import React, { useEffect, useState } from 'react' +import * as l10n from '@vscode/l10n' +import Popover from 'reactjs-popup' +import cx from 'classnames' +import { VscChevronDown, VscRefresh } from 'react-icons/vsc' + +import { + MIN_REFRESH_RATE, + StorageItem, +} from 'uiSrc/constants' +import { + errorValidateRefreshRateNumber, + validateRefreshRateNumber, +} from 'uiSrc/utils' +import { InlineEditor } from 'uiSrc/components' +import { localStorageService } from 'uiSrc/services' +import { Nullable } from 'uiSrc/interfaces' +import { Checkbox, RiButton, Tooltip } from 'uiSrc/ui' +import { + getTextByRefreshTime, + DEFAULT_REFRESH_RATE, + DURATION_FIRST_REFRESH_TIME, + MINUTE, + NOW, +} from './utils' + +import styles from './styles.module.scss' + +export interface Props { + postfix: string + loading: boolean + displayText?: boolean + lastRefreshTime: Nullable + testid?: string + containerClassName?: string + turnOffAutoRefresh?: boolean + onRefresh: (enableAutoRefresh: boolean) => void + onRefreshClicked?: () => void + onEnableAutoRefresh?: (enableAutoRefresh: boolean, refreshRate: string) => void + onChangeAutoRefreshRate?: (enableAutoRefresh: boolean, refreshRate: string) => void + disabled?: boolean + enableAutoRefreshDefault?: boolean +} + +const TIMEOUT_TO_UPDATE_REFRESH_TIME = 1_000 * MINUTE // once a minute + +const AutoRefresh = React.memo(({ + postfix, + loading, + displayText = true, + lastRefreshTime, + containerClassName = '', + testid = '', + turnOffAutoRefresh, + onRefresh, + onRefreshClicked, + onEnableAutoRefresh, + onChangeAutoRefreshRate, + disabled, + enableAutoRefreshDefault = false, +}: Props) => { + let intervalText: NodeJS.Timeout + let intervalRefresh: NodeJS.Timeout + + const [refreshMessage, setRefreshMessage] = useState(NOW) + const [isPopoverOpen, setIsPopoverOpen] = useState(false) + const [refreshRate, setRefreshRate] = useState('') + const [refreshRateMessage, setRefreshRateMessage] = useState('') + const [enableAutoRefresh, setEnableAutoRefresh] = useState(enableAutoRefreshDefault) + + const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen) + const closePopover = () => { + setEnableAutoRefresh(enableAutoRefresh) + setIsPopoverOpen(false) + } + + useEffect(() => { + const refreshRateStorage = localStorageService.get(StorageItem.autoRefreshRate + postfix) + || DEFAULT_REFRESH_RATE + + setRefreshRate(refreshRateStorage) + }, [postfix]) + + useEffect(() => { + if (turnOffAutoRefresh && enableAutoRefresh) { + setEnableAutoRefresh(false) + clearInterval(intervalRefresh) + } + }, [turnOffAutoRefresh]) + + // update refresh label text + useEffect(() => { + const delta = getLastRefreshDelta(lastRefreshTime) + updateLastRefresh() + + intervalText = setInterval(() => { + if (document.hidden) return + + updateLastRefresh() + }, delta < DURATION_FIRST_REFRESH_TIME ? DURATION_FIRST_REFRESH_TIME : TIMEOUT_TO_UPDATE_REFRESH_TIME) + return () => clearInterval(intervalText) + }, [lastRefreshTime]) + + // refresh interval + useEffect(() => { + updateLastRefresh() + + if (enableAutoRefresh && !loading && !disabled) { + intervalRefresh = setInterval(() => { + if (document.hidden) return + + handleRefresh() + }, +refreshRate * 1_000) + } else { + clearInterval(intervalRefresh) + } + + if (enableAutoRefresh) { + updateAutoRefreshText(refreshRate) + } + + return () => clearInterval(intervalRefresh) + }, [enableAutoRefresh, refreshRate, loading, disabled, lastRefreshTime]) + + const getLastRefreshDelta = (time:Nullable) => (Date.now() - (time || 0)) / 1_000 + + const getDataTestid = (suffix: string) => (testid ? `${testid}-${suffix}` : suffix) + + const updateLastRefresh = () => { + const delta = getLastRefreshDelta(lastRefreshTime) + const text = getTextByRefreshTime(delta, lastRefreshTime ?? 0) + + lastRefreshTime && setRefreshMessage(text) + } + + const updateAutoRefreshText = (refreshRate: string) => { + enableAutoRefresh && setRefreshRateMessage( + // more than 1 minute + +refreshRate > MINUTE ? `${Math.floor(+refreshRate / MINUTE)} min` : `${refreshRate} s`, + ) + } + + const handleApplyAutoRefreshRate = (initValue: string) => { + const value = +initValue >= MIN_REFRESH_RATE ? initValue : `${MIN_REFRESH_RATE}` + setRefreshRate(value) + localStorageService.set(StorageItem.autoRefreshRate + postfix, value) + onChangeAutoRefreshRate?.(enableAutoRefresh, value) + } + + const handleRefresh = () => { + onRefresh(enableAutoRefresh) + } + + const handleRefreshClick = () => { + handleRefresh() + onRefreshClicked?.() + } + + const onChangeEnableAutoRefresh = (value: boolean) => { + setEnableAutoRefresh(value) + + onEnableAutoRefresh?.(value, refreshRate) + } + + return ( +
+ + {displayText && ( + {enableAutoRefresh ? l10n.t('Auto refresh:') : l10n.t('Last refresh:')} + )} + {displayText && ( + {` ${enableAutoRefresh ? refreshRateMessage : refreshMessage}`} + )} + + + + + + + + + + + + )} + > +
+ onChangeEnableAutoRefresh(e.target.checked)} + data-testid={getDataTestid('auto-refresh-switch')} + className={styles.switchOption} + labelText={l10n.t('Auto Refresh')} + /> +
+
+
Refresh rate:
+
+ handleApplyAutoRefreshRate(value)} + /> +
+
+
+
+ ) +}) + +export { AutoRefresh } diff --git a/src/webviews/src/components/auto-refresh/styles.module.scss b/src/webviews/src/components/auto-refresh/styles.module.scss new file mode 100644 index 00000000..d34f4f3e --- /dev/null +++ b/src/webviews/src/components/auto-refresh/styles.module.scss @@ -0,0 +1,61 @@ +.container { + @apply flex justify-center items-center relative whitespace-nowrap; +} + +.btn { + transition: transform 0.3s ease; + + &.rolling svg { + color: var(--vscode-button-background) !important; + } +} + +.time { + @apply pr-1.5; + + &.disabled { + @apply opacity-50; + } +} + + +:global(.popover-auto-refresh-content) { + @apply max-w-[225px] w-[225px] #{!important}; + +} + +.switch { + @apply pb-4; +} + +.input { + @apply h-[30px] inline-block w-[90px]; + + input { + @apply h-[30px]; + } +} +.inputContainer { + @apply h-[30px]; +} + +.inputLabel { + @apply inline-block w-20; +} + +.anchorBtn { + @apply min-h-[14px] min-w-[14px] -ml-[3px]; + + svg { + @apply w-[10px] h-[10px]; + } +} + +.enable { + svg { + fill: var(--vscode-button-background) !important; + } + .time { + color: var(--vscode-button-background) !important; + } +} diff --git a/src/webviews/src/components/auto-refresh/utils.ts b/src/webviews/src/components/auto-refresh/utils.ts new file mode 100644 index 00000000..d2f8261b --- /dev/null +++ b/src/webviews/src/components/auto-refresh/utils.ts @@ -0,0 +1,23 @@ +import * as l10n from '@vscode/l10n' +import { truncateNumberToFirstUnit } from 'uiSrc/utils' + +export const NOW = l10n.t('now') +export const MINUTE = 60 +export const DURATION_FIRST_REFRESH_TIME = 5 +export const DEFAULT_REFRESH_RATE = '5.0' + +export const getTextByRefreshTime = (delta: number, lastRefreshTime: number) => { + let text = '' + + if (delta > MINUTE) { + text = truncateNumberToFirstUnit((Date.now() - (lastRefreshTime || 0)) / 1_000) + } + if (delta < MINUTE) { + text = '< 1 min' + } + if (delta < DURATION_FIRST_REFRESH_TIME) { + text = NOW + } + + return text +} diff --git a/src/webviews/src/components/database-form/DbIndex.tsx b/src/webviews/src/components/database-form/DbIndex.tsx deleted file mode 100644 index c9a87cbd..00000000 --- a/src/webviews/src/components/database-form/DbIndex.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { ChangeEvent, useId } from 'react' -import cx from 'classnames' -import { FormikProps } from 'formik' -import * as l10n from '@vscode/l10n' -import { VSCodeDivider } from '@vscode/webview-ui-toolkit/react' - -import { validateNumber } from 'uiSrc/utils' -import { DbConnectionInfo } from 'uiSrc/interfaces' -import { Checkbox, InputText, CheckboxChangeEvent } from 'uiSrc/ui' - -export interface Props { - - formik: FormikProps -} - -const DbIndex = (props: Props) => { - const { formik } = props - - const id = useId() - - const handleChangeDbIndexCheckbox = (e: CheckboxChangeEvent): void => { - const isChecked = e.target.checked - if (!isChecked) { - // Reset db field to initial value - formik.setFieldValue('db', null) - } - formik.setFieldValue('showDb', isChecked) - } - - return ( - <> -
- -
- - {formik.values.showDb && ( -
- ) => { - formik.setFieldValue( - e.target.name, - validateNumber(e.target.value.trim()), - ) - }} - type="text" - min={0} - /> -
- )} - - ) -} - -export { DbIndex } diff --git a/src/webviews/src/components/database-form/TlsDetails.tsx b/src/webviews/src/components/database-form/TlsDetails.tsx index ddd70cd6..d4398d89 100644 --- a/src/webviews/src/components/database-form/TlsDetails.tsx +++ b/src/webviews/src/components/database-form/TlsDetails.tsx @@ -2,40 +2,61 @@ import React, { ChangeEvent, useId } from 'react' import cx from 'classnames' import { FormikProps } from 'formik' import * as l10n from '@vscode/l10n' -import { VSCodeDivider } from '@vscode/webview-ui-toolkit/react' import { CheckboxChangeEvent } from 'rc-checkbox' +import { find } from 'lodash' +import { MenuListProps } from 'react-select' -import { validateCertName, validateField } from 'uiSrc/utils' +import { sendEventTelemetry, TelemetryEvent, validateCertName, validateField } from 'uiSrc/utils' import { ADD_NEW, ADD_NEW_CA_CERT, - ADD_NEW_CA_CERT_LABEL, - ADD_NEW_LABEL, + ApiEndpoints, NO_CA_CERT, - NO_CA_CERT_LABEL, } from 'uiSrc/constants' -import { DbConnectionInfo } from 'uiSrc/interfaces' -import { Checkbox, InputText, Select, TextArea } from 'uiSrc/ui' +import { DbConnectionInfo, RedisString } from 'uiSrc/interfaces' +import { Checkbox, InputText, TextArea } from 'uiSrc/ui' +import { removeCertAction } from 'uiSrc/store' +import { SuperSelectRemovableOption, SuperSelect, SuperSelectOption } from 'uiSrc/components' import styles from './styles.module.scss' +const suffix = '_tls_details' + export interface Props { formik: FormikProps caCertificates?: { id: string, name: string }[] certificates?: { id: string, name: string }[] } + const TlsDetails = (props: Props) => { const { formik, caCertificates, certificates } = props const id = useId() - const optionsCertsCA = [ - { - value: NO_CA_CERT, - label: NO_CA_CERT_LABEL, - }, - { - value: ADD_NEW_CA_CERT, - label: ADD_NEW_CA_CERT_LABEL, - }, + const handleDeleteCaCert = (id: RedisString, onSuccess?: () => void) => { + removeCertAction(id, ApiEndpoints.CA_CERTIFICATES, () => { + onSuccess?.() + handleClickDeleteCert('CA') + }) + } + + const handleDeleteClientCert = (id: RedisString, onSuccess?: () => void) => { + removeCertAction(id, ApiEndpoints.CLIENT_CERTIFICATES, () => { + onSuccess?.() + handleClickDeleteCert('Client') + }) + } + + const handleClickDeleteCert = (certificateType: 'Client' | 'CA') => { + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_CERTIFICATE_REMOVED, + eventData: { + certificateType, + }, + }) + } + + const optionsCertsCA: SuperSelectOption[] = [ + NO_CA_CERT, + ADD_NEW_CA_CERT, ] caCertificates?.forEach((cert) => { @@ -45,12 +66,7 @@ const TlsDetails = (props: Props) => { }) }) - const optionsCertsClient = [ - { - value: ADD_NEW, - label: ADD_NEW_LABEL, - }, - ] + const optionsCertsClient: SuperSelectOption[] = [ADD_NEW] certificates?.forEach((cert) => { optionsCertsClient.push({ @@ -130,17 +146,22 @@ const TlsDetails = (props: Props) => {
{`${l10n.t('CA Certificate')}${formik.values.verifyServerTlsCert ? '*' : ''}`}
-